20

On https://doc-snapshots.qt.io/qtcreator-extending/coding-style.html it is recommended to write for loops like the following:

Container::iterator end = large.end();
for (Container::iterator it = large.begin(); it != end; ++it) {
        //...;
}

instead of

for (Container::iterator it = large.begin(); it != large.end(); ++it) {
        //...;
}

Since I have rarely seen this style in any code, I would like to know whether the consecutive call of end() really adds a noticeable run-time overhead for large loops over stl containers or whether compilers already optimize such cases.

Edit: As many of to very good comments pointed out: This question is only valid if the code inside the loop does not modify the end iterator. Otherwise of course the repeated call of end is mandatory.

MBach
  • 1,647
  • 16
  • 30
Martin
  • 4,738
  • 4
  • 28
  • 57
  • 12
    As an aside: I'd even go for `for (Container::iterator it = large.begin(), end = large.end(); it != end; ++it) { ... }` in order to limit scope for variable `end` to just the for-loop. – haffax Mar 19 '12 at 10:43
  • C# and Java devs write this kind of loops to let the JITer optimize it (one check less per iteration). It seems not the case for C++. – Lukasz Madon Mar 19 '12 at 14:25
  • 4
    C++ devs just write `for_each(begin(c), end(c), [](){});` Loops are for library writers :P – MSalters Mar 19 '12 at 15:23
  • 1
    @MSalter: Lets say C+11 devs. Lambdas with e.g. boost are not for the faint hearted – Martin Mar 19 '12 at 16:49

7 Answers7

18

The C++11 standard (§ 23.2.1) mandates that end has O(1) complexity, so a conforming implementation would have the same performance characteristics for both versions.

That said, unless the compiler can prove that the return value of end will never change then pulling end out of the loop might be faster by some constant quantity (as Steve Jessop comments, there are lots of variables that can influence whether this is true or not).

Still, even if in one particular case there is absolutely no performance difference, pulling such tests out of the loop is a good habit to get into. An even better habit to get into is to utilize standard algorithms as @pmr says, which sidesteps the issue entirely.

Jon
  • 428,835
  • 81
  • 738
  • 806
  • 7
    Even if the compiler can't prove that the value of `end` is loop-invariant, hoisting it still might not be any faster. The reason is just that even if it's hoisted, the value still has to be stored in a variable, which might well be on the stack. The container might well also be on the stack, in which case reading its `end` out of some data member in the container is likely to be exactly the same operation as reading the hoisted `end` out of a variable. If the container has been passed by reference, then there could be an additional indirection and that might well be slightly slower. – Steve Jessop Mar 19 '12 at 10:34
  • @SteveJessop: Thank you for the insightful comment, I changed the wording so that it does not perhaps leave the wrong impression. Other than that, IMHO we (humans) really should not be dissecting this so thin because it's highly unlikely that performance differences of such small magnitude can be predicted. – Jon Mar 19 '12 at 10:41
  • 2
    I agree that we can't predict these small differences. I don't really agree that hoisting such tests is a good habit. For some people it might improve readability (2 shorter lines rather than 1 longer), otherwise it's a speculative optimization and not really worth cluttering the code for IMO. If the hoist reduces the complexity of the whole operation, or if it's clearly going to be a significant proportion of the work done in the loop, then I'd do it speculatively. Otherwise I wouldn't, although I also wouldn't object to taking it "for free" when using an algorithm or a range-based for. – Steve Jessop Mar 19 '12 at 12:09
  • Thanks for the revealing answer: As the comments also contain very good information (thanks Steve) I'm choosing this one. An afterthought reading Steves notes: Since for C++11 erasing on a constant iterator is possible (which is very good in my opinion) putting end outside the loop might introduce bad errors if someone later decides to modify the container inside the loop. While this is unlikely it is still a possible source of errors that can be avoided – Martin Mar 19 '12 at 13:33
  • One reason I consider it a good habit is that it implicitly states an invariant for the loop. This is typically why I hoist it as a variable -- it is a bit of documentation (for myself as well as others) that the size of the collection is not expected to change over the course of the loop. That it might be slightly more optimal is secondary. On the contrary, if I don't use this idiom, it suggests that the size may change. – mcmcc Mar 19 '12 at 15:50
  • @mcmcc: True as well if all guys modifying the code know this rule ;-) – Martin Mar 19 '12 at 16:46
  • @Martin: My viewpoint has always been that code, even when it is wrong, is at least _definitively_ wrong. The same cannot be said for comments. ;^) – mcmcc Mar 19 '12 at 20:04
