6

In a C++ programming book I saw the following for a std::list iterator:

for (iterator = list.start(); iterator != list.end(); iterator++)

Isn't it inefficient to call list.end() all the time? Would it be better to save the end to another variable or will the C++ compiler (i. e. g++) take care of this automatically?

zneak
  • 134,922
  • 42
  • 253
  • 328
Martin Ueding
  • 8,245
  • 6
  • 46
  • 92
  • `std::list::end` is the end of a doubly-ended linked list, so it should be a constant time operation. Given you aren't appending while in the loop, an optimizing compiler should be able to cache it. – obataku Aug 28 '12 at 19:44
  • 3
    This would be in the realm of "premature optimization". Worrying about it too soon can cause problems, expecially because if you add or remove elements to the list inside your loop, end is able to change. If you've isolated a specific part of your code (that relies on this) as being a CPU hog, then by all means optimize, but otherwise there are bigger fish in the sea. – Wug Aug 28 '12 at 19:46
  • @Wug If you know `.end()` is constant time, yes (and yes, I know that it's required to be in C++, for this specific container). But for lists which may be very long and whose `.end()` is O(n), such an optimization is not premature at all. –  Aug 28 '12 at 19:49
  • Possible duplicate: [C++ iterators & loop optimization](http://stackoverflow.com/questions/795987/c-iterators-loop-optimization). Also, you should use C++11 range-based for instead. – Jesse Good Aug 28 '12 at 19:50
  • @delnan, I think `std::list` is forced to have O(1) for `end()`. If you're talking about a home-grown list container then I suppose it's possible. – Mark Ransom Aug 28 '12 at 20:02

8 Answers8

8

list::end() ought to have constant time complexity and for linked lists in particular it means it's probably very efficient.

It could be slightly more efficient to store the value if your algorithm allows that (again, the difference is unlikely to be large for especially linked lists).

Oh, and and do read Steve Jessop's answer about testing the efficiency yourself!

eq-
  • 9,986
  • 36
  • 38
  • http://www.cplusplus.com/reference/stl/list/end/ - I check stuff there, see "Complexity". – Shi Aug 28 '12 at 19:45
  • @Shi, do note that "constant" complexity does not mean a no-operation. Vectors in particular probably do pointer addition within their respective `end()`; lists probably don't (like I mentioned) – eq- Aug 28 '12 at 19:47
  • Constant complexity still takes some time, and depending on your STL, implementation, chosen container, etc (especially non-STL containers), that might be a significant amount of time. – ssube Aug 28 '12 at 19:48
4

It's unlikely to make any difference.

Standard container functions get inlined, so there should be no noticeable function call overhead. What's left is whether the optimizer is smart enough to avoid unnecessary overhead that's not strictly necessary in order to perform the comparison. For example: does it actually create a temporary list::iterator object, fill in its current position field, and then read that field back, or does the comparison end up just as a pointer comparison between a value from iterator and a value in the head of the list?

Even if there is some unnecessary overhead, it might be negligible compared with incrementing the iterator, and even more negligible compared with your loop body.

You could test it, which is more reliable than guessing. Remember to enable optimization -- testing performance without optimization is kind of like saying that Blake must be faster than Bolt if Blake walks quicker from the warm-up track to the bus.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
4

Generally, no, it's not inefficient. end() will typically be an inline function, and the compiler will generate good code to do whatever it does. More to the point, inefficient compared to what? Yes, you could add code to create a variable to hold the result, and that might or might not be a little bit faster than simply calling end(). It seems very unlikely that such a change would make a big enough speed difference to turn a program that's too slow into one that meets requirements.

Pete Becker
  • 74,985
  • 8
  • 76
  • 165
4

The call to std::list<T>::end() is unlikely to be a big efficiency issue and probably just reads a single value. However, you'd give the compiler a hint that it isn't meant to change by storing it a variable. For other containers a computation may be involved in addition to reading a base address which is a bit more involved. Still nothing dramatic but possibly worth avoiding.

Note, however, that it may also change the semantic of the loop: If the body of the loop may append elements, the former end may move. Interestingly, I don't find any specific requirements in the standard stating whether std::list<T>::end() may change when inserting elements into the container (I can imagine implementations where it does change as well as some where it doesn't; most likely it doesn't change, though). If you want to get guaranteed behavior when also modifying the list, you might very well call list.end() in every iteration.

BTW, there is a bigger performance concern I'd have about using iterator++ instead of ++iterator, especially this is really what the author used in the book. Still, this is a micro optimization like storing the result of list.end() but one cheap to do.

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • "I don't find any specific requirements in the standard stating whether `std::list::end()` may change when inserting elements into the container" - surely 23.3.5.4/1, "Does not affect the validity of iterators and references"? Since it's still valid, it must still refer to somewhere, and since you can't insert after the end iterator it must still be the same place. – Steve Jessop Aug 28 '12 at 20:05
  • What prohibits the implementation to have `end()` point to a half-baked object which is initialized with the new value upon inserting an element at the end? The iterator stays valid but doesn't point to the end anymore and there was no object in this location before so no reference or pointer becomes invalid. As it happens, this is how `std::vector` behaves if there is enough capacity. I agree that this would be an odd implementation but a DS9k implementation is known to choose the most awkward implementation deliberately (it may also behave in a sane way in unexpected situations). – Dietmar Kühl Aug 28 '12 at 20:16
  • Fair point. Out of interest, if I have an iterator that *isn't* an end iterator, and I insert before it, what says that afterwards it doesn't point to the newly-inserted object? Even with enough capacity, `vector::insert` invalidates iterators and references after the insertion point, so the non-requirement for the old end iterator to still be an end iterator is obvious there. – Steve Jessop Aug 28 '12 at 21:46
  • Well, non-end iterators reference a value which is also meant to stay the same. On the other hand, changing the value doesn't invalidate the reference, so maybe a DS9k would be allowed to move the value but I think this is pushing it because moving a value can fail and also the values are supposed to stay put in node-based containers (but I also can't locate this requirement). The standard talks about not invalidating references to objects and I think this is meant to say that the same object stays in place. This would restrict funny things to happen to only the end iterator. – Dietmar Kühl Aug 28 '12 at 22:29
2

In practice, for STL containers, container::end() is extremely cheap. In fact, the C++ standard mandates algorithmic complexity for several methods of several classes (if not for all), and container::end() is always constant-time.

Also, the compiler is free to inline those methods, removing essentially any overhead it could have. I can think of no other way to get the end of a list in constant time than storing it, so your list.end() call probably ends up being a field access, which is no more expensive on x86 platforms than storing it on the stack.

Your mileage may vary with other collections, but it's a safe bet that list.end() will not end up being your bottleneck.

zneak
  • 134,922
  • 42
  • 253
  • 328
1

If you need to micro-optimize, yes.

In general, calling list.end() won't have a significant performance penalty and probably won't be an issue. It may return the same value every call, may be inlined, and so on. While not slow, it does take some small amount of time.

If you absolutely need the speed, you want to use for (iterator = list.start(), end = list.end; iteration != end; ++iterator). This caches the end iterator (and does a pre-inc), and should have no repeated calls.

The second type is typically unnecessary, but if .end() is expensive or the loop is very large, may be useful.

ssube
  • 47,010
  • 7
  • 103
  • 140
1

While premature optimization is evil, good habits are not. If you don't expect your loop termination condition to change, i.e. you're not changing the container, then this pattern can be used:

for (mylist::iterator it = alist.begin(), finish = alist.end();  it != finish;  ++it)

The compiler is unlikely to make this optimization for you if it can't determine that the container isn't changing.

Note that this is unlikely to make a measurable timing difference, but it can't hurt.

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • If the code is running on a system that's tight for registers or has a limited stack, creating extra variables can hurt. – Pete Becker Aug 28 '12 at 20:03
  • @PeteBecker, since the variable is replacing the result of a function call I think it's likely to help rather than hurt. You still might be right though, it's impossible to know without looking at the generated code. – Mark Ransom Aug 28 '12 at 20:06
  • Except that the value has to be kept around through the loop body; with a function call the result can be discarded after it's used. – Pete Becker Aug 29 '12 at 01:52
0

One good reason not to cache end() on std::list is that it prevents you from making the following mistake:

for (iterator = list.rstart(), end = list.rend(); iterator != end; iterator++) {
    // modify list

No iterators will be invalidated when you make modifications in std::list, but rend is NOT a sentinel (it points to the first element of the underying list), which means that it will stop being the end of the list if you append to the end of the reverse list (aka prepend to the beginning of the unreversed list.)

Edward Z. Yang
  • 26,325
  • 16
  • 80
  • 110