I've discovered that std::string
s are very slow compared to old-fashioned null-terminated strings, so much slow that they significantly slow down my overall program by a factor of 2.
I expected STL to be slower, I didn't realise it was going to be this much slower.
I'm using Visual Studio 2008, release mode. It shows assignment of a string to be 100-1000 times slower than char*
assignment (it's very difficult to test the run-time of a char*
assignment). I know it's not a fair comparison, a pointer assignment versus string copy, but my program has lots of string assignments and I'm not sure I could use the "const reference" trick in all places. With a reference counting implementation my program would have been fine, but these implementations don't seem to exist anymore.
My real question is: why don't people use reference counting implementations anymore, and does this mean we all need to be much more careful about avoiding common performance pitfalls of std::string?
My full code is below.
#include <string>
#include <iostream>
#include <time.h>
using std::cout;
void stop()
{
}
int main(int argc, char* argv[])
{
#define LIMIT 100000000
clock_t start;
std::string foo1 = "Hello there buddy";
std::string foo2 = "Hello there buddy, yeah you too";
std::string f;
start = clock();
for (int i=0; i < LIMIT; i++) {
stop();
f = foo1;
foo1 = foo2;
foo2 = f;
}
double stl = double(clock() - start) / CLOCKS\_PER\_SEC;
start = clock();
for (int i=0; i < LIMIT; i++) {
stop();
}
double emptyLoop = double(clock() - start) / CLOCKS_PER_SEC;
char* goo1 = "Hello there buddy";
char* goo2 = "Hello there buddy, yeah you too";
char *g;
start = clock();
for (int i=0; i < LIMIT; i++) {
stop();
g = goo1;
goo1 = goo2;
goo2 = g;
}
double charLoop = double(clock() - start) / CLOCKS_PER_SEC;
cout << "Empty loop = " << emptyLoop << "\n";
cout << "char* loop = " << charLoop << "\n";
cout << "std::string = " << stl << "\n";
cout << "slowdown = " << (stl - emptyLoop) / (charLoop - emptyLoop) << "\n";
std::string wait;
std::cin >> wait;
return 0;
}
If you have an indication of the eventual size of your vector you can prevent excessive resizes by calling reserve() before filling it up.
Good performance isn't always easy with STL, but generally, it is designed to give you the power. I found Scott Meyers' "Effective STL" an eye-opener for understanding how to deal with the STL efficiently. Read!
As others said, you are probably running into frequent deep copies of the string, and compare that to a pointer assignment / reference counting implementation.
Generally, any class designed towards your specific needs, will beat a generic class that's designed for the general case. But learn to use the generic class well, and learn to ride the 80:20 rules, and you will be much more efficient than someone rolling everything on their own.
One specific drawback of
std::string
is that it doesn't give performance guarantees, which makes sense. As Tim Cooper mentioned, STL does not say whether a string assignment creates a deep copy. That's good for a generic class, because reference counting can become a real killer in highly concurrent applications, even though it's usually the best way for a single threaded app.I would say that STL implementations are better than the traditional implementations. Also did you try using a list instead of a vector, because vector is efficient for some purpose and list is efficient for some other
This test is testing two fundamentally different things: a shallow copy vs. a deep copy. It's essential to understand the difference and how to avoid deep copies in C++ since a C++ object, by default, provides value semantics for its instances (as with the case with plain old data types) which means that assigning one to the other is generally going to copy.
I "corrected" your test and got this:
Apparently we should cease using C-style strings since they are soooo much slower! In actuality, I deliberately made my test as flawed as yours by testing shallow copying on the string side vs. strcpy on the :
The main point is, and this actually gets to the heart of your ultimate question, you have to know what you are doing with the code. If you use a C++ object, you have to know that assigning one to the other is going to make a copy of that object (unless assignment is disabled, in which case you'll get an error). You also have to know when it's appropriate to use a reference, pointer, or smart pointer to an object, and with C++11, you should also understand the difference between move and copy semantics.
People do use reference-counting implementations. Here's an example of one:
The difference is that string doesn't do it internally as that would be inefficient for those who don't need it. Things like copy-on-write are generally not done for strings either anymore for similar reasons (plus the fact that it would generally make thread safety an issue). Yet we have all the building blocks right here to do copy-on-write if we wish to do so: we have the ability to swap strings without any deep copying, we have the ability to make pointers, references, or smart pointers to them.
To use C++ effectively, you have to get used to this way of thinking involving value semantics. If you don't, you might enjoy the added safety and convenience but do it at heavy cost to the efficiency of your code (unnecessary copies are certainly a significant part of what makes poorly written C++ code slower than C). After all, your original test is still dealing with pointers to strings, not
char[]
arrays. If you were using character arrays and not pointers to them, you'd likewise need tostrcpy
to swap them. With strings you even have a built-in swap method to do exactly what you are doing in your test efficiently, so my advice is to spend a bit more time learning C++.If used correctly, std::string is as efficient as char*, but with the added protection.
If you are experiencing performance problems with the STL, it's likely that you are doing something wrong.
Additionally, STL implementations are not standard across compilers. I know that SGI's STL and STLPort perform generally well.
That said, and I am being completely serious, you could be a C++ genius and have devised code that is far more sophisticated than the STL. It's not likely , but who knows, you could be the LeBron James of C++.
A large part of the reason might be the fact that reference-counting is no longer used in modern implementations of STL.
Here's the story (someone correct me if I'm wrong): in the beginning, STL implementations used reference counting, and were fast but not thread-safe - the implementors expected application programmers to insert their own locking mechanisms at higher levels, to make them thread-safe, because if locking was done at 2 levels then this would slow things down twice as much.
However, the programmers of the world were too ignorant or lazy to insert locks everywhere. For example, if a worker thread in a multi-threaded program needed to read a std::string commandline parameter, then a lock would be needed even just to read the string, otherwise crashes could ensue. (2 threads increment the reference count simultaneously on different CPU's (+1), but decrement it separately (-2), so the reference count goes down to zero, and the memory is freed.)
So implementors ditched reference counting and instead had each std::string always own its own copy of the string. More programs worked, but they were all slower.
So now, even a humble assignment of one std::string to another, (or equivalently, passing a std::string as a parameter to a function), takes about 400 machine code instructions instead of the 2 it takes to assign a char*, a slowdown of 200 times.
I tested the magnitude of the inefficiency of std::string on one major program, which had an overall slowdown of about 100% compared with null-terminated strings. I also tested raw std::string assignment using the following code, which said that std::string assignment was 100-900 times slower. (I had trouble measuring the speed of char* assignment). I also debugged into the std::string operator=() function - I ended up knee deep in the stack, about 7 layers deep, before hitting the 'memcpy()'.
I'm not sure there's any solution. Perhaps if you need your program to be fast, use plain old C++, and if you're more concerned about your own productivity, you should use Java.