26 October 2021 · 4 min
Most C++ programmers rely on “STL” for their data structures. The most popular data structure is probably vector, which is just a dynamic array. The set and the map are other useful ones.
The STL data structures are a minimalist design. You have relatively few methods. All of them allow you to compute the size of the data structure, that is, how many elements it contains, via the size() method. In recent C++ (C++11), the size() method must have constant-time complexity for all containers. To put it in clearer terms, the people implementing the containers can never scan the content to find out the number of elements.
These containers also have another method called empty() which simply returns true of the container is… well… empty. Obviously, an equivalent strategy would be to compare the size with zero: mystruct.size() == 0.
Determining whether a data structure is empty is conceptually easier than determining its size. Thus, at least in theory, calling empty() could be faster.
Inspecting the assembly output, I find that recent versions of GCC produce nearly identical code for the comparison of the size and the empty call. The exception being the list data structure where the assembly is slightly different, but not in a manner that should affect performance.
Of course, there are different implementations of C++ and it is possible that other implementations could provide more efficient code when calling empty(). An interesting question is whether effort is needed from the compiler.
Travis Downs wrote a list data structure by hand, but with a size() function that is linear time. He then implemented the empty function naively:
struct node { struct node *next; int payload; }; int count_nodes(const node* p) { int size = 0; while (p) { p = p->next; size++; } return size; } bool is_not_empty(const node* p) { return count_nodes(p) > 0; }
Amazingly, we find that the GCC compiler is able to compile Travis’ is_not_empty C++ function to constant-time code. The compiler inlines the count_nodes function into is_empty. Then the compiler figures out that as soon as you enter the loop once with count_nodes, then size is going to be greater than zero, so there is no need to keep looping.
However, the optimisation is not robust. Suppose that I wish instead to return an unsigned type instead of Travis’ int value. The problem with an unsigned type is that I might overflow if the list is very long. With a signed integer, the compiler is allowed to assume that overflows do not happen. It could be difficult for the compiler to tell whether count_nodes() return 0 or not, if the compiler must handle overflows. To handle this potential issue, I can forcefully bound the return value of count_nodes() to be no more than 1000. If I change the code to return a standard size_t type, like so…
#include <cstddef> struct node { struct node *next; int payload; }; size_t count_nodes(const node* p) { size_t size = 0; while (p) { p = p->next; size++; if(size == 1000) { return 1000; } } return size; } bool is_not_empty(const node* p) { return count_nodes(p) > 0; }
Sadly, GCC is now unable to optimize away the call. Maybe compilers are not yet all-powerful beings?
The lesson is that it is probably wise to get in the habit of calling directly empty() if you care about performance. Though it may not help much with modern STL data structures, in other code it could be different.
Of course, another argument is that the call to empty() is shorter and cleaner.
Credit: This blog post was motivated by a tweet by Richard Startin.
Daniel Lemire, "In C++, is empty() faster than comparing the size with zero?," in Daniel Lemire's blog, October 26, 2021, https://lemire.me/blog/2021/10/26/in-c-is-empty-faster-than-comparing-the-size-with-zero/.
[BibTeX]
Still disappointed C++ and Rust containers don’t have a non_empty() method, and the Rust discussion fizzled out.
Have you considered writing `not array.empty()`?
The is_empty function in this blog post has an obvious mistake 🙂
> the compiler figures out that as soon as you enter the loop once with count_nodes, then size is going to be greater than zero, so there is no need to keep looping.
It could result in an endless loop, is it allowed to optimize that away? Also, it could wrap (to 0 for unsigned, or negative for signed). With 32 bit integers, it would be quite easy; with 64 bit, not sure how long it would take / memory size needed. Anyway, to me it looks like an incorrect optimization.
The other case, with a limit on 1000, on the other hand, can be optimized away.
Ah I see, integer overflow causes undefined behaviour. And for endless loops: GCC assumes that a loop with an exit will eventually exit (option -ffinite-loops)
-ffinite-loops (or conversely -O2 -fno-finite-loops) does not affect the assembly output of the optimizable example in this article.
In C++, an infinite loop without side effects is Undefined Behavior, so the compiler is allowed to optimize it away.
Amazingly, yes! But only because the function has a signed return type.
is_not_empty()specifically compares the return value ofcount_nodes()to 0, returning true if it’s greater or otherwise false. And inliningcount_nodes()allows the compiler to observe thatsizecan only increase and never decrease. Because signed values cannot overflow™, any situation which would cause an overflow is undefined behaviour, and can be handled however the compiler desires. Thus, if the number of nodes can be stored in anint, it can be compared to 0. And if it’s sufficiently large that it _can’t_ be stored in anint, then it invokes UB, allowing the compiler to just say that “if N > 0, then N+X > 0 because X is always positive”; it would overflow, but since it **can’t** overflow, the compiler must assume that the value is incomprehensibly positive. Thus, assizecan never be smaller than 0, all UB cases are automatically> 0, and all cases wheresizeis in the range of 1 toINTMAX(inclusive) are> 0, the only situation whereis_not_empty()could possibly return false is whensizeis never increased. And that, in turn, allows the compiler to recognise that assizewill always increase if the loop body is entered, any situation where the loop body enters will implicitly fail the check.This allows it to replace the loop body with
while (p) { size = 1; break; }, which in turn allows the inlinedcount_nodes()to be flattened to something likeint size = 0; if (p) size = 1; return size;, and then transformed into something like(p ? 1 : 0)for inlining purposes. (Because inlining allows the compiler to understand that we only care whethercount_nodes()does or does not return 0 specifically, and thus allows it to treat all non-zero "return values" as 1.) Which means thatis_not_empty()can be treated as having an effective body ofreturn (p ? 1 : 0) > 0, and thus that the entire function is essentially a null pointer check with more work.In essence, the facts that 1)
sizecan only increase and never decrease, 2) the function only cares whethersizeis greater than zero (and its actual value doesn't matter), and 3) signed variables are not allowed to overflow (thus any situation which causes signed overflow is UB), allow it to optimise infinite loops away. Becausesizecan never decrease, any infinite loop will simply result in an infinitely positive value, and because there's no signed overflow, this infinitely positive value will never loop around, and will always be greater than zero. Thus, any situation which would cause signed overflow, such as an infinite loop, simply means thatis_not_empty()will return true; importantly, it doesn't need to actually _enter_ the infinite loop to know this, because it knows that it's impossible™ for the loop to ever decreasesize's value. Basically, if you ask someone that can only count to ten whether infinity is bigger than zero, they can answer "infinity is bigger than ten, so yes"; they don't need to actually count to infinity to figure it out, they just need to realise that infinity is bigger than they can count and stop there.----
Notably, this is also why changing
count_nodes()to use an unsigned type removesis_not_empty()'s constant-time complexity. Unsigned values are allowed to overflow, and have defined wraparound behaviour when they do so. This means that sufficiently highsizes can, in fact, be exactly equal to zero, and thus it cannot make assumptions anymore. The signed version is constant-time because the compiler is allowed to use UB to make logical assumptions such as "numbers that can never decrease will never decrease", and ignore the hardware limitations in favour of the code's actual intent. The unsigned version has no UB for the compiler to work with, and also has known breakpoints where the signed assumption will be provably incorrect, forcing it to calculate an exact value instead of making assumptions about values too large to calculate. It's the same situation that allows compilers to realise that ifxis signed,(x + 1) > xandx * 2 / 2 == xare always true ifxis signed, even if the expression would cause signed overflow, as explained in LLVM's What Every C Programmer Should Know About Undefined Behavior article.Significantly, while infinite loops would also be UB, and could result in the compiler just discarding the entire true branch and forcing
is_not_empty()to always return false, the compiler cannot actually _determine_ whether a loop would be infinite at compile time, off of the given information. For a loop to be infinite, a node would need to be linked to a node that's earlier in the list, which would only happen at runtime. Thus, for the compiler to do infinite loop optimisations, it would need to be able to prove that allnode-based linked lists are infinite, which it cannot do with the given code. The best it could do would be to throw in a few runtime traps just in case, but it's unlikely that the optimiser would keep the traps in place. So, the compiler won't really care whether the loop is infinite or not, since it has no way to know.----
tl;dr: Basically, the compiler can't tell whether the loop is infinite or not, so the only UB it can see is signed overflow. Thus, any loop that _would_ be infinite is just a case of
(x + 1) > x, and the compiler can determine that the answer is "true" regardless of whether the loop is infinite or not. Which then goes on to let the optimiser see that the _real_ question is whether(0 + x) > 0, and that entering the loop always results inx > 0, thus allowing it to determine that the entire function is just a "would we enter the loop?" check with more steps. (The unsigned version doesn't have that, though, forcing it to actually iterate over the list in case there are exactly enough nodes to overflow to 0. And even if we cap it to _n_, it still needs to run through the loop a maximum of _n_ times. Hence the unsigned version never seeing this optimisation.)Maybe a typo? In the end of paragraph 2:
> to find out the number of containers
should probably be
> to find out the number of elements
Yes. Thanks.
Better?
bool is_empty(const node* p) {
return p ;
}
Er. return !p
I’m not sure what the point of this article is. The author explicitly states “the people implementing the containers can never scan the content to find out the number of elements.”
Then continues on to test against exactly what he described as against the rules. Its not hard to keep track of the size of a container via a class member variable. There is literally no need to scan the items. The empty() probably just returns the size as a bool. This entire discussion and article is pointless.
The author explicitly states “the people implementing the containers can never scan the content to find out the number of elements.”
For STL containers.
Here is the conclusion:
The assumption is that you will not just use STL containers in all of the code you are relying upon. Of course, if you are quite sure to only use recent STL containers, then you may consider the blog post pointless, but what makes you so sure?
The point of it is that
empty()can sometimes be optimised more aggressively thansize(), so we should assume thatempty()is always at least as efficient assize() == 0, and sometimes more efficient. Thus, it’s better to get in the habit of doing the former and not the latter.thanks for bringing the awareness to us
I have spent a bit too much time in Python, but I wish the STL containers had an implicit conversion to bool. That would be even less code than calling empty().
I still prefer clarity and readability. I use empty() to show the intent to, well, check emptiness.
FYI: https://godbolt.org/z/bh3PMe8nG
Seems like “<=" allows more optimizations than a simple "!="
With that implementation, the count_nodes function returns 0 for an empty list and 1000 for a non-empty list. That is not what was meant for the overflow-preventing case.