[a]: Obviously, people still write their own collections/containers in C#, but they tend to only do so for very specific/performance-sensitive circumstances.
[a]: Obviously, people still write their own collections/containers in C#, but they tend to only do so for very specific/performance-sensitive circumstances.
Whereas due to C++'s "never break compatibility" decision, the standard library has progressively decayed over time. It has become a bloated, rotting dinosaur where even the slowest of interpreted languages can comfortably beat several of its aspects. (Ex: std::regex is pathetic and pitiful, vector<bool> triggers laughter, substandard maps, etc). Considering that C++ thumps its chest and loudly proclaims its superb performance, this has now become a sad joke.
In the natural world, a species that cannot adapt to new circumstances and never discards undesirable characteristics simply perishes.
The C++ Standard Committee has firmly and unequivocally decided that the C++ language should mirror the same approach and limp down the road, carrying the full-weight of its sins for all its journey, until it falls into oblivion.
Also breaking everything a couple of times is why most corporations are nowadays stuck in a Python 2 / 3 parallel world in .NET Framework / .NET Core , or in Xamarin.Forms / MAUI, UWP / WinUI,...
One that I find particularly annoying lately: if you work on a project that makes heavy use of the STL (or, really, any heavily templated library written in a similar style, with a focus on "ergonomics" :-( ), you'll quickly find that backtraces for debug builds can easily be 50-100 levels deep with most stack frames just consisting of incomprehensible layers of abstraction which get optimized away. So, debug builds are totally useless, and build times are typically long. Contrast that with something written in "C style" or simply making far fewer use of C++ features, written in a more straightline style, with fewer levels of abstraction: builds will be fast, stack traces will be small and can easily map directly onto the concepts relevant to the program or library, and debug builds are useful once more. Night and day difference.
Maybe as an example just compare a library like Eigen to a similar imaginary library written in C.
Eigen leans heavily on C++ features to reduce line count and make something look visually more mathematical or maybe more MATLAB-style in the name of ergonomics; and obviously on the backend it relies on the behavior of the C++ compiler to simplify the elaborate template expressions and make the code efficient. If you ever try to step through code that uses Eigen in a debug build or examine a stack trace from inside Eigen where an exception has originated, the situation is not pretty! Contrast this with what will happen in the imagined C-style library. If all the heavy lifting happens in BLAS or LAPACK and the C library is basically a thin library to make things a little more automatic and easier to manage, the stack traces will be short and each stack frame will be easy to digest at a glance because of names like "mat_mat_mul" or similar, instead of "mat<double, double, 4, allocator<...>> const & operator*(mat<double, double... 159 more characters of template gobbledygook that makes the eyes glaze over)". The former will also be significantly faster to compile.
Anyway, I guess I just don't see how the latter STL/Eigen/whatever-style approach to things is an improvement over the far simpler idiot style of doing things.
I'm not trying to be argumentative here, I just want to clarify my point as much as possible.
My point is that one can have a lot of C++ working at scale. Reading code is very important part of large scale projects. Dropping debug builds is a reasonable tradeoff.
I would also argue that keeping it so that you can always easily run your project "REPL style" from a debugger exerts a very favorable downward force on all the complexities that make large projects hard to maintain.
Some of the common issues: static allocation or lack thereof, requiring default constructible classes, initializing memory you are going to overwrite anyway, inability to be used in some metaprogramming contexts, suboptimal allocation behavior, etc. The STL is opinionated but unfortunately that opinion dates to a time when C++ was primarily used for ordinary app development, not high-performance code.
Like many, I maintain my own C++ "standard library" that is much better designed for the kinds of software I tend to work on (database kernels and data infrastructure, mostly).
That's actually not true, though I certainly don't fault you for believing it :-) but there are definitely more things to quibble about around vector if you're serious about performance. As an example, try writing a can_fit(n) function, which tells you whether the vector can fit n elements without reallocating. Observe the performance difference between (a) a smart manual version, (b) a naive manual version, and (c) the only STL version: https://godbolt.org/z/88sfM1sxW
#include <vector>
template<class T>
struct Vec { T *b, *e, *f; };
template<class T>
bool can_fit_fast(Vec<T> const &v, size_t n) {
return reinterpret_cast<char*>(v.f) - reinterpret_cast<char*>(v.e) >= n * sizeof(T);
}
template<class T>
bool can_fit(Vec<T> const &v, size_t n) {
return v.f - v.e >= n;
}
template<class T>
bool can_fit(std::vector<T> const &v, size_t n) {
return v.capacity() - v.size() >= n;
}
struct S { size_t a[3]; };
template bool can_fit_fast(Vec<S> const &, size_t);
template bool can_fit(Vec<S> const &, size_t);
template bool can_fit(std::vector<S> const &, size_t); template<typename T,
class vector {
private:
pointer __begin_;
pointer __end_;
__compressed_pair<pointer, allocator_type> __end_cap_;
public:
constexpr const pointer& __end_cap() const noexcept {
return this->__end_cap_.first();
}
constexpr size_type size() const noexcept {
return static_cast<size_type>(this->__end_ - this->__begin_);
}
constexpr size_type capacity() const noexcept {
return static_cast<size_type>(__end_cap() - this->__begin_);
}
};
So if the functions are fully inlined we should end up with template<class T>
bool can_fit(std::vector<T> const &v, size_t n) {
return static_cast<size_t>(v.__end_cap_.first() - v.__begin_) - static_cast<size_t>(v.__end_ - v.__begin_);
}
At least algebraically (and ignoring casts) it should be equivalent to v.__end_cap_.first() - v.__end_, which is more or less what the manual implementations do. Maybe the optimizer can't make that transformation for some reason or another (overflow and/or not knowing the relationship between the pointers involved, maybe)?If you change can_fit(Vec<S>) to:
return (v.f - v.b) - (v.e - v.b) >= n;
You end up with code that looks pretty similar to the can_fit(std::vector<S>) overload (the same for clang, a bit different for GCC), so it does seem it might be something about the extra pointer math that can't be safely reduced, and the casts aren't really relevant.(I'm also a bit surprised that can_fit_fast produces different assembly than the Vec can_fit overload)
(There's also a secondary issue here, which is that sizeof(S) isn't a power of 2. That's what's introducing multiplications, instead of bit shifts. You might not see as drastic of a difference if sizeof(S) == alignof(S).)
I feel the division by sizeof(T) shouldn't matter that much, since the compiler knows it has pointers to T so I don't think the divisions would have remainders. I want to say pointer overflow and arithmetic on pointers to different objects (allocations?) should also be UB, so I suppose that might clear up most obstacles? I think I'm still missing something...
Does make me wonder how frequently this pattern might pop up elsewhere if it does turn out to be optimizable.
> Does make me wonder how frequently this pattern might pop up elsewhere if it does turn out to be optimizable.
Probably a fair bit, but as I mentioned, it might break a lot of code too, because there's too much code in the wild doing illegal things with pointers (like shoving random state into the lower bits, etc.). Or not... the Clang folks would probably know better.
Maybe this could be a good way to jump into messing with LLVM...
Out of curiosity, how much of a performance difference did you observe in practice when you made this optimization?
(Although, note I didn't claim "the STL is slow"... that's painting with a much broader stroke than I made.)
Speaking of realism, putting these in quickbench seems to confirm that the differences between them are not material, and that the STL version is in fact the quickest, but they are all essentially free. There's not a way to make a realistic microbenchmark for this, for the same reason that it doesn't feel like a real-world performance issue.
By the way clang does a much better job here: https://quick-bench.com/q/XcKK782d-7A6YHbiBRTlOnIRnPY
Your fallacy here is assuming that that just because N is a variable, therefore N is large. N can easily be 0, 1, 2, etc... it's a variable because it's not a fixed compile time value, not because it's necessarily large. (This isn't complexity analysis!)
> Speaking of realism, putting these in quickbench seems to confirm that the differences between them are not material, and that the STL version is in fact the quickest,
Your benchmark is what's unrealistic, not my example.
I'm guessing you didn't look at the disassembly (?) because your STL version is using SSE2 instructions (vectorizing?), which should tell you something funny is going on, because this isn't vectorizable, and it's not like we have floating-point here (which uses SSE2 by default).
Notice you're just doing arithmetic on the pointers repeatedly. You're not actually modifying them. It sure looks like Clang is noticing this and vectorizing your (completely useless) math. This is as far from "realistic" as you could possibly make the benchmark. Nobody would do this and then throw away the result. They would actually try to modify the vector in between.
I don't have the energy to play with your example, but I would suggest playing around with it more and trying to disprove your position before assuming you've proven anything. Benchmarks are notoriously easy to get wrong.
> So it doesn't feel like a realistic use case.
I only knew of this example because I've literally had to optimize this before. Not everyone has seen every performance problem; clearly you hadn't run across this issue before. That's fine, but that doesn't mean reality is limited to what you've seen.
I really recommend not replying with this sentiment in the future. This sort of denial of people's reality (sadly too common in the performance space) just turns people off from the conversation, and makes them immediately discredit (or just ignore/disengage from) everything you say afterward. Which is a shame, because this kind of a conversation can be a learning experience for both sides, but it can't do that if you just cement yourself in your own position and assume others are painting a false reality.
Admittedly that's not a performance issue, but it's annoying.
Vector<bool> is a little weird if you are just starting with C++, but it does have major performance benefits in its niche, and it came from the 1990s so we can be generous in overlooking its rough edges.
Not really. Unless you have a very specific set of performance requirements, STL containers are usually more than enough. And we have third party libraries such as Abseil/Boost to cover the major gaps in the rest. I do see some legitimate cases to write own container libraries. But for many cases people don't really measure their primary workload before writing such libraries, instead they just write it because they can write (and it's fun).
I mean: i remember the days, when each and every project had its own string class.
Every library having its own version of common data structure is unfortunately something that C++ programmers can't really seem to agree on :)
There are special cases, and there are engineering politics, but it's all basically fine.
Example: want to measure the length of a UTF-8 code point in a string and did not synchronize the call? Well, too bad, now you might have corrupted the global mutable state it relies on! (the fact that you can make such a trivial piece of code have two!!! points of thread-safety failure one is C locale and another is transcoder still refuses to leave my mind)