How do you convert a number to a string in C++ with MSVC...

| 4 Comments

Converting a numeric type to a string format in C++ is one of those problems that was 'solved' a long time ago. The 'standard' method usually involves streaming the type to be converted into a std::stringstream and then extracting the resulting string representation. Something like this, perhaps:

#include<iostream>
#include<string>
#include<sstream>
  
using namespace std;
  
string itos(int i)	// convert int to string
{
	stringstream s;
	s << i;
	return s.str();
}
  
int main()
{
	int i = 127;
	string ss = itos(i);
	const char* p = ss.c_str();
	cout << ss << " " << p << endl;
}

This style of conversion is usually the first response to any new C++ programmer's request for help with this task; see here, here and here. More modern practitioners might suggest using boost::lexical_cast (docs here) which is, as expected, somewhat more complicated, but which often uses std::stringstream internally... I have had some code in my libraries that does essentially the same thing for almost 10 years now. It uses std::ostringstream but in other ways is similar to the first example.

So why am I bothering to talk about this? Well, not so long ago I was running my lock explorer tool on a server that I was adjusting. I wanted to make sure that I didn't introduce any lock inversions into a fairly complex server that I hadn't worked on in a long time and so I ran the server under my lock explorer tool so that the tool could report on lock inversions if I introduced any. The tool also reports on lock usage and contention and the contention report included one lock that was very very hot. The lock was one that was used by the STL that ships with Microsoft's Visual Studio and that was locked every time my 'number to string conversion' code was executed. The lock was one that protected access to the 'locale' that was used by the conversion operation. When an std::ostringstream is constructed it calls through to std::ios_base::_Init() which eventually calls std::locale::facet::_Incref() which uses a critical section to lock around a reference count update. It actually locks and unlocks this lock several times during construction and then during the actual conversion the insertion operator locks itself (using a different critical section) and then the locale lock is locked again when the facet is used... Etc, etc.

My relatively simple looking piece of code that outwardly gave no indication that it was thread-unfriendly was behaving in a most thread-unfriendly manner. Of course this may not be important to you; the convenience of the 'stream to convert' style of conversion may outweigh the potential multi-threaded use performance hit. Unfortunately for me that wasn't the case. Since the utility function only needs to be written and tested once and is then reused in many, many disparate pieces of code the fact that it was badly behaved when used from multiple threads at the same time was a reasonably big issue for me. In a rather contrived test which ran more threads than there were CPUs and where each thread was doing repeated numeric to string conversions, the streaming version of the code pegged a single CPU at 100% and left the rest of the CPUs pretty much idle... Note that the equivalent code using STLPort 5.1.5 doesn't exhibit the same issues, but previous versions of Visual C++'s STL do.

Since I can't mandate that my code always be used with STLPort for the STL implementation I decided to look at a replacement utility function for my library code. Of course the 'old-fashioned' C way of doing things still works, and when updated for today's buffer overflow concious world it might look something like this:

string ToString(
   const unsigned int val)
{
   static const size_t bufferSize = 10 + 1;
  
   char buffer[bufferSize];
  
   if (-1 == sprintf_s(buffer, bufferSize, "%u", val))
   {
      throw exception;
   }
  
   return buffer;
}

You then need something like that for each type that you want to convert from and for each type that you want to convert to (typically std::string and std::wstring), which is more work initially but since this code contains less unexpected cross thread locking and since the code can be written and tested once I figure the up front work is worth it in this instance. My performance tests show that this 'old school' style of conversion is almost 3 times faster than the streamed conversion when used from a single thread and suffers far less of the cross thread contention when used from multiple threads (construction of a result string object requires memory allocation which locks the heap, but then that's also the case with the streaming version.). There's scope for further improvements as the construction and return of the result string could probably be optimised in such a way that 'compilers that do' could apply the named return value optimisation; but that's off into the realm of optimising for the sake of it IMHO.

Obviously when you add a level of abstraction to a job, such as streaming via the STL when converting between numbers and strings, you expect to lose a little in performance for the convenience of not having to work at the lower level. Usually this doesn't matter, or we'd all be working in assembler still, but sometimes there are hidden costs that far outweigh the convenience.

4 Comments

Len said: "My performance tests show that this 'old school' style of conversion is almost 3 times faster than the streamed conversion when used from a single thread"

Yes, it will be - sprintf doesn't take things like locales into account. It's been recognised for quite a while that streams are slow - not merely because they're streams, but because of all the localisation stuff behind them.

You'll probably find that uisng ltoa or a handwritten producer is even quicker, as it doesn't have to do the format parsing or cater for optional items that sprintf has to. I had to resort to ltoa and atol in some heavy duty parsing code (parsing debug info to construct symbol databases for inspection of embedded systems) because things like stringstream were just too much of a performance hit.

Yes, I found myself thinking that I could next try ltoa() and/or write my own but at that point it became a bit of an exercise in pointless optimisation for my current situation and so I decided to stop where I was. It was mainly the cross thread locking that I wanted to get rid of as that had been what had surprised me and what was really clobbering performance for this one particular server. Now that I have the bulk of the code in place I can easily drop into it and replace some of the implementations with more optimised ones if necessary.

Why cant c++ just die already? This is NOT the kind of problem a developer should be trying to solve anymore.

Brad,

Each to his own, if you don't want to have to deal with these kinds of issues then don't use C++. Nobody is forcing you to.

Sometimes it makes sense to use C++, often it doesn't. I personally select the work that I like to do and mostly that involves doing it in C++ because that's the kind of work that it is.

Leave a comment