7

This is less about end being costly and more about the ability of a compiler to see that end will not change through a side effect in the loop body (that it is a loop invariant).

The complexity of end is required to be constant by the standard. See table 96 in N3337 in 23.2.1.

Using standard library algorithms circumvents the whole dilemma nicely.

pmr
  • 58,701
  • 10
  • 113
  • 156
3

If you plan to modify the collection as you iterate, you have to do it the 2nd way (end can change) - otherwise the first is theoretically a fraction faster. I doubt it would be noticeable though.

John3136
  • 28,809
  • 4
  • 51
  • 69
2

In fact, the end() method is inline. The 2nd not call it every time, I don't think end() gives any performance lag.

20082663
  • 21
  • 1
0

For anyone reading this now, the question has become somewhat of a moot point with C++11.

I wasn't sure whether this response qualifies as an answer, because it doesn't actually address the point of the question. But I do think it's valid to point out that the problem raised here will be seldom encountered in practice for a C++11 programmer, and I certainly would have found this response useful a few years ago. This response is therefore aimed at the reader who simply wants to know the best way of iterating through all elements in a STL container (vector, list, deque, etc.).

Assuming that the OP wanted access to each element in the container, we can easily sidestep the entire question of whether defining end is sufficiently faster than calling Container::end() by writing a range-based for loop:

Container container; // my STL container that has been filled with stuff

// (note that you can replace Container::value_type with the value in the container)

// the standard way
for (Container::value_type element : container) {
    // access each element by 'element' rather than by '*it'
}

// or, if Container::value_type is large
Container container; // fill it with something
for (Container::value_type& element : container) {
    //
}

// if you're lazy
Container container; // fill it with something
for (auto element : container) {
    //
}

The OP has asked whether the trade-off between brevity of simply comparing it to Container::end() at every iteration and the performance of declaring a variable end and comparing that at each step instead is worth it. Since range-based for loops provide a simple, easy to write and easy to read alternative that also happens to, internally, declare an end iterator rather than calling the Container::end() method at every step, the number of cases where we need to dwell on this question has been reduced to a limited number of cases.

As per cppreference.com, the range-based for loop will produce code with the same side-effects as the following:

{
  auto && __range = range_expression ; 
  for (auto __begin = begin_expr,
        __end = end_expr; 
      __begin != __end; ++__begin) { 
    range_declaration = *__begin; 
    loop_statement 
  } 
} 
quant
  • 21,507
  • 32
  • 115
  • 211
0

std::vector.end() (for example) return an iterator by value. In the second loop you create an object at every loop. The coding standard is telling you to do not create object if you don't need to. The compiler may be smart and optimize the code for you, however this is not guarantee. A much better solution is using stl algorithms. They are already optimized and your code will be more readable. Beware the two loops are equivalent only if you do not modified the collection.

P.S. it is very likely that the difference in performance is very minimal

Alessandro Teruzzi
  • 3,918
  • 1
  • 27
  • 41
  • 1
    The call to `end()` is likely inlined, as is the call to `operator!=`. At that level we are comparing two pointers. Having a separate copy of one of them is not likely to increase performance. – Bo Persson Mar 19 '12 at 12:00
  • I agree that the final result in performance is not going to be very different, however I like the idea to pay attention of object proliferation. – Alessandro Teruzzi Mar 19 '12 at 12:11
-3

Depends on the implementation, but I don't think end() gives that much of a performance lag.

ApprenticeHacker
  • 21,351
  • 27
  • 103
  • 153