Live data from Hacker News

Under the hood: Vec

marma.dev

101–110 of 142 posts

Re: Under the hood: Vec<T>

#101

Earlier quoted context omitted.

I could look this up, but I’m enjoying reading this conversation. Do reserve and reserve_exact increment the capacity, or ensure there’s still at least that much capacity remaining? If the former, if I reserve(1) 10 times in a row, does that mean it could be rounding up to a thousand elements (say, because of the page table size) each time?

At least that much remaining. For both Vec::reserve and Vec::reserve_exact the parameter is an unsigned integer representing how many more items you expect to need space for. So reserve(1) 10 times in a row will just repeatedly ensure there's enough space for at least 1 more item, which after the first time there certainly is† There's an excellent chance the capacity check in particular got inlined, so the optimized…

That makes sense, and it's how I'd hope it'd work. I can imagine all sorts of cases where I'm getting data from an external source, like an API request returning some mudball of data monstrosity, where the easiest path doesn't give all the information at once.

Nice. I truly do appreciate the developer ergonomics that went into Rust. Its APIs are pleasantly well thought out.

Re: Under the hood: Vec<T>

#102

Because this is focused on how the data structure works it doesn't mention lots of nice API design choices in Rust. The one I particularly want to call out because it came up this morning is providing both Vec::reserve and Vec::reserve_exact Vec::reserve lets us hint about our upcoming capacity expectations without damaging the O(1) amortized growth which is the whole point of this collection type, but it can waste s…

Just today I saw a (2 year old) video on that very topic in Rust and C++: https://www.youtube.com/watch?v=algDLvbl1YY Towards the end they mention what to use instead in C++ to get the same characteristics as Rust's Vec::reserve:

    vec.insert(
        vec.cend(),
        std::move_iterator(newElems.begin()),
        std::move_iterator(newElems.end())
    );

Re: Under the hood: Vec<T>

#103

Earlier quoted context omitted.

From your description, I cannot determine the difference. Engineers like exact things, whats this fuzzy concept called “hint”?

Hint means that the implementation is free to ignore it. It's just some extra metadata that the implementation can optionally make use of.

In this case, the implementation is not free to ignore calls to reserve or reserve_exact -- the documentation makes explicit guarantees about this: "After calling reserve, capacity will be greater than or equal to self.len() + additional".

This matters for performance reasons, and also because unsafe code is allowed to use the extra capacity of the vector -- e.g. you can write to the unused space and then call set_len to insert elements in-place. If `reserve` did not guarantee the requested capacity was allocated, this would cause buffer overflows.

The documentation for these functions is very carefully worded to explain exactly what each method does and does not guarantee. Both functions have the exact same hard guarantees: after calling the function, the vector will have at least enough capacity for the additional elements.

The difference is in what the methods try to do without guaranteeing exact behavior: reserve follows the usual amortized growth pattern, while reserve_exact tries to avoid overallocating. These are described as best-effort performance optimizations rather than guarantees you can rely on because 1) the amortized growth strategy is subject to change between platforms and compiler versions, and 2) memory allocators typically don't support arbitrarily-sized blocks of memory, and instead round up to the nearest supported size.

Re: Under the hood: Vec<T>

#104
post #57

Earlier quoted context omitted.

Nothing really happens on the instruction level because this is all type system logic.

I think this might be the issue. If something has zero effect in the end, why should I care about in the first place?

Variance doesn't affect generated code because it acts earlier than that: determining whether code is valid or not and in doing so preventing invalid code (UB) from being compiled in the first place.

The simplest example of incorrect variance → UB is that `&'a mut T` must be invariant in T. If it were covariant, you could take a `&'a mut &'static T`, write a `&'b T` into it for some non-static lifetime `'b` (since `'static: 'b` for all `'b`), and then... kaboom. 'b ends but the compiler thought this was a `&'a mut &'static T`, and you've got a dangling reference.

`&'a mut T` can't be covariant in T for a similar reason: if you start with a `&'a mut &'b T`, contravariance would let you cast it to a `&'a mut &'static T`, and then you'd have a `&'static T` derived from a `&'b T`, which is again kaboom territory.

So, variance’s effect is to guide the compiler and prevent dangling references from occurring at runtime by making code that produces them invalid. Neither of the above issues is observable at runtime (barring compiler bugs) precisely because the compiler enforces variance correctly.

Re: Under the hood: Vec<T>

#105
post #90

Earlier quoted context omitted.

I believe that you are describing `Vec::with_capacity` which allows to change the initial reserved memory on construction. `reserve` and `reserve_exact` are used when mutating an existing vec. What you provide is not the total wanted capacity but the additional wanted capacity. `reserve` allows to avoid intermediate allocation. Let's say that you have a vec with 50 items already and plan to run a loop to add 100 more…

This should be in the docs or a blog post somewhere. Very clear explanation.

That's a nice idea, thank you. I have personal blog, I'll try to clean it up a bit and provide performance measurements so it's worth posting.

Regarding the official documentation, I've returned to read them. I agree that the docs would benefit from more discussion about when to use each method. In particular, the code examples are currently exactly the same which is not great. Still, the most critical piece of information is there [0]

