5

I want to read a file line by line and capture one particular line of input. For maximum performance I could do this in a low level way by reading the entire file in and just iterating over its contents using pointers, but this code is not performance critical so therefore I wish to use a more readable and typesafe std library style implementation.

So what I have is this:

 std::string line;
 line.reserve(1024);
 std::ifstream file(filePath);
 while(file)
 {
    std::getline(file, line);
    if(line.substr(0, 8) == "Whatever")
    {
        // Do something ...
    }
 }

While this isn't performance critical code I've called line.reserve(1024) before the parsing operation to preclude multiple reallocations of the string as larger lines are read in.

Inside std::getline the string is erased before having the characters from each line added to it. I stepped through this code to satisfy myself that the memory wasn't being reallocated each iteration, what I found fried my brain.

Deep inside string::erase rather than just resetting its size variable to zero what it's actually doing is calling memmove_s with pointer values that would overwrite the used part of the buffer with the unused part of the buffer immediately following it, except that memmove_s is being called with a count argument of zero, i.e. requesting a move of zero bytes.

Questions:

Why would I want the overhead of a library function call in the middle of my lovely loop, especially one that is being called to do nothing at all?

I haven't picked it apart myself yet but under what circumstances would this call not actually do nothing but would in fact start moving chunks of buffer around?

And why is it doing this at all?

Bonus question: What the C++ standard library tag?

Ben Voigt
  • 260,885
  • 36
  • 380
  • 671
Neutrino
  • 6,048
  • 4
  • 37
  • 64

3 Answers3

11

This is a known issue I reported a year ago, to take advantage of the fix you'll have to upgrade to a future version of the compiler.

Connect Bug: "std::string::erase is stupidly slow when erasing to the end, which impacts std::string::resize"

The standard doesn't say anything about the complexity of any std::string functions, except swap.

Ben Voigt
  • 260,885
  • 36
  • 380
  • 671
  • +1 for finding compiler bug. Not a lot of people can do this. – FailedDev Nov 17 '11 at 18:13
  • 1
    Well not really a compiler but STL implementation "bug". I just wanted to point to the initials of the answering MS guy - what a coincidence :D – Voo Nov 17 '11 at 18:23
  • +1 Nice report, Ben. Apparently I saw it when you reported initially, since I had already upvoted it on Connect. :-P – ildjarn Nov 17 '11 at 19:20
  • Thanks. With the insights raised by these responses and looking at it a bit more, it's clear that in the case of my code the move will never occur, so as you say the library implementation is suboptimal but not enough for me to worry about in this instance. – Neutrino Nov 17 '11 at 22:04
3

std::string::clear() is defined in terms of std::string::erase(), and std::string::erase() does have to move all of the characters after the block which was erased. So why shouldn't it call a standard function to do so? If you've got some profiler output which proves that this is a bottleneck, then perhaps you can complain about it, but otherwise, frankly, I can't see it making a difference. (The logic necessary to avoid the call could end up costing more than the call.)

Also, you're not checking the results of the call to getline before using them. Your loop should be something like:

while ( std::getline( file, line ) ) {
    //  ...
}

And if you're so worried about performance, creating a substring (a new std::string) just in order to do a comparison is far more expensive than a call to memmove_s. What's wrong with something like:

static std::string const target( "Whatever" );
if ( line.size() >= target.size()
        && std::equal( target.begin(), target().end(), line.being() ) ) {
    //  ...
}

I'ld consider this the most idiomatic way of determining whether a string starts with a specific value.

(I might add that from experience, the reserve here doesn't buy you much either. After you've read a couple of lines in the file, your string isn't going to grow much anyway, so there'll be very few reallocations after the first couple of lines. Another case of premature optimization?)

