Comparing 3 modern c++ ways to convert integral values to strings
I was trying to pick a standard way to convert integrals to strings, so I went on and did a small performance evaluation by measuring the execution time of 3 methods
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <chrono>
#include <random>
#include <exception>
#include <type_traits>
#include <boost/lexical_cast.hpp>
using namespace std;
// 1. A way to easily measure elapsed time -------------------
template<typename TimeT = std::chrono::milliseconds>
struct measure
{
template<typename F>
static typename TimeT::rep execution(F const &func)
{
auto start = std::chrono::system_clock::now();
func();
auto duration = std::chrono::duration_cast< TimeT>(
std::chrono::system_clock::now() - start);
return duration.count();
}
};
// -----------------------------------------------------------
// 2. Define the conversion functions ========================
template<typename T> // A. Using stringstream ================
string StringFromNumber_SS(T const &value) {
stringstream ss;
ss << value;
return ss.str();
}
template<typename T> // B. Using boost::lexical_cast =========
string StringFromNumber_LC(T const &value) {
return boost::lexical_cast<string>(value);
}
template<typename T> // C. Using c++11 to_string() ===========
string StringFromNumber_C11(T const &value) {
return std::to_string(value);
}
// ===========================================================
// 3. A wrapper to measure the different executions ----------
template<typename T, typename F>
long long MeasureExec(std::vector<T> const &v1, F const &func)
{
return measure<>::execution([&]() {
for (auto const &i : v1) {
if (func(i) != StringFromNumber_LC(i)) {
throw std::runtime_error("FAIL");
}
}
});
}
// -----------------------------------------------------------
// 4. Machinery to generate random numbers into a vector -----
template<typename T>
typename std::enable_if<std::is_integral<T>::value>::type
FillVec(vector<T> &v)
{
std::mt19937 e2(1);
std::uniform_int_distribution<> dist(3, 1440);
std::generate(v.begin(), v.end(), [&]() { return dist(e2); });
}
template<typename T>
typename std::enable_if<!std::is_integral<T>::value>::type
FillVec(vector<T> &v)
{
std::mt19937 e2(1);
std::uniform_real_distribution<> dist(-1440., 1440.);
std::generate(v.begin(), v.end(), [&]() { return dist(e2); });
}
// -----------------------------------------------------------
int main()
{
std::vector<int> v1(991908);
FillVec(v1);
cout << "C++ 11 method ......... " <<
MeasureExec(v1, StringFromNumber_C11<int>) << endl;
cout << "String stream method .. " <<
MeasureExec(v1, StringFromNumber_SS<int>) << endl;
cout << "Lexical cast method ... " <<
MeasureExec(v1, StringFromNumber_LC<int>) << endl;
return 0;
}
A typical output (running Release in VS2013 which implies /O2 optimization flag) would be
C++ 11 method ......... 273
String stream method .. 1923
Lexical cast method ... 222
UPDATE
Alternatively an online run on gcc with
g++ -std=c++11 -Ofast -march=native -Wall -pedantic main.cpp && ./a.out
C++ 11 method ......... 414
String stream method .. 1538
Lexical cast method ... 275
Disclaimer : Results are to be compared among each other and not across machines
Questions
1. Why is the string stream method consistently the worst (by an order of magnitude)? Should it be viewed as deprecated now that faster alternatives emerged?
2. Why is lexical cast consistently the best? Can we assume that this is the fastest implementation?
Please feel free to tweak and play with your versions of this code. I'd appreciate your insights on the topic.
PS
The code that was actually run, had only one measurement per main()
. Here all were 3 were presented together to save space.
Optimization flags are compiler specific or application mandated. I'm just providing the code blocks to perform the tests and expect from SO users to chip in with their results or suggestions to what the optimum configuration per compiler would be (for what it's worth I provided the flags used here).
The code works for any numeric to string conversion (it takes changing the type of v1
in main
). sehe did for double
(mentioned in his answer's comment). It's a good idea to play with that too.
Solution 1:
Question 1. Why is the string stream method consistently the worst?
The classical mistake: creating a new stringstream every single time
template<typename T> // 1. Using stringstream
string StringFromIntegral_SS(T const &value) {
thread_local stringstream ss;
ss.str("");
ss.clear();
ss << value;
return ss.str();
}
Question 2. Why is lexical cast consistently the best? Can we assume that this is the fastest implementation ?
Because it's most specialized; and, no, faster implementations exist. FastFormat and Boost Spirit have competitive offerings, as far as I know.
Update Boost Spirit Karma still easily beats the bunch:
template<typename T> // 4. Karma to string
std::string StringFromIntegral_K(T const &value) {
thread_local auto const gen = boost::spirit::traits::create_generator<T>::call();
thread_local char buf[20];
char* it = buf;
boost::spirit::karma::generate(it, gen, value);
return std::string(buf, it);
}
Timings:
C++ 11 method 111
String stream method 103
Lexical cast method 57
Spirit Karma method 36
Spirit Karma method with string_ref 13
See it Live On Coliru Clang or GCC
BONUS
Just to goof off, a version using boost::string_ref
is much faster still due the reduced allocations:
template<typename T> // 5. Karma to string_ref
boost::string_ref StringFromIntegral_KSR(T const &value) {
thread_local auto const gen = boost::spirit::traits::create_generator<T>::call();
thread_local char buf[20];
char* it = buf;
boost::spirit::karma::generate(it, gen, value);
return boost::string_ref(buf, it-buf);
}
I've tested all modified methods for correctness using an asserting test loop:
return measure<>::execution(
//[&]() { for (auto const &i : v1) { func(i); }});
[&]() { for (auto const &i : v1) { assert(func(i) == StringFromIntegral_LC(i)); }});