Profiling is essential. I found a performance bug in calling some C++ functions a while ago, because they accepted a const std::string& and were being called in loops with a C const char*. Every single call had to construct a std::string involving a strlen, allocation and copy.
std::string_view is a nice fix for this but few programmers seem to use it yet.
std::string_view can optimize both performance and code readability in code sections which handle strings. However it can also lead to UB and to memory issues if used incorrectly.
The C++ version uses many memory allocations. Using allocators some in the C++ program would certainly cut down on the number of allocations. It would also be interesting to see if doing so also improved performance.
Similarly, it would be interesting to see if using the C++17 string_view (or the gsl version if C++17 isn't available to you) instead of `const string &` parameters affected performance.
Finally. I see that in most (all?) cases, objects are returned by value, not returned through reference parameters or pointers. It's interesting to see that that choice didn't compare poorly to a C implementation.
Uhh... you do realize C++ std::string has small string optimizations right? That would probably take care of most of the 30%. const string& would probably take care of the rest. No program should have performance problems due to strings.
i have been using std::string for many, many years and have never needed to know how my specific standard libraries implement it. if i want to worry about performance, i do it by timing and profiling, not reading standard library code.
I would expect that std::string_view would still be significantly faster. Copying or moving an std::string with small string optimization is likely going to boil down to a branch (to check if the instance is using the small string optimization) and a memcpy. As opposed to copying or moving an std::string_view, which should be two MOV instructions.
I'm not even sure if I do stuff like this in prototypes. My experience has been that using a matrix/arena/pool can speed a program up that has inner loop allocations by x7. I think the average pc can do about 10,000,000 heap allocations per second, but as far as I know it causes thread locking to some degree.
Don't many std::string implementations have small string optimizations? This is actually the first time I have every heard of C++ strings being the bottleneck of an application (and it seems that is even still up for debate here).
C++ code usually use std:: string, std::string_view, or other string classes that stores the string size and has some capacity pre-allocated to avoid such issues.
I don't even know where to start with C++ stdlib string problems, but being mutable and doing a unique heap allocation (above a certain length - a behaviour which however isn't even standardized) are definitely at the top of the list.
std::string_view would have been a good thing if it hadn't added another memory corruption foot gun.
A universal string type is one of those things where you can either have convenience or performance, but never both.
"you can't go around sticking the entirety of a parsed file in a std::string"
Sure you can. C++ is at best programmed like python. Just throw the working first version together - and once you hit a performance gap, you can optimize it.
Using a profiler.
Premature optimization is mostly pointless because for most non-trivial performance critical apps the bottleneck wont be where you expect it.
The faster you find the actual problematic hotspots with production content the better. The way to get there fast is to treat C++ like filthy python.
Don't trust anyone trying to goad you into premature optimization unlesa they understand the specific problem you are trying to solve, and only then with skepticism.
std::string is a zero-cost abstraction joined at the hip to a non-zero-cost abstraction. At heart, it’s an abstraction over the pattern of having a uniquely-owned string pointer which is freed when the owner (object or stack frame) is freed. If you use it when you would otherwise use that pattern, the overhead should be effectively zero, at least if your STL implementation is high-quality.
However, as you note, the API encourages you to use that ownership pattern everywhere, in a way you likely wouldn’t if you were doing allocation by hand – and often by accident. As the worst offender, the copy constructor allocates a new buffer and copies the whole string, though it’s implicitly called whenever you simply assign one std::string variable to another, or pass a std::string to a std::string-typed function parameter. It’s hard to avoid copies even if you know what you’re doing, and easy to misunderstand the costs if you don’t. In contrast, if you were doing allocation by hand, most of the time you’d try to just pass around pointers to existing buffers, with various ownership semantics: temporary access (borrowed), transferring ownership (move), or even using reference counting.
- For the unowned case, you could always use `const std::string &`, but that only works when referencing a full existing heap allocation; it can’t represent substrings of heap allocations, or data that wasn’t on the heap in the first place. C++17 improved the situation by adding std::string_view, but you have to know to use it, especially since the API design is limited by backwards compatibility constraints: for example, std::string’s substr method still returns a std::string, not a string_view. And some people say that in ‘modern C++’ you should avoid raw unowned types to ensure safety, so if you follow their advice you would have to avoid string_view as well.
- For the transferring-ownership case, C++11 added essentially zero-cost move construction that just moves the pointer from one std::string to another. But again, you have to know to use it, and the default is an expensive copy.
- For reference counting, you could use std::shared_ptr, but it’s suboptimal (always using atomic reference counting even if you don’t need it, and doing a double heap indirection).
Rust’s standard library has a type with similar semantics, String. But with the benefit of hindsight, it improves upon std::string in each of those areas:
- For the unowned case, there’s &str, which is like C++ string_view, and it’s idiomatic to use &str everywhere instead of &String (the equivalent of C++ `const std::string &`). Slicing a String (i.e. the equivalent of substr) gives you &str; if you want to copy that into its own owned buffer, you have to do it explicitly via `.to_string()`. Also, Rust’s borrow checker verifies that your use of unowned pointers is safe, so there’s no safety/performance tradeoff.
- Regarding the transferring-ownership case, Rust makes move semantics the default. Assigning one String-typed variable to another, or passing a String to a String-typed function parameter, will do a move; if you try to use the source afterwards, or if you don’t uniquely own the source, it will complain at compile time (there are workarounds depending on what you want to do). If you want to copy the data to a new heap allocation, you have to explicitly write `.clone()`.
- For reference counting… well, it’s arguably less than optimal in Rust too, but better than C++. For example, the language provides atomic and non-atomic reference-counting wrapper. You can use the faster non-atomic version if you don’t need to share the value across threads (and the compiler prevents you from doing so by accident). Also, you can avoid the double indirection by using Rc<str> instead of Rc<String>.
- Oh, and there is also Cow, the copy-on-write type, which represents a pointer that may or may not be owned. I don’t think the C++ STL has any equivalent; some STL implementations used to make all std::strings copy-on-write, before C++11 made that illegal, but that adds overhead for all users rather than just those who need it.
Thus, I’d say Rust’s String has a better claim to being a true zero-cost abstraction.
To really make string_view sane and safe, you'd have to make std::string reference counted (with each string view holding a ref), but I can't see that being popular.
std::string_view is a nice fix for this but few programmers seem to use it yet.
reply