James Kanze
  • 142,482
  • 15
  • 169
  • 310
  • I don't think it's premature. It doesn't add any confusion, and it does have a speed benefit. – Mooing Duck Nov 17 '11 at 18:24
  • @MooingDuck I presume you're talking about the `reserve`: it doesn't have any measurable effect on performance, and it adds an unnecessary line of code. – James Kanze Nov 17 '11 at 18:35
  • I was talking about the `reserve` yes. It would appear that you and I have differing definitions of "premature" optimization, which is fine. It's not important enough to debate. – Mooing Duck Nov 17 '11 at 18:39
  • I see now the obvious thing which I overlooked, namely that when only a part of the string is being erased then erase does indeed have to move the rest of the stuff down, however as Ben Voigt points out, in the common case of erasing the entire string then that is rather a lot of code that executes for no purpose at all, and in that common case the check to avoid the call will come nowhere near the cost of executing all that pointless code. Thanks for your other tips though, all quite right, I hadn't really finished tidying my loop up before I got sidetracked by what I discovered in erase :) – Neutrino Nov 17 '11 at 21:59
  • On the point about the reserve I tend to agree that technically it probably is a premature optimization. However I think the specific case of selecting a reasonable buffer size before entering a loop is a standard enough pattern that it could arguably be considered just good form rather than an optimisation per se. Possibly just a matter of opinion this one. – Neutrino Nov 17 '11 at 22:09
  • @Neutrino: Rather, it's when the end of the string is not erased, that the move is required. Whenever the string is truncated (whether to zero length or not), no move is needed. And there already was a check in `erase`, the "fix" is pure savings with no downside. – Ben Voigt Nov 17 '11 at 22:47
  • Yup, I did get that, I just couldn't reiterate it clearly as I ran out of characters, so I expressed it in terms of the total erase in my example :) – Neutrino Nov 17 '11 at 23:11
  • @Neutrino I'm not sure that I see where "a lot of code executes". Calling `memmove_s` with a 0 length is not what I'd call a lot of code. And adding if's right and left to handle special cases (even if they are very frequent) doesn't necessarily improve things. (G++ handles `clear()` in roughly the same manner, using `std::copy` instead of `memmove_s`.) It would be interesting to modify a string class so that `clear` does just set the length field to 0, and nothing else, and see if this made a significant different in various applications. – James Kanze Nov 18 '11 at 08:37
  • If you follow the link in Ben's thread the MS guy already posted performance metrics of the fixed erase function. Looks like a decent improvement to me. – Neutrino Nov 18 '11 at 09:32
  • @Neutrino I didn't see any measures with regards to its impact on real code. Not that the change isn't an improvement, at least on some architectures. But I wouldn't consider the change a "bug fix", but rather an "enhancement"---Microsoft is being generous in considering it a bug. (Historically, of course, a lot of implementation used reference counting, and g++ still does. And with reference counting, the optimization is more complex.) – James Kanze Nov 18 '11 at 09:50
0

In this case, I think the idea you mention of reading the entire file and iterating over the result may actually give about as simple of code. You're simply changing: "read line, check for prefix, process" to "read file, scan for prefix, process":

size_t not_found = std::string::npos;
std::istringstream buffer;

buffer << file.rdbuf();

std::string &data = buffer.str();

char const target[] = "\nWhatever";
size_t len = sizeof(target)-1;

for (size_t pos=0; not_found!=(pos=data.find(target, pos)); pos+=len)
{
    // process relevant line starting at contents[pos+1]
}
Jerry Coffin
  • 437,173
  • 71
  • 570
  • 1,035
  • I actually meant going to an even lower level and using Win32 CreateFile to read the entire file into a raw memory buffer, and the only reason I was even considering that was because that's how all the other IO is done in the app I'm currently working on :( In your example even though you take a reference to istringstream.str() it's still a copy of the entire controlled sequence though right? In which case since you've copied the entire file is that really more performant than reading the file line by line? – Neutrino Nov 17 '11 at 22:38
  • @Neutrino: It may or may not be a copy. In my experience, it usually does have better performance than reading line by line. You might want to look at a [previous answer](http://stackoverflow.com/questions/3303527/how-to-pre-allocate-memory-for-a-stdstring-object/3304059#3304059) for a little benchmarking, and some better performing code. At least if memory serves, reading line by line seems to be around the same speed as the iterators used there. – Jerry Coffin Nov 17 '11 at 22:43