News for hackers. Stuff that matters.
Re:Appending to an std:string character-by-character: how do... by pacaro on Thursday October 26, 2023 @06:36PM (#38032564)
This is, of course, the same mechanism that allows appending to an ArrayList in Java to be O (1), or at least amortized too such
Re:Appending to an std:string character-by-character: how do... by kadoban on Thursday October 26, 2023 @06:43PM (#38032608)
Interestingly, the C++ standard doesn't specify a cost for this, or an implementation. So there could be some C++ implementation out there somewhere that just copies the whole string on each append.

This is in contrast to std::vector, where the cost of adding one element to the end _is_ specified to be amortized O(1).

Re:Appending to an std:string character-by-character: how do... by vitus on Thursday October 26, 2023 @07:35PM (#38033018)
> Interestingly, the C++ standard doesn't specify a cost for this, or an implementation.

Table 76 (page 797-798 of the C++20 draft [0]; page numbers read 789-790) specifies the sequence operations on containers that shall take amortized constant time. Among these includes push_back() for basic_string.

[0] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n48...

This was also the case in the C++11 draft, and quite possibly before then as well. (I don't have a copy of the C++03 spec handy)

Re:Appending to an std:string character-by-character: how do... by kadoban on Thursday October 26, 2023 @07:45PM (#38033092)
Woops, thanks. Not sure how I missed that.
Re:Appending to an std:string character-by-character: how do... by AnimalMuppet on Thursday October 26, 2023 @10:17PM (#38033941)
Yeah. It was only on page 797! How could you miss that?

;-)

Re:Appending to an std:string character-by-character: how do... by sltkr on Thursday October 26, 2023 @07:26PM (#38032949)
The C++ standard requires:

  1. Both std::vector<T> and std::basic_string<T> support push_back(T) with amortized constant performance.
  2. Additionally, std::basic_string<T> has an operator+=(T) that behaves semantically identical to push_back(T), but does not have a complexity requirement imposed by the standard.
Logically that leads to every reasonable standard library implementation to simply dispatch std::basic_string<T>::operator+=(T) to std::basic_string<T>::push_back(T) (or vice versa, of course) and have both operations run in amortized constant time.

You're technically correct that the standard theoretically allows push_back(T) and operator+=(T) to have different time complexities, so you could make operator+=(T) run in linear time if you're trolling (but in that case, why stop at O(N) and not make it O(2^N) or something?), but since push_back(T) and +=(T) need to be equivalent and the former needs to run in O(1) amortized time, there is no reason to make the latter perform worse than the former.

Re:Appending to an std:string character-by-character: how do... by lmm on Friday October 27, 2023 @12:59AM (#38034863)
Might an implementor use a specialised implementation for operator+= that performs better for practical-sized strings while not being O(1)?
Re:Appending to an std:string character-by-character: how do... by foota on Thursday October 26, 2023 @06:52PM (#38032679)
Are there reasonable string implementations lacking 0(1) amortized append runtime?
Re:Appending to an std:string character-by-character: how do... by sltkr on Thursday October 26, 2023 @07:28PM (#38032964)
No, because appending with push_back() which strings must also support is required to have O(1) amortized time.

See my comment here for details: https://news.ycombinator.com/item?id=38032949

Re:Appending to an std:string character-by-character: how do... by kadoban on Thursday October 26, 2023 @07:04PM (#38032775)
Depends what you mean by reasonable I suppose. If you just implement it naively, either by growing by 1 or a constant amount each time, it'll be O(n) amortized.

I'd call those potentially reasonable, in that it works and it doesn't break the standard. I probably wouldn't be mad if a student did that impl, but I'd mention it.

I would be surprised if a well-supported, non-toy implementation without strange constraints (eg not for embedded and not trying to optimize solely for size or simplicity or something) did something other than amortized O(1).

Re:Appending to an std:string character-by-character: how do... by not2b on Friday October 27, 2023 @01:39AM (#38035068)
No, but there was an unreasonable one. Microsoft Foundation Classes, which preceded the STL. Originally, Their CString class extended the capacity in such a way that appending one character at a time had quadratic cost.
Re:Appending to an std:string character-by-character: how do... by rurban on Friday October 27, 2023 @04:14AM (#38035956)
It was just using the naive approach (grow by the exact size), the same idea as Google is now proposing. https://news.ycombinator.com/item?id=38033206
Re:Appending to an std:string character-by-character: how do... by Permit on Thursday October 26, 2023 @07:05PM (#38032785)
> Under macOS (LLVM 15), I get that clang doubles the capacity and add one

That’s interesting, why add one? Is there something to suggest this is better than simply doubling in real world scenarios?

Re:Appending to an std:string character-by-character: how do... by kevinventullo on Thursday October 26, 2023 @07:27PM (#38032956)
It’s a stretch, but perhaps this is related to the desire to avoid hash tables which are of size exactly a power of two?

https://cs.stackexchange.com/questions/19020/why-should-one-...

Re:Appending to an std:string character-by-character: how do... by pjscott on Thursday October 26, 2023 @07:50PM (#38033120)
LLVM's libc++ doesn't use a generic growable vector thing for std::string; they rolled their own, specifically for strings. (I checked the source. Alas, no explanatory comments and nothing in the "initial libc++ import" git commit message.)
Re:Appending to an std:string character-by-character: how do... by akomtu on Thursday October 26, 2023 @07:11PM (#38032829)
The trailing 0 char?
Re:Appending to an std:string character-by-character: how do... by tedunangst on Thursday October 26, 2023 @07:32PM (#38032997)
In theory, it should be (len - 1) x 2 + 1 in that case.
Re:Appending to an std:string character-by-character: how do... by jltsiren on Thursday October 26, 2023 @08:24PM (#38033314)
With an allocation of size n, you have capacity for n-1 characters and the trailing 0, and the trailing 0 does not count as a character. If you double the size of the allocation to 2n, the capacity increases to 2(n-1) + 1.
Re:Appending to an std:string character-by-character: how do... by tedunangst on Thursday October 26, 2023 @09:23PM (#38033655)
So why does it increase the size of the allocation to 2n + 1?
Re:Appending to an std:string character-by-character: how do... by jltsiren on Thursday October 26, 2023 @10:05PM (#38033868)
Allocation size is capacity + 1. If you double the allocation size to 2 * (capacity + 1), the new capacity is 2 * capacity + 1.
Re:Appending to an std:string character-by-character: how do... by anonymoushn on Friday October 27, 2023 @12:41AM (#38034766)
Surely 2 * (capacity + 1) = 2 * capacity + 2
Re:Appending to an std:string character-by-character: how do... by jltsiren on Friday October 27, 2023 @01:38AM (#38035064)
That is the new allocation size. The new capacity is one character less. The allocation must include space for the trailing 0, which is not a part of the string and is hence not included in the length/capacity numbers.
Re:Appending to an std:string character-by-character: how do... by anonymoushn on Friday October 27, 2023 @02:27AM (#38035309)
I see, thanks for your patience :)
Re:Appending to an std:string character-by-character: how do... by ace2358 on Friday October 27, 2023 @05:31AM (#38036375)
Great thread, I was scratching me head a bit too.
Re:Appending to an std:string character-by-character: how do... by bluGill on Thursday October 26, 2023 @07:49PM (#38033115)
They may also know something about the memory allocator.
Re:Appending to an std:string character-by-character: how do... by rerdavies on Friday October 27, 2023 @01:02AM (#38034883)
Better L1/L2 cache performance? I'd put good money on that explanation.
Re:Appending to an std:string character-by-character: how do... by smoothc on Thursday October 26, 2023 @07:45PM (#38033093)
There’s also [small string optimization](https://stackoverflow.com/questions/10315041/meaning-of-acro...)
Re:Appending to an std:string character-by-character: how do... by compiler-guy on Thursday October 26, 2023 @08:03PM (#38033206)
Google has proposed adding precise growth to std::string on with the benefit that it saves quite a bit of memory and time you the string isn't being repeatedly appended. However, the project rejected the idea.

https://reviews.llvm.org/D102727

Re:Appending to an std:string character-by-character: how do... by jeffbee on Thursday October 26, 2023 @08:18PM (#38033286)
Google also proposed uninitialized resize, and the ability to treat string as if it were unique_ptr, by just giving it some already-allocated buffer. You can tell they use these internally because the fingerprints of these features are all over the protocol buffers open source libraries. I believe none of them have been adopted by the standard.
Re:Appending to an std:string character-by-character: how do... by terrelln on Thursday October 26, 2023 @09:02PM (#38033540)
folly provides functions to resize std::string & std::vector without initialization [0].

[0] https://github.com/facebook/folly/blob/3c8829785e3ce86cb821c...

Re:Appending to an std:string character-by-character: how do... by jeffbee on Thursday October 26, 2023 @09:12PM (#38033598)
Yeah, it's nice! And Abseil does it, IFF you use LLVM libc++.

https://github.com/abseil/abseil-cpp/blob/master/absl/string...

The standard adopted it as resize_and_overwrite. Which I think is a little clunky.

Re:Appending to an std:string character-by-character: how do... by spacechild1 on Thursday October 26, 2023 @09:06PM (#38033565)
> Google also proposed uninitialized resize

What would be the practical difference to std::string::reserve?

Re:Appending to an std:string character-by-character: how do... by jeffbee on Thursday October 26, 2023 @09:19PM (#38033632)
Reserve sets the capacity, not the size. If you were to reserve and overwrite the buffer, the size is still wrong. Now if you resize, STL overwrites your contents with value initialized chars.
Re:Appending to an std:string character-by-character: how do... by gary_0 on Thursday October 26, 2023 @09:26PM (#38033666)
Also I don't think the Standard requires that data() returns a valid pointer after only calling reserve(), so it's technically UB.
Re:Appending to an std:string character-by-character: how do... by zabzonk on Thursday October 26, 2023 @11:40PM (#38034445)
i think you may have misunderstood the standard - data() will always return a valid pointer, but the pointer returned may be invalidated by any number of functions:

    string s;
    s.reserve(50);
    char * p = s.data();   // ok so far
    s.reserve(100);        // now p may be invalid
Re:Appending to an std:string character-by-character: how do... by spacechild1 on Saturday October 28, 2023 @10:01AM (#38049869)
Of course, you need to call push_back() or insert() to add the data. Actually, insert() wouldn't even require reserve() in the first place.

I guess the idea of uninitialized resize is that you reserve the space, bump the size, and then you can directly set the new members with operator[]. If your input data is not contiguous, and would need to use push_back() otherwise, you can save a few if-checks. But that's pretty much it, I assume.

Re:Appending to an std:string character-by-character: how do... by rerdavies on Friday October 27, 2023 @01:43AM (#38035096)
Fwiw, they seem to imply (admit) that you can get precise growth using

    s.reserve(n); // precise allocation.
    s.resize(n);  // doesn't grow.
or

    s.reserve(n);
    s.append(....); // a fairly robust set of overloads.

That being said... I completely agree that imprecise resize() is unfortunate, and unexpected. I'm going to have to keep an eye out for how I use resize() going forward. (Not a lot, admittedly).
Re:Appending to an std:string character-by-character: how do... by zabzonk on Thursday October 26, 2023 @10:14PM (#38033924)
it is possible that the compiler has simply elided all that loop.
Re:Appending to an std:string character-by-character: how do... by Animats on Thursday October 26, 2023 @10:17PM (#38033937)
I've had much grief with this in Rust, adding to an array with "push".

Down at the bottom, this grows by resizing an allocation. Ordinarily, that's cheap. But if you're resizing a reasonably sized array, at least under Wine it's a copy of the array with the lock on the memory allocator set. There's a spinlock involved. If many threads are allocating, futex congestion is possible and speed drops by two orders of magnitude. Ouch.

Re:Appending to an std:string character-by-character: how do... by nynx on Thursday October 26, 2023 @11:14PM (#38034318)
That’s a bug in the allocator. Realloc should not grab a lock the entire time.
Re:Appending to an std:string character-by-character: how do... by Animats on Friday October 27, 2023 @02:46PM (#38042337)
It's complicated. There are three nested locks. I could see snapshots of what was happening in gdb, but don't fully understand the dynamics.

The effect was that a game-type program dropped from 60FPS to 0.5 FPS.

Re:Appending to an std:string character-by-character: how do... by hawk_ on Thursday October 26, 2023 @11:45PM (#38034469)
Surely that must be a specific allocator. Does rust allow you to use say jemalloc or alternatives? Or are you saying this lock is acquired no matter which allocator is used?
Re:Appending to an std:string character-by-character: how do... by connicpu on Thursday October 26, 2023 @11:59PM (#38034536)
Yeah you can override the global allocator pretty easily[1]. But the default uses the system allocator, and I guess it sounds like Wine's is pretty bad

[1]: https://doc.rust-lang.org/std/alloc/index.html#the-global_al...

Re:Appending to an std:string character-by-character: how do... by Animats on Friday October 27, 2023 @02:30AM (#38035332)
It's in the DLL that Wine uses to emulate Windows. Not in the Rust library.

Bug report: [1]

[1] https://bugs.winehq.org/show_bug.cgi?id=54979

Re:Appending to an std:string character-by-character: how do... by hawk_ on Friday October 27, 2023 @09:19AM (#38038063)
Nice! Thanks for the link. Also is it the John Nagle who reported this?
Re:Appending to an std:string character-by-character: how do... by Animats on Friday October 27, 2023 @02:44PM (#38042315)
Yes.
Re:Appending to an std:string character-by-character: how do... by rurban on Friday October 27, 2023 @02:34AM (#38035355)
Wonder why he didn't mention the correct growth factor for the geometric series, the golden ratio 1.618f.

It's only not used when shift by 1 was so much cheaper than float mult.

llvm's libc++ uses this grow logic instead:

    if (actual > 30)
      actual = ((actual & ~15) == actual) ? (actual + 15) 
               : ((actual + 15) & ~15) - 1;
which is actually closer to the metal, allocating a new page when needed.