xlnt/benchmarks/microbenchmarks/double_to_string.cpp
JCrawfy ee593c2673 bug fixes, move the faster serialisation into the numeric header
serialisation ends up roughly 2x improvement going from sstream to snprintf

Run on (4 X 3500 MHz CPU s)
CPU Caches:
  L1 Data 32K (x4)
  L1 Instruction 32K (x4)
  L2 Unified 262K (x4)
  L3 Unified 6291K (x1)
-------------------------------------------------------------------------------------------------------------
Benchmark                                                                   Time             CPU   Iterations
-------------------------------------------------------------------------------------------------------------
RandFloatStrs/double_from_string_sstream                                  968 ns          977 ns       640000
RandFloatStrs/double_from_string_strtod                                   272 ns          270 ns      2488889
RandFloatStrs/double_from_string_strtod_fixed                             272 ns          270 ns      2488889
RandFloatStrs/double_from_string_strtod_fixed_const_ref                   273 ns          270 ns      2488889
RandFloatStrs/double_from_string_std_from_chars                           193 ns          195 ns      3446154
RandFloatCommaStrs/double_from_string_strtod_fixed_comma_ref              272 ns          273 ns      2635294
RandFloatCommaStrs/double_from_string_strtod_fixed_comma_const_ref        276 ns          273 ns      2635294
RandFloats/string_from_double_sstream                                    1311 ns         1318 ns       497778
RandFloats/string_from_double_sstream_cached                             1076 ns         1050 ns       640000
RandFloats/string_from_double_snprintf                                    601 ns          600 ns      1120000
RandFloats/string_from_double_snprintf_fixed                              600 ns          600 ns      1120000
RandFloats/string_from_double_std_to_chars                                117 ns          117 ns      5600000
RandFloatsComma/string_from_double_snprintf_fixed_comma                   600 ns          600 ns      1120000
2020-03-01 22:01:14 +13:00

207 lines
5.3 KiB
C++

// A core part of the xlsx serialisation routine is taking doubles from memory and stringifying them
// this has a few requirements
// - expect strings in the form 1234.56 (i.e. no thousands seperator, '.' used for the decimal seperator)
// - outputs up to 15 significant figures (excel only serialises numbers up to 15sf)
#include "benchmark/benchmark.h"
#include <locale>
#include <random>
#include <sstream>
namespace {
// setup a large quantity of random doubles as strings
template <bool Decimal_Locale = true>
class RandomFloats : public benchmark::Fixture
{
static constexpr size_t Number_of_Elements = 1 << 20;
static_assert(Number_of_Elements > 1'000'000, "ensure a decent set of random values is generated");
std::vector<double> inputs;
size_t index = 0;
const char *locale_str = nullptr;
public:
void SetUp(const ::benchmark::State &state)
{
if (Decimal_Locale)
{
locale_str = setlocale(LC_ALL, "C");
}
else
{
locale_str = setlocale(LC_ALL, "de-DE");
}
std::random_device rd; // obtain a seed for the random number engine
std::mt19937 gen(rd());
// doing full range is stupid (<double>::min/max()...), it just ends up generating very large numbers
// uniform is probably not the best distribution to use here, but it will do for now
std::uniform_real_distribution<double> dis(-1'000, 1'000);
// generate a large quantity of doubles to deserialise
inputs.reserve(Number_of_Elements);
for (int i = 0; i < Number_of_Elements; ++i)
{
double d = dis(gen);
inputs.push_back(d);
}
}
void TearDown(const ::benchmark::State &state)
{
// restore locale
setlocale(LC_ALL, locale_str);
// gbench is keeping the fixtures alive somewhere, need to clear the data after use
inputs = std::vector<double>{};
}
double &get_rand()
{
return inputs[++index & (Number_of_Elements - 1)];
}
};
/// Takes in a double and outputs a string form of that number which will
/// serialise and deserialise without loss of precision
std::string serialize_number_to_string(double num)
{
// more digits and excel won't match
constexpr int Excel_Digit_Precision = 15; //sf
std::stringstream ss;
ss.precision(Excel_Digit_Precision);
ss << num;
return ss.str();
}
class number_serialiser
{
static constexpr int Excel_Digit_Precision = 15; //sf
std::ostringstream ss;
public:
explicit number_serialiser()
{
ss.precision(Excel_Digit_Precision);
ss.imbue(std::locale("C"));
}
std::string serialise(double d)
{
ss.str(""); // reset string buffer
ss.clear(); // reset any error flags
ss << d;
return ss.str();
}
};
class number_serialiser_mk2
{
static constexpr int Excel_Digit_Precision = 15; //sf
bool should_convert_comma;
void convert_comma(char *buf, int len)
{
char *buf_end = buf + len;
char *decimal = std::find(buf, buf_end, ',');
if (decimal != buf_end)
{
*decimal = '.';
}
}
public:
explicit number_serialiser_mk2()
: should_convert_comma(std::use_facet<std::numpunct<char>>(std::locale{}).decimal_point() == ',')
{
}
std::string serialise(double d)
{
char buf[Excel_Digit_Precision + 1]; // need space for trailing '\0'
int len = snprintf(buf, sizeof(buf), "%.15g", d);
if (should_convert_comma)
{
convert_comma(buf, len);
}
return std::string(buf, len);
}
};
using RandFloats = RandomFloats<true>;
using RandFloatsComma = RandomFloats<false>;
} // namespace
BENCHMARK_F(RandFloats, string_from_double_sstream)
(benchmark::State &state)
{
while (state.KeepRunning())
{
benchmark::DoNotOptimize(
serialize_number_to_string(get_rand()));
}
}
BENCHMARK_F(RandFloats, string_from_double_sstream_cached)
(benchmark::State &state)
{
number_serialiser ser;
while (state.KeepRunning())
{
benchmark::DoNotOptimize(
ser.serialise(get_rand()));
}
}
BENCHMARK_F(RandFloats, string_from_double_snprintf)
(benchmark::State &state)
{
while (state.KeepRunning())
{
char buf[16];
int len = snprintf(buf, sizeof(buf), "%.15g", get_rand());
benchmark::DoNotOptimize(
std::string(buf, len));
}
}
BENCHMARK_F(RandFloats, string_from_double_snprintf_fixed)
(benchmark::State &state)
{
number_serialiser_mk2 ser;
while (state.KeepRunning())
{
benchmark::DoNotOptimize(
ser.serialise(get_rand()));
}
}
// locale names are different between OS's, and std::from_chars is only complete in MSVC
#ifdef _MSC_VER
#include <charconv>
BENCHMARK_F(RandFloats, string_from_double_std_to_chars)
(benchmark::State &state)
{
while (state.KeepRunning())
{
char buf[16];
std::to_chars_result result = std::to_chars(buf, buf + std::size(buf), get_rand());
benchmark::DoNotOptimize(
std::string(buf, result.ptr));
}
}
BENCHMARK_F(RandFloatsComma, string_from_double_snprintf_fixed_comma)
(benchmark::State &state)
{
number_serialiser_mk2 ser;
while (state.KeepRunning())
{
benchmark::DoNotOptimize(
ser.serialise(get_rand()));
}
}
#endif