Performance Improvement with the StringBuilder for C++
codeproject.com
codeproject.com
If you replace the code
start = clock();
for (int i = 0; i < loops; ++i) {
std::wstring result2 = tested.ToString();
}
double secsBuilder = (double) (clock() - start) / cps;
with start = clock();
for (int i = 0; i < loops; ++i) {
StringBuilder<wchar_t> wide;
wide.Add(tested2.begin(), tested2.end()).AppendLine();
std::wstring result2 = wide.ToString();
}
double secsBuilder = (double) (clock() - start) / cps;
the results go from Accumulate took 0.134847 seconds, and ToString() took 0.014123 seconds.
The relative speed improvement was 854.804%
to Accumulate took 0.146171 seconds, and ToString() took 0.099802 seconds.
The relative speed improvement was 46.461%
Much less impressive.Furthermore, the author is not using any of the standard techniques to avoid memory allocations in C++ (such as reusing the same container with .clear() instead of creating a new one each time), that would improve even more the performance.
Besides, despite what the author says, std::list is an awful container (one allocation per element, terrible locality, ...). You should never use it, unless you really know what you are doing (for example, see Stroustrup's recent talks).
To put a finer point on that: you should use std::list iff you need to splice (or insert into the middle) in constant time.
Linked lists really have a very narrow use case.
Alternatively `std::ostringstream` is specially designed for this type of task as well... how does it compare? Is it better/worse? Looks like reinventing the wheel to me.
If I use ostringstream, and also I change the code so it has to construct the StringBuilder every test (at the moment they build it once and then keep calling 'toString'), then I get the output (from the test program on that website):
Accurate performance test:
ostringstream took 0.0120331 seconds, and ToString() took 0.0221947 seconds.
The relative speed improvement was -45.784%
Join took 0.0176613 seconds. Accumulate took 0.00195327 seconds
ToString() took 0.00283577 seconds.
Join took 0.00462704 seconds.
stringstream took 0.00084927 seconds.
The relative speed improvement was -71.1482% s += "..."
Though I'm not sure if it similarly optimizes: s = s + "..."
But old habits die hard so I still use a list of strings and join them at the end.Although it is still possible to make it faster by overallocating in the same way as std::vector, but at the cost of more memory use.
According to that link, c_str() and data() work in constant time. With that restriction, it's impossible to do the joining lazily - it must be done when data is added to the string.
An answer to http://programmers.stackexchange.com/questions/124731/what-p... indicates that C++03 doesn't require constnat time, either.
Thanks for the education.
In the innermost of inner loops, I've been known to use a static string or vector to avoid repeated allocation entirely. Only in single-threaded code of course!
Startup is now instantaneous. It was also making queries slower. Queries are now also instantaneous.
It was taking forever.
I eventually realized that this string reallocation that was being done 10,000,000 times was the problem.
To solve this, I did a two-level accumulation (perhaps three levels would have been better, but two was enough). I first accumulated 3,000 of the 32-character strings (3,000 because that was about the square root of 10,000,000).
I then accumulated the (about) 3,000 of these (about) 100,000 character strings.
The result took about 30 seconds, which was good enough for what I needed to do.
string s = accumulate(vec.begin(), vec.end(), s);
Is that legal C++? I would think that passes s to 'accumulate' before constructing it (http://www.gotw.ca/gotw/001.htm). IMO, a correct way to do this would be: string s; // calls string::string()
s = accumulate(vec.begin(), vec.end(), s);
or
string s = accumulate(vec.begin(), vec.end(), "");C++ has a StringBuilder, it's called std::ostringstream, but the author didn't seem to know about it, so reinvented it.
To be polite, his reinvention is reasonable, and knowing about this problem is useful.
For example it may create a new StringBuilder in every iteration of a loop whereas you may be able to code it such that only a single StringBuilder needs to be created and you may be able to provide better initial array size hinting. If it's just a single concatenation statement, building a log message or something, then using the '+' operator won't have much if any impact on performance.
It's not even the JIT, it's a static transformation at byte code creation time. Last I checked:
String s = "foo" + "bar";
Produced identical byte code to: String s = new StringBuilder()
.append("foo")
.append("bar")
.toString();It is a very complex language, and that is definitely a mark against it, but the exact same thing could happen in Java or C# if people didn't know to use StringBuilder instead of relying on concatenating strings.
I'm actually wondering if we can get a speed boost for javascript in a similar way. I find myself concating strings together often in the code.
Just grep the QString API docs for "QStringBuilder".
In the case of concatenation, where the goal is to end up with a contiguous array of the characters from the strings to be joined, no block of memory sufficiently large exists anywhere to be appropriated, so new memory must be allocated.
a) SunSpider was overwhelmingly a benchmark measuring string concatenation performance. b) Firefox had slow string concatenation.
The solution to b) was trivial -- whenever Firefox saw that you were doing str = str + something, it would realloc str to the new length of len(str)+len(something)+1 and then strcpy something to the tail of str. By changing the code slightly to trade a relatively small amount of memory (in most situations), making every realloc size to the next power-of-two greater than the new combined length, this improved SunSpider performance 20x+ because the vast majority of concatenations could be done in place.