> Prefer `reserve` if future insertions are expected.

If anyone wants to reuse my explanation above, feel free to do it; no need to credit.

[0]: https://doc.rust-lang.org/std/vec/struct.Vec.html#method.res...

Re: Under the hood: Vec<T>

#106
post #57

Earlier quoted context omitted.

Nothing really happens on the instruction level because this is all type system logic.

I think this might be the issue. If something has zero effect in the end, why should I care about in the first place?

It doesn't have zero effect. Like everything about type systems, it helps prevent incorrect, and possibly unsound, code from compiling. So I guess the giant runtime effect is that you either have a program to run or not.

Re: Under the hood: Vec<T>

#107
post #76
post #46

Earlier quoted context omitted.

> Vec::reserve_exact is a more narrow idea - we can hint about the ultimate capacity needed, if we're wrong and later need more capacity this has a significant performance cost because we thew away the amortized growth promise to get this, but we don't waste memory. The claim that using reserve_exact "throws away the amortized growth promise" is wrong. You don't disable amortized growth, you just won't get extra head…

> The claim that using reserve_exact "throws away the amortized growth promise" is wrong Well, in some sense this depends on what exactly you think you're amortizing over. If you `Vec::reserve` with size N, fill to N, and then append a single element you get the usual amortized O(1) growth of an append (or at least you can, the docs for `Vec::reserve` say it may reserve additional space, not that it must ). But if yo…

> if you `Vec::reserve_exact` with size N, fill to N, and then append a single element you are guaranteeing that that first append triggers a potentially O(N) resize.

The documentation does not guarantee this, because memory allocators can't typically allocate arbitrarily-sized blocks of memory, instead rounding to the nearest supported size. For example, for small allocations glibc malloc allocates in multiples of 8 bytes, with a minimum size of 24 bytes. So if you make a 35-byte allocation, there will be 5 bytes of wasted space at the end which you could theoretically use to store more elements without reallocating if your collection grows.

If you're using the system allocator, Rust can't take advantage of this, because the C malloc/free APIs don't provide any (portable) way for the allocator to inform the application about this excess capacity. But Rust's (currently unstable) API for custom allocators does expose this information, and the documentation is written to allow Vec to take advantage of this information if available. If you reserve_exact space for 35 u8's, and your allocator rounds to 40 bytes (and informs the Vec of this the allocator API), then the vector is allowed to set its capacity to 40, meaning the next append would not trigger a resize.

On current stable Rust, this is all just theoretical and Vec works as you describe -- but the documentation specifically does not promise this because the situation is expected to change in the future.

Re: Under the hood: Vec<T>

#108
post #90

Earlier quoted context omitted.

I believe that you are describing `Vec::with_capacity` which allows to change the initial reserved memory on construction. `reserve` and `reserve_exact` are used when mutating an existing vec. What you provide is not the total wanted capacity but the additional wanted capacity. `reserve` allows to avoid intermediate allocation. Let's say that you have a vec with 50 items already and plan to run a loop to add 100 more…

This should be in the docs or a blog post somewhere. Very clear explanation.

It is, though not worded as nicely as the GP comment.

docs: https://doc.rust-lang.org/std/vec/struct.Vec.html#method.res...

Re: Under the hood: Vec<T>

#109

Earlier quoted context omitted.

I don't think the point of the article is the technical information, I think it's more of an emotional expression. Still valuable, just differently, I suppose.

Like much of what surrounds Rust. Looks quite emotional to me. If you do not know what I mean, go to the Rust reddit and discuss and compare on solid grounds without using an extremely flattering tone. You will see armies of fanatics voting negative.

If you actually look at the Rust subreddit: https://www.reddit.com/r/rust/top/?sort=top&t=all

The fifth and eighth articles are explicitly negative about Rust. The seventh is about serious bugs in the Rust ecosystem.

Let's look at the top comment on the highest post: https://www.reddit.com/r/rust/comments/1cdqdsi/lessons_learn...

It's Josh Triplett, long time team member. It starts like this:

> First of all, thank you very much for taking the time to write this post. People who leave Rust usually don't write about the issues they have, and that's a huge problem for us, because it means we mostly hear from the people who had problems that weren't serious enough to drive them away. Thank you, seriously, for caring enough to explain the issues you had in detail.

This is a very different vibe than the one you're describing.

It's true that the Rust subreddit can brush criticism off, but that's also because there are a lot of low-quality criticisms of Rust, and seeing the same thing over and over again can be frustrating. But I've never seen a well thought out post that's critical get downvoted, they're often upvoted, and discussed in a very normal way.

That said, it's reddit, so there's always gonna be some garbage posts.

Re: Under the hood: Vec<T>

#110

I'm sure all these layers are good engineering and provide meaningful safety guarantees, but it sure makes the code harder to understand if you want to see how things are implemented. Another example, I was trying to see how i64::isqrt is implemented, but first you have to wade through layers of macros

"The standard library internally overuses macros and you probably shouldn't use them that much in your own code" is at least a respectable opinion in the Rust community, I think.

Yep. Heck, I've barely even written any macros at all.
Post reply on HN