News for hackers. Stuff that matters.
Re:In C++, is empty() faster than comparing the size with zero? by thamer on Thursday October 28, 2021 @01:50PM (#29028923)
It's interesting that size() has to be constant-time. From what I understand of the standard linked from the blog post, it looks like size() being constant-time is a property of Container and not each individual method (§ 23.2.1, page 747).

What does this mean for implementations that want to have these properties but have internal structures that make this inherently difficult? Do they have to recompute a cached size value during other operations, and amortize the cost of size() on insert, for example?

I can see it being tricky in some situations: imagine a collection with expiring data, for example. You might want to prune the expired items in the background, but still have size() return the number of elements currently not expired. How would someone do this while maintaining an exact size() value when the background clean-up will inherently take some time?

Re:In C++, is empty() faster than comparing the size with zero? by plorkyeran on Thursday October 28, 2021 @02:04PM (#29029120)
The standard's requirements for the standard library types don't imply any corresponding requirement for types you define yourself. If you want to write a container which can't reasonably do O(1) size() then you just don't do O(1) size(). If such a container was added to the standard library, the standard library's Container requirements will be adjusted to permit that.
Re:In C++, is empty() faster than comparing the size with zero? by chakkepolja on Thursday October 28, 2021 @02:43PM (#29029808)
Exactly, most STL algorithms, for example, can be applied using iterators as well. But anyway if GP has specific requirements iterator invalidation might be a problem too, they wouldn't probably want to constrain their interface to that of STL containers.
Re:In C++, is empty() faster than comparing the size with zero? by tialaramex on Thursday October 28, 2021 @04:42PM (#29031671)
Wait, isn't the point of the STL algorithms that they use iterators?

For sure the iterator over a vector may be very cheap to use, but it is still an iterator, even if you tacitly know it's just pointer arithmetic under the hood.

Re:In C++, is empty() faster than comparing the size with zero? by brandmeyer on Thursday October 28, 2021 @02:23PM (#29029393)
Once upon a time, size() wasn't required to be constant-time. This showed up in some standard library implementations of std::list::size which actually did count all the things.

The painful ABI break at C++11 is a big part of why implementors were so opposed to allowing another ABI break in C++20.

Re:In C++, is empty() faster than comparing the size with zero? by jeffbee on Thursday October 28, 2021 @02:40PM (#29029751)
It's also why other people in the C++ community are opposed to ABIs themselves.
Re:In C++, is empty() faster than comparing the size with zero? by brandmeyer on Thursday October 28, 2021 @03:21PM (#29030427)
The live-at-head model really only works well in a cloud computing or JIT environments. It really shouldn't have been a surprise to find that this model is unpopular outside of those narrow use-cases.
Re:In C++, is empty() faster than comparing the size with zero? by jcelerier on Thursday October 28, 2021 @06:30PM (#29032862)
in my personal experience it also works great for desktop software
Re:In C++, is empty() faster than comparing the size with zero? by saagarjha on Thursday October 28, 2021 @11:00PM (#29034711)
That depends. It’s awful for the libraries that your OS provides to support your desktop app, for example.
Re:In C++, is empty() faster than comparing the size with zero? by tialaramex on Thursday October 28, 2021 @04:34PM (#29031551)
> What does this mean for implementations that want to have these properties but have internal structures that make this inherently difficult?

Such implementations are de facto forbidden in the STL.

Although its containers have vague names like "forward_list" today the C++ STL's containers are in practice very specific data structures that have the specified properties because other data structures would miss one or more of the requirements.

This suits "stability at all costs" C++ programmers fine. Better that every C++ program is 10% slower than that their buggy library from 1995 has to be fixed to actually do what the standard says rather than whatever worked in 1995.

If you care about this but you like C++ you just don't use the STL. Third parties are free to make containers that use data structures that aren't old enough to stand for President of the United States of America, some of them aren't even old enough to legally drive in the United States of America.

Re:In C++, is empty() faster than comparing the size with zero? by fhrow4484 on Thursday October 28, 2021 @02:08PM (#29029188)
> What does this mean for implementations that want to have these properties but have internal structures that make this inherently difficult

I'm not sure I can think of an example of such a data structure?

my understanding is that whatever your data structure is, you can keep a "size_t current_size;" private counter which you atomically increment/decrement on every insertion/deletion. size() is simply:

    size_t size() { return current_size; }
In your background clean-up example structure, if the background thread is removing N elements, it must decrease current_size by N. Atomically! (i.e. remove the element and decrease the current_size using a mutex::lock/unlock)
Re:In C++, is empty() faster than comparing the size with zero? by johntb86 on Thursday October 28, 2021 @02:41PM (#29029769)
Some

  std::list::splice(const_iterator pos, list& other, const_iterator first, const_iterator last)
implementations used to be constant-time, since they could just change some pointers in the spliced nodes. Nowadays they take linear time, since they need to count how many nodes are transferred so they can update the sizes correctly.
Re:In C++, is empty() faster than comparing the size with zero? by jfrunyon on Thursday October 28, 2021 @05:19PM (#29032160)
Wouldn't this still be constant-time today? It doesn't need to count the nodes because the other list already has a count of its nodes.
Re:In C++, is empty() faster than comparing the size with zero? by hermitdev on Thursday October 28, 2021 @05:59PM (#29032592)
No, because you'd still need book keeping around the length of [first, last), even if the implementation does under-the-hood O(1) movement of nodes in [first, last) from other to *this. Computing the length of [first, last) is still an O(n) operation for forward iterators.

Maybe you're assuming [first, last) is the entirety of other? For that case, there are overloads for splicing an entire list in O(1) time:

    void splice( const_iterator pos, list& other );
    void splice( const_iterator pos, list&& other );
Re:In C++, is empty() faster than comparing the size with zero? by jfrunyon on Thursday October 28, 2021 @06:53PM (#29033074)
This seems like an extremely niche use-case, compared to the prevalence of use-cases needing to check the size of a list.
Re:In C++, is empty() faster than comparing the size with zero? by ycombobreaker on Thursday October 28, 2021 @08:02PM (#29033645)
This niche is exactly why I've used std::list in several cases. It allows fairly easy preallocation of list nodes.

Fortunately, the single-node splice is still fast. But batch allocations, say related to some scatter/gather operations, would still be hit by this change.

Re:In C++, is empty() faster than comparing the size with zero? by MauranKilom on Thursday October 28, 2021 @05:37PM (#29032368)
No, because you can splice in the middle. Given just the iterators you simply have no other way than counting.

(The reason why it used to be constant time is that it didn't have to count because it didn't have to compute the new size).

Re:In C++, is empty() faster than comparing the size with zero? by ape4 on Thursday October 28, 2021 @04:16PM (#29031322)
Interesting, a good case for another std::list<> like std::non_counting_list<>
Re:In C++, is empty() faster than comparing the size with zero? by ycombobreaker on Thursday October 28, 2021 @08:05PM (#29033669)
Reminds me of Michael Bolton from Office Space, "Why should I change, he's the one who sucks!"

std::list WAS noncounting. introducing std::counting_list was an option. I didn't follow this decision so I don't know why they opted to break the behavior (performance expectations).

Re:In C++, is empty() faster than comparing the size with zero? by klyrs on Thursday October 28, 2021 @04:09PM (#29031236)
> I'm not sure I can think of an example of such a data structure?

C style strings are the classical example. Computing the size (strlen) takes linear time. Not saying that this is a good idea or anything, but it's extremely common.

One could imagine a binary tree implemented in a similar manner. It costs a little time and memory to keep track of the current size, but it almosy always pays off.

Re:In C++, is empty() faster than comparing the size with zero? by kllrnohj on Thursday October 28, 2021 @08:22PM (#29033801)
Which is long since fixed with std:string & co. The data structure concept of a string has no need for linear length calculation. Rather, C just did strings badly.
Re:In C++, is empty() faster than comparing the size with zero? by thamer on Thursday October 28, 2021 @05:23PM (#29032206)
My point was that the background thread would remove elements periodically, by checking something like:

    if (it->expiry_time < now) erase(it);
Where erase() could certainly remove the item and update the size, but it would remove elements after they have expired. So if size() must return the actual number of unexpired elements present right now, the cached counter you mention would not be up to date.

This is what I meant by "have size() return the number of elements currently not expired". What you're describing is "size returns the number of elements currently not expired OR expired but not yet cleaned up", which is different.

Re:In C++, is empty() faster than comparing the size with zero? by sgtnoodle on Friday October 29, 2021 @03:52AM (#29036271)
When you have multiple threads running concurrently, you're going to have to deal with coherency and synchronization one way or another.

What does `now` even mean? It's presumably a sampled count from a running clock. With concurrent execution, there's no guarantee about the relationship between two thread's samples of the clock unless you synchronize them. One thread may read the clock, then get preempted for a while before doing any actual work. According to the preempted thread's point of view it's still in the present and running calculations without a care in the world, but from the preempting thread's point of view it's frozen in the past to be thawed out in the future.

Do you really need to be that picky about coherency of this hypothetical multi-threaded expiring data structure? So what if the size reads slightly larger on average? It seems like the reasonable direction to err on, since elements are getting deleted out from under you anyway, and so there's an inherent race condition between when a thread would call size() and then call another function dependent on the result of calling size().

If you need it to truly be cycle-by-cycle coherent with the hardware clock, then you'll need to add synchronization. The size() function will no longer take a predictably constant time, as it will either block on a mutex, spin some number of times on a lock-free atomic, or remove expired elements itself.

As other folk have also pointed out, though, the time complexity of the STL member functions are only a self-imposed rule. A function like size can do whatever you want if you're writing the implementation.