For processing strings, streams in C++ can be slow
lemire.me
lemire.me
(From the Node.js GitHub issue.) Sounds like this guy is mixing up his Java knowledge with C++ knowledge.
C++ streams are frankly insane: Loads of implicit state, needing to set about a half dozen flags to do any nontrivial formatting, running the risk of accidentally "poisoning" all downstream operations if you forget to reset any of that state, the useless callbacks API [1], obfuscated function names (xsgetn, epptr, egptr), a ridiculously convoluted inheritance hierarchy that includes virtual/diamond inheritance [2], and use of virtual functions for simple buffer manipulation. These were all bad decisions even at the time.
[1] https://en.cppreference.com/w/cpp/io/ios_base/register_callb...
That is exactly it. C++ string streams have had atrocious performance since forever. Good abstraction, not very useful in practice.
In Java, if I remember correctly, strings are immutable, so the StringBuilder or whatever ridiculous name it had was the faster way to build a string.
> "I recently learned that some Node.js engineers prefer stream classes when building strings, for performance reasons."
Pretty much tells you everything you need to know about node js, I guess.
Imagine you have two Java Strings: a and b
If you write the Java code:
String c = a + b;
The Java compiler will roughly replace this code with: String c = new StringBuilder(a).append(b).toString();
I'm not sure I would say this is "smart". Rather, it is just a hack to allow Strings to override the plus (+) operator. Normally, Java does not allow operator overidding, like C++.First, it has nothing to do with overriding the + operator, the operation is statically decidable so the compiler is perfectly aware that this is a string concatenation (not a numerical addition) and string is a special builtin, the compiler can generate anything it wants. Which is exactly what it does, they just originally decided to implement string concatenation as stringbuffer/stringbuilder ops. Meanwhile the addition of integers, longs, floats, and doubles are (or were anyway) different bytecode ops. This does not require general purpose operator overloading support. Builtins are not limited to the language’s user level semantics.
And second OpenJDK has not done that since Java 9 (JEP 280), the compiler now emits generic “string concatenation” bytecode for the runtime / jit to deal with.
a += b + c;
Would become something like StringBuilder sb = new StringBuilder();
sb.append(a);
sb.append(b);
sb.append(c);
a = sb.toString();
However it was never capable of doing such an optimisation across loops.In Java 9, they gave up on such static optimisations, instead the compiler now emits dedicated string concatenation bytecode (see JEP 280), which the runtime (and JIT) can then hook into.
Does that handle loops? Unclear. Benchmarks / testimonies from back then (java9/java10 days) hint that no, direct concatenation remains much slower than StringBuilder. But I didn’t find anything super recent so maybe they improved the behaviour in the meantime.
Google Closure Library includes a StringBuffer class. [1]
I recall it having explanatory notes, but I don't see them in the code now. JavaScript engines can optimize a string concatenating to in-place edit, if there is only one reference to the first string. The StringBuffer class keeps the reference count at one, guaranteeing this optimization is available, even if the StringBuffer itself is ever shared.
[1] https://github.com/google/closure-library/blob/master/closur...
Overloading the shift operators for this purpose is prima facie insane, and anyone who has single-stepped through a C++ "hello world" program can figure out it isn't remotely efficient, but it was certainly creative.
What's broken with them has nothing to do with bikeshedding about operator choice.
Which just shows how bad streams are. Overloading the shift operators was a terrible decision on multiple levels, but you're right -- that's not the worst streams sin.
Of course you could just use good old C printf to get some work done. But if you did that the "real" C++ programmers would sneer at you.
The fun thing with printf is that variadic functions cannot handle C++ classes. In the past was doubly fun because compilers proactively compiled an error into the binary instead of raising an error at compile time. So any forgotten c_str() became a crash waiting to happen.
C Printf never was, never could be and never will be a suitable way to output data from C++. Now excuse me while I go through the list of thousands of predefined format macros to find out which I need to use to output a uint_fast16_t without making the compiler vomit nonsense.
printf("%d\n", (int) myfast16_t);
Not that terrible for a type that I've never used, nor seen used.If you cast to signed that can't represent the complete range that still wouldn't be called "overflow" AFAIK, and no matter what you call it it is not "undefined". And assuming 32-bit ints there is no loss of information given a 16-bit ints.
You can also just cast to unsigned or whatever type you think is enough (you should know). The point is, use a conversion, cast to a simple type, make your code compatible.
I could have a 16 bit wide bitmask in it, lets flip them ~myMaskFast16, that leaves the higher order bits set to 0xFF... whether I care about them or not.
> The point is, use a conversion, cast to a simple type, make your code compatible.
Casting to a type that depending on platform may or may not hold enough space to represent the value is not "making it compatible" it makes it non portable.
That's really not a valid assumption. There are 16 bit int platforms, I've worked on them. It's not even that uncommon.
Casting in this scenario is simply incorrect. The correct thing to do is to use the formatting macros e.g. PRIuFAST16. Which noone ever does because it's gross and most developers don't actually care about portability.
As someone who has been programming in C++ since before there were C++ compilers (back in the day, you had to run it through a translator to make it into C code, then use a C compiler), I think I'm as real of a C++ programmer as anybody.
C++ programmers who sneer at you for this are fully worthy of being ignored.
Of course, printf() has its own set of issues as well.
https://github.com/nodejs/node/pull/50253
Note that the person mixing java knowledge and C++ isn't Daniel Lemire.
If you really need speed, then estimate how large string you need in advance and preallocate it (either with new char[], or string::reserve ig).
C++ the base language has issues, for sure
But iostream is like taking someone that's crazy to use every single language feature and who think there's some ulterior motive to create these crazy inheritance levels etc
Maybe we need C+=2 to make things less crazy
Almost everything related to c++ iostreams has this code smell of OOP pushed too far:
- Usage of runtime virtual dispatch with virtual calls when it was not necessary. Causing a negative unavoidable impact on performance.
- Heavy usage of function overloading with the "<<" operator. Leading to pages long compilation errors when an overload fails.
- Hidden states everywhere with the usage of state formatters and globals in the background.
- Unnecessary complexity with std::locale which is almost entirely useless for proper internationalisation.
- Bloat. Any statically compiled binary will inherit around ~100k of binary fat bloat when using iostream
- Useless encapsulation with error reports done as abstracted bit flags. Which is absolutely horrendous when dealing with file I/O: It hides away the underlying error with no proper way to access it.
- Deep class hierarchy making the entire thing looks like spaghetti.
- Useless abstraction with stringstream that hides the underlying buffer away, making it close to unusable on embedded safety critical systems where memory allocations are forbidden.
All of that made <iostreams> aged pretty badly, and for good reasons.
Fortunately there is an incoming way out of that with work of Victor Zverovich on std::format and libfmt [1].
Those are great, but iostreams hasn't been necessary in a very long time thanks to other libraries like Qt and Boost.
The success of Victor has been to make the C++ committee accepts the idea that a new formatter was necessary and to bring <format> in the STL.
This was not a small task: The committee has its fair amount of dinosaur gatekeepers and windmills [1]. For the best and the worst.
We at least now have a way forward to evolve from <iostream> if we want to with maybe one day the hope of getting something that can entirely replace iostream.
[1]: Windmills: Person displacing air around but not much more than air.
For all their flaws, iostreams have the advantage of being simple to implement. They are just overloads of the << and >> operators. std::format and likes require a lot of meta-programming magic to work correctly. It means longer compile times, less tolerance for broken compilers, and possibly weird edge cases, which are important considerations when designing a standard that will be used everywhere. And when it's there, it is there for good, so I understand the committee for being careful.
From ANSI C++ to C++20, a lot of work has been done making meta-programming more sensible, and computers became more powerful, which makes it ready for something like std::format.
And pulling Boost, let alone Qt just to avoid the occasional use of iostreams (or printf) is a bit much IMHO. I usually try to avoid Boost, as I feel it is more of a sort of beta/preview for the standard library. Don't get me wrong, it is production-worthy, but it can lead to awkward things when some boost feature ends up in the standard libraries and the project ends up with bits of both.
std::format is great because at last, we can use it without dependencies.
I agree that the formatting could have been done better, and that part is indeed handled much better in fmt, although personally I dislike format strings. It's much better than printf, granted.
- crafting a high context yet succinct description
- addressing PR feedback well
- giving respect to a pedantic commenter who understands the inner workings far less than Daniel while not conceded to make a destructive change.
I will share this PR widely as arole model in open source contributions.
I periodically have interview candidates work through problems involving binary search, then switch to bounded and ask them how to make it go faster over N elements, where N is < 1e3. The answer is "just linear search, because CPUs really like to do that".
One thing I don't like about lemire's phrasing is that he only looks at the current, often only most available, implementations and doesn't make this point explicit for most cases.
EDIT: Thankfully he does acknowledge that in a later post [2].
[1] https://timsong-cpp.github.io/cppwp/n4861/strings#string.app...
[2] https://lemire.me/blog/2023/10/23/appending-to-an-stdstring-...
I have a hard time believing that because std::vector guarantees that the memory is contiguous.
The lesson here is to always, always watch your own review tone, and not make this mistake.
The other lesson is that when a PR shows up with this kind of technical information attached to it, spend the 60 seconds it takes to Google for "lemire".
The rapid incorporation of the excellent `format` package for printing points to a future falling back at least to ANSI buffered IO and possibly raw POSIX IO.
There's also a proposal for a type safe scanf: scnlib, sort of format in reverse: https://scnlib.dev/en/master/
printf("%d %s", i, obj.convert_to_string())
because the const char* you get from obj.convert_to_string() might be deleted from the heap before printf() is called. And similar issues. printf("%s\n", obj.to_string().c_str());
should be perfectly fine. struct ZeroCopyBuf : public std::streambuf
{
ZeroCopyBuf(const std::string &s) : ZeroCopyBuf(s.c_str(), s.length()) {}
ZeroCopyBuf(const char *c, std::size_t l) : ZeroCopyBuf(const_cast<char*>(c), l) {}
ZeroCopyBuf(char *c, std::size_t l) { setg(c, c, c + l); }
};
...
std::string s ...;
ZeroCopyBuf buf(s);
std::istream is(&buf);
Terrible.
C++-20 makes it better by adding move semantics to most methods... Still.The array_source device can be used with any buffer (char*, size_t)
The little I did competitive programming, input parsing time was negligible compared to the allowed runtime for solving the problem. Inputs were designed so that if you had the right algorithm, you could do it easily even with terrible optimization. Fast code could be an advantage in the algorithm (but not in parsing), as it could help you "cheat" and, for example, do a problem designed for N² in N³. Personally, I used iostreams, just because I found it a bit easier to type.
But then, different competition have different rules, and maybe there are some where fscanf really is an advantage.
std::ios_base::sync_with_stdio(false);
What is the effect of turning off synchronization with legacy functions from C? When C++ is used for I/O and no C is used this should be a habit. I’ve the impression that most C++ books don’t mention it (e.g. Primer) or only late.It is similar to String and StringBuilder from Java. You need to know it, remember it and use it by habit. And again, books often mention it only late (e.g. Head First).
By the way. I like the plain things from <iostream>, especially the shift << and >> operators and ease of concatenating and handling strings. But as others mentioned, the implementation (e.g. inheritance) looks complicate.
Source https://en.cppreference.com/w/cpp/io/ios_base/sync_with_stdi...
std::sync_with_stdio(false);
Helps with that a bit.
The fastest possible way is to have C strings in an array and run a function over it.
If there are advantages to OOP, speed is not one of them.
Here's a great talk about software speed: https://www.youtube.com/watch?v=pgoetgxecw8
If a compiler is sophisticated enough a functional program should perform as well as a procedural one that uses a comparable garbage collector. But of course real compilers have shortcomings.
I recommend taking a read at Haskell's wiki performance article[1] to have an understanding of the shortcomings that are specific to Haskell.
That said, most usage of closures in practice tend to have very little state manipulation -- a closure with 5 mutable fields is weird, an object with 5 mutable fields is "clean code" approved. Also, closures have only one entry point which makes making complex ones much more difficult (you have to implement a state machine or dispatch or whatever.)
Standard "design patterns" over-the-top OOP translated to FP would be like passing around collection of closures (one per method) that all alter a big shared mutable state. At that point OOP is definitely going to be faster, but the FP code would be so ugly you wouldn't write it like that to start with.
Well, that's not always true; it's better to use a profiler.
In many cases, it's trivial for the compiler to inline objects (e.g., used as predicates for <algorithm> routines), resulting in better performance compared to the equivalent procedural code.
> running constructors and destructors is expensive
If the object is not polymorphic (and sometimes even if it is), the compiler can inline both the constructor and destructor, resulting in exactly the same assembly output as the procedural code.
In the case of C++ I'd put something like: you can use free or costly abstractions, and OOP in general has a preference towards costly ones.
Also vector is a weird point to make, it's been some time I had to deal with Java (luckily) but arrays there are also linear AFAIK. And there are GCs that have a bump allocator for new objects (not sure if Java fits here), so cache would benefit more than in sparse malloc allocations in C/C++.
I think the point is that Object[] in Java is a linear block of pointers to objects, whereas vector<Object> in C++ is a linear block of the objects themselves.
Deep down the problem could be rephrased as "there are no structs in Java". In C# for example you could have a vector of structs and enjoy linear memory access.