-
Notifications
You must be signed in to change notification settings - Fork 415
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #447 from Crzyrndm/feature/benchmark
microbenchmarks for double<->string conversion, serialisation improvements
- Loading branch information
Showing
20 changed files
with
670 additions
and
163 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# FetchContent added in cmake v3.11 | ||
# https://cmake.org/cmake/help/v3.11/module/FetchContent.html | ||
# this file is behind a feature flag (XLNT_MICROBENCH_ENABLED) so the primary build is not affected | ||
cmake_minimum_required(VERSION 3.11) | ||
project(xlnt_ubench) | ||
|
||
# acquire google benchmark dependency | ||
# disable generation of the various test projects | ||
set(BENCHMARK_ENABLE_TESTING OFF) | ||
# gtest not required | ||
set(BENCHMARK_ENABLE_GTEST_TESTS OFF) | ||
|
||
include(FetchContent) | ||
FetchContent_Declare( | ||
googlebenchmark | ||
GIT_REPOSITORY https://github.com/google/benchmark | ||
GIT_TAG v1.5.0 | ||
) | ||
# download if not already present | ||
FetchContent_GetProperties(googlebenchmark) | ||
if(NOT googlebenchmark_POPULATED) | ||
FetchContent_Populate(googlebenchmark) | ||
add_subdirectory(${googlebenchmark_SOURCE_DIR} ${googlebenchmark_BINARY_DIR}) | ||
endif() | ||
# equivalent of add_subdirectory, now available for use | ||
FetchContent_MakeAvailable(googlebenchmark) | ||
|
||
|
||
add_executable(xlnt_ubench) | ||
target_sources(xlnt_ubench | ||
PRIVATE | ||
string_to_double.cpp | ||
double_to_string.cpp | ||
) | ||
target_link_libraries(xlnt_ubench benchmark_main xlnt) | ||
target_compile_features(xlnt_ubench PRIVATE cxx_std_17) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
// 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 |
Oops, something went wrong.