Fish tank with Puck the Fish Fish tank with Puck the Fish Water on the floor spelling Fish
 

I Didn’t Order That, So Why Is It On My Bill, Episode 2

September 17th, 2009

This is the second episode of I Didn’t Order That, So Why Is It On My Bill: C++ features you never use, but pay for anyways. See episode one here.

For those who like their punchlines first: std::string is designed to allow copy-on-write. But once operator[] is called, that string is ruined for COW forevermore. The reason is that it has to guard against a future write at any time, because you may have stashed away the internal pointer that operator[] returns – a feature that you surely know better than to use, but that costs you nevertheless.

The C++ standard string class is std::string. Here’s a string containing 10 megabytes of the letter ‘x’:

#include <string>
using std::string;
int main(void) {
    string the_base(1024 * 1024 * 10, 'x');
}

Big string. Now let’s make a hundred copies, one by one:

#include <string>
using std::string;
int main(void) {
    string the_base(1024 * 1024 * 10, 'x');
    for (int i = 0; i < 100; i++) {
        string the_copy = the_base;
    }
}

This runs in .02 seconds, which is very fast. Suspiciously fast! This old iMac can’t schlep a gigabyte of data in two hundredths of a second. It’s using copy-on-write.

So let’s write and measure the copy:

#include <string>
using std::string;
int main(void) {
    string the_base(1024 * 1024 * 10, 'x');
    for (int i = 0; i < 100; i++) {
        string the_copy = the_base;
        the_copy[0] = 'y';
    }
}

The program now runs in 2.5 seconds. These scant 100 writes resulted in a 100x slowdown. Not surprising: it’s exactly what we would expect from a COW string.

the_copy[0] returns a reference to a char. But maybe we aren’t going to write into it – perhaps we only want to read. How can these copy-on-write strings know when the write occurs, so they can copy? The answer is that they cannot – they must be pessimistic and assume you will write, even if you never do. We can trick it: don’t assign anything, and still see the performance hit:

#include <string>
using std::string;
int main(void) {
    string the_base(1024 * 1024 * 10, 'x');
    for (int i = 0; i < 100; i++) {
        string the_copy = the_base;
        the_copy[0];
    }
}

No writes! Just a read! But same performance as before. This isn’t copy on write – it’s copy on read!

Things get even worse. operator[] returns a reference: basically a sugary pointer. You can read from it or write to it. Or you can be a jerk and just hang onto it:

string original = "hello";
char & ref = original[0];
string clone = original;
ref = 'y';

In case that’s not clear, we made a string, stashed away a reference to the first character, copied the string, and then wrote to the string’s guts. The write is just a normal store to a memory location, so the poor string is caught completely unaware. If the copy-on-write were naive, then we would have modified both the original and the copy.

Oh man, is that evil. But the string’s got to be paranoid and guard against it anyways – in fact, the C++ standard explicitly calls out this case as something that has to work! In other words, when you get a reference to a char inside a string, the string is tainted. It can never participate in copy-on-write again, because you just might have stashed away the internal pointer it handed back, and write into the string as a completely unexpected time.

Of course you know better than to hang onto internal pointers. You probably don’t need this feature. But you pay for it anyways:

#include <string>
using std::string;
int main(void) {
    string the_base(1024 * 1024 * 10, 'x');
    the_base[0];
    for (int i = 0; i < 100; i++) {
        string the_copy = the_base;
    }
}

This takes the 2.5 seconds. Just one silly read wrecked all of the COW machinery for all the future copies. You’re paying for writes that never come!

Could the C++ standard have fixed this? Sure – the C++ standard goes into great detail about when iterators and references into strings are invalidated, and all it would have taken would be to add the copy constructor and operator=() to the list of reference-invalidating functions (section 23.1.5, for those who are following along in the standard). A second option would have been to declare that operator=() invalidates existing references for writing, but not for reading. But the standards committee preferred simplicity, convenience and safety to performance – uncharacteristically, if you’ll forgive a smidgen of editorializing.

If you call a reference-invalidating function, you can, in principle, get back in the COW fast lane. Although GNU STL does not take advantage of this in all cases, it does when resizing strings:

#include <string>
using std::string;
int main(void) {
    string the_base(1024 * 1024 * 10, 'x');
    the_base[0];
    the_base.push_back('x');
    for (int i = 0; i < 100; i++) {
        string the_copy = the_base;
    }
}

Allowing the string to modify itself allowed it to know that any previous references were invalidated. We're fast again! It's called a COW, and it's not about to blow it now.

The moral is that you should avoid using operator[], the at() function, iterators, etc. to read from a non-const string - especially a long string - if it may be copied in the future. If you do use these functions, you risk paying for a write that you don't make, and for hanging onto an internal pointer that you released. Unfortunately, there's no good function for just reading from a non-const string. You can do it by adding a crazy cast:

#include <string>
using std::string;
int main(void) {
	string the_base(1024 * 1024 * 10, 'x');
	const_cast<const string &>(the_base)[0];
	for (int i = 0; i < 100; i++) {
		string the_copy = the_base;
	}
}

This hits the fast path too.

Scene. I hope you enjoyed this post. As usual the point is not to rag on C++, but to explore aspects of its design that, for whatever reason, tilt a tradeoff away from performance.

 

The Internet!

π = 3.2851405312

Morten

One other option, which we use in Qt, is to have operator[] return a temporary character reference object. If you assign to that object the string will detach, but for any read-only operation it won’t.

Achilleas Margaritis

And all this just because c++ avoids garbage collection like a plague…

Alex

string original = “hello”;
char & ref = start[0];
string clone = start;
ref = ‘y’;

did you mean:

string original = “hello”;
char & ref = original[0];
string clone = original;
ref = ‘y’;

Enzo90910

+1 Alex

Anonymous

Please stop justifying your text. A great article was ruined by nasty gaps between words in an attempt to get text to stretch from margin-to-margin needlessly…

C++ guy

COW for strings is an implementation detail. In fact, COW performs poorly under multithreading since it has to lock the shared representation for all accesses. Microsoft’s string implementation supplied with Visual C++ does not use COW at all, so you always pay the copy price.

Guy

For certain applications, garbage collection IS a plague. I resorted to managing a memory pool myself to avoid stuttering in games written in C#. At that point, what’s the use?

Anonymous

Seems to me this is better described as an issue with STL, not C++. Everywhere I’ve worked as a programmer for the past 15 years or so I’ve seen people go through stages of STL as it has matured.. slight familiarity leading to wonderment, followed by overuse & abuse leading to surprise, distrust and repulsion as they are shocked by what is really going on behind the scenes; eventually winding up at a place where STL is correctly understood and rarely used mostly to avoid newbies screwing things up as they are mostly stuck in stage 1. STL is powerful, to be sure, but its pretty clear most people simply don’t understand it beyond the most basic of details.

Tom Swirly

Copy-on-write was only in GCC 3.x, if I recall correctly: neither GCC 2.x nor the current 4.x has it. And for very good reason – you have to lock your string each time you access it and that’s death to your performance.

Copy-on-write is one of those things that seems like a good idea but is not in practice. In real-world code, you use const char* const quite a lot instead – because the source of the string is either an actual constant, or is a C++ string that has a very long lifespan.

Tim

First Principles: Say What You Mean. If you want to announce that you have no intentions of changing this object and that the code may optimize for that MAKE IT CONST.

Charles

Nice article. But, “anyways” isn’t a real word. It’s just “anyway” :)

Илья Казначеев

You’d be amazed how many bugs would you left behind if you make your string class immutable.
Java did just that.

“STL is powerful, to be sure, but its pretty clear most people simply don’t understand it beyond the most basic of details” shows that someone did it really wrong, because Java standard library comes with strings and collections that are like STL, but can be understood by really anyone.

And it’s even not that java’s any good! It’s just C++ that’s bad.

Илья, I suspect most of fish’s readers are Cocoa programmers who are very familiar with immutable strings. ;-)

ridiculous_fish

Yes, thanks alex! Fixed.

And again,
The design of a certain class can hardly show anything on the language itself. Sure, STL is considered an official and standard library of the language, but it is not the language itself, nor does it represent any special mechanisms in the language. So again, I can’t really agree with you, from the two posts in this series I’ve only seen some ranting on shortcomings of either external mechanisms (shared library loading) or specific design choices you dont agree with (this post).
The COW on operator[] could be implemented by using a proxy object and it would have invalidated your whole post, it’s not like this kind of design is enforced by the language – on the contrary, it can be be avoided just as easily.

When i have some time i will explain how to implement this kind of COW on my blog.

And again, thanks for the read.

ridiculous_fish

Hi rmn,

Qt’s QString implements QString::operator[] by returning a proxy object that, when written to, turns around and reflects the write into the string. I’m guessing this is what you mean by a “proxy object,” and it does indeed solve the problem.

Unfortunately, it is not possible to use this technique with std::string, because the return type of std::string::operator[] is dictated by the C++ standard and may not be a proxy object.

Anonymous

No need to do the crazy cast.

string the_base(“whatever”);
const string& the_const_base(the_base);
the_const_base[0];

tod(D)

As others have said, copy-on-write performs terribly if you try to make it thread safe (and who wants a low level thing like std::string that isn’t threadsafe?), one of the reasons it has dropped from most standard libraries and effectively rendered illegal in c++0x (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2668.htm).

If you need to accomplish fast, low overhead copies, the flyweight pattern (http://www.boost.org/doc/libs/1_40_0/libs/flyweight/doc/index.html) performs better and is far more extensible than just rendering all strings immutable.

Hi fish,
As far as I know, the standard does not deman std::string to use COW – that’s just what your implementation does. That’s why the interface is that way. It’s alot more user friendly as well.

Thanks for the reply.

Michael Pyne

Another method which is common (and which I’m surprised didn’t work when I tested it) is if you want only a read-only reference, to use a const return variable. i.e.:

const char& first = the_base[2];

This allows the string class to use it’s const member function
const char& operator[](int) const;

to fulfill the request with the assurance that the recipient of the char won’t do anything tricky. GNU STL doesn’t seem to take advantage of that here though, as this case is still as slow as the other COW-breakers.

(I mention this because it’s used all the time in Qt, where it works as designed. ;)

Michael Pyne, this code will not achieve the desired behaviour. C++ does not choose which function to invoke by inspecting the context; it does not matter that the return value is conse char& and not just char&. The invoked function will be the non const operator[] (and not operator[] const), since the string itself is not const.
What you could do to get the desired behavior is use the code in Anonymous’s comment, which makes the call to operator[] through a const string.

Regards,
Roman.

Alex

You fail. This is what const_iterators are for.

Leonardo Boiko

Justification simply doesn’t work on the Web. Good justification needs sophisticated hyphenation algorithms, language-specific information, and mechanisms for fine-tuning, a la LaTeX. Our current crop of *standards* don’t support any of that, never mind the browsers. One should just use left-align, at least until 2077 when CSS3 decides to define hyphenation support. Otherwise, you end up with awfully stretched inter-word spacing or even intra-word spacing, depending on your window size, font, and browser.

ridiculous_fish

Heh, didn’t even realize I was justifying. Left-align, activate!

Anonymous

I understand that C++0x adds some things to strings so that CoW will no longer be feasible.

dv

Please note that your code makes a big mistake: it always invokes the LHS version of the [] operator. In other words, the expression “the_copy[0];” is NOT a simple read. You should use “char c = the_copy[0];” instead, since this uses the RHS operator. This one cannot write, so it does not have to copy.

LHS version: char & operator[](size_type const index)
RHS version: char const & operator[](size_type const index) const

LHS examples:
the_copy[0];
the_copy[0] = ‘a’;

RHS example:
std::cout << the_copy[0];

What "const_cast(the_base)[0];” does is simply force the_base to be const, so that the only matching [] operator is the RHS one.

As for COW in gcc 4.x, it does exist. Just look into the basic_string header. However, they use atomic operations instead of mutexes/critical sections.

dv

Update: I was mistaken about the LHS-RHS-preferences. C++ always prefers the LHS version over the RHS one. For some reason, the RHS one was preferred in my tests. This means the crazy cast is indeed necessary.