Vec<T>

(marma.dev)

130 points | by r4um 3 days ago

11 comments

  • tialaramex 3 hours ago
    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 some memory to achieve this. This is probably what you want in most code.

    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.

    C++ only provides the equivalent of Vec::reserve_exact which makes this a footgun but if you use this call when you really needed the other one you trash your perf, as a result of which people teaching C++ tend to just say don't use reservation to uh, reserve capacity, but now you're leaving perf on the table so that's not great.

    • panzi 29 minutes ago
      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())
          );
    • smallstepforman 3 hours ago
      From your description, I cannot determine the difference. Engineers like exact things, whats this fuzzy concept called “hint”?
      • throwaway173738 3 hours ago
        Suppose I think I may need 128 entries at some point, but the vector is allocated with room for 16 entries by default. I may not want to allocate and then immediately allocate again. But if I get to a 17th entry then I’m already causing allocation. So I might as well allocate 128 at that time so there are no more allocations at all.
        • demurgos 3 hours ago
          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 (so 150 in total). The initial internal capacity is most likely 64, if you just do regular `push` calls without anything else, there will be two reallocations: one from 64 to 128 and one from 128 to 256.

          If you call `reserve(100)`, you'll be able to skip the intermediate 64 to 128 reallocation: it will do a single reallocation from 64 to 256 and it will be able to handle the 100 pushes without any reallocation.

          If you call `reserve_exact(100)`, you'll get a single reallocation for from 64 to 150 capacity, and also guarantee no reallocation during the processing loop.

          The difference is that `reserve_exact` is better if these 100 items were the last ones you intended to push as you get a full vec of capacity 150 and containing 150 items. However, if you intend to push more items later, maybe 100 more, then you'd need to reallocate and break the amortized cost guarantees. With `reserve`, you don't break the amortized cost if there are follow-up inserts; at the price of not being at 100% usage all the time. In the `reserve` case, the capacity of 256 would be enough and let you go from 150 to 250 items without any reallocation.

          In short, a rule of thumb could be:

          - If creating a vec and you know the total count, prefer `Vec::with_capacity`

          - If appending a final chunk of items and then no longer adding items, prefer `Vec::reserve_exact`

          - If appending a chunk of items which may not be final, prefer `Vec::reserve`

          • rsyring 1 hour ago
            This should be in the docs or a blog post somewhere. Very clear explanation.
            • unshavedyak 7 minutes ago
              It is, though not worded as nicely as the GP comment.

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

            • demurgos 17 minutes ago
              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 return 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 to credit.

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

        • arka2147483647 3 hours ago
          All the functions mentioned above, even the cpp one, will reserve atleast the number of elements given to resize() or resize_exact(), but may reserve more than that.

          After some pondering, and reading the rust documentation, I came to the conclusion that te difference is this: reserve() will grow the underlaying memory area to the next increment, or more than one increment, while reserve_exact() will only grow the underlaying memory area to the next increment, but no more than that.

          Eg, if grow strategy is powers of two, and we are at pow(2), then reserve() may skip from pow(2) to pow(4), but reserve_exact() would be constrained to pow(3) as the next increment.

          Or so i read the documentation. Hopefully someone can confirm?

          • swiftcoder 2 hours ago
            > even the cpp one, will reserve atleast the number of elements given

            The C++ one, however, will not reserve more than you ask for (in the case that you reserve greater than the current capacity). It's an exact reservation in the rust sense.

            > reserve() will grow the underlaying memory area to the next increment, or more than one increment, while reserve_exact() will only grow the underlaying memory area to the next increment, but no more than that

            No, not quite. Reserve will request as many increments as it needs, and reserve_exact will request the exact total capacity it needs.

            Where the docs get confusing, is that the allocator also has a say here. In either case, if you ask for 21 items, and the allocator decides it prefers to give you a full page of memory that can contain, say, 32 items... then the Vec will use all the capacity returned by the allocator.

            • kbolino 2 hours ago
              As far as I can tell, in the current implementation, reserve_exact is indeed exact. The only situation in which the capacity after calling reserve_exact will not equal length + additional is when it was already greater than that. Even if the allocator gives more than the requested amount of memory, the excess is ignored for the purposes of Vec's capacity: https://github.com/rust-lang/rust/blob/4b57d8154a6a74d2514cd...

              Of course, this can change in the future; in particular, the entire allocator API is still unstable and likely won't stabilize any time soon.

              • tialaramex 2 hours ago
                Maybe more interestingly, line 659, slightly above that, explains that we know we got [u8] but today the ordinary Rust allocator promises capacity is correct, so we just ignore the length of that slice.

                We could, as that comment suggests, check the slice and see if there's enough room for more than our chosen capacity. We could also debug_assert that it's not less room, 'cos the Allocator promised it would be big enough. I dunno if that's worthwhile.

            • vlovich123 35 minutes ago
              > In either case, if you ask for 21 items, and the allocator decides it prefers to give you a full page of memory that can contain, say, 32 items... then the Vec will use all the capacity returned by the allocator.

              It would be nice if this were true but AFAIK the memory allocator interface is busted - Rust inherits the malloc-style from C/C++ which doesn’t permit the allocator to tell the application “you asked for 128 bytes but I gave you an allocation for 256”. The alloc method just returns a naked u8 pointer.

            • arka2147483647 2 hours ago
              https://en.cppreference.com/w/cpp/container/vector/reserve.h...

              says

              > Increase the capacity of the vector (the total number of elements that the vector can hold without requiring reallocation) to a value that's greater or equal to new_cap.

              I belive that the behaviour of reserve() is implementation defined.

              • tialaramex 2 hours ago
                Because there's only a single function here, it has to either be Vec::reserve or Vec::reserve_exact

                If you don't offer Vec::reserve_exact then people who needed that run out of RAM and will dub your stdlib garbage. If you don't offer Vec::reserve as we've seen C++ programmers will say "Skill issue" whenever a noob gets awful performance as a result. So, it's an easy choice.

              • mandarax8 2 hours ago
                That said MSVC,GCC and clang all implement it to allocate an exact value.
          • vlovich123 38 minutes ago
            You misread the documentation. Reserve-exact is precisely that - the growth strategy is ignored and you are ensured that at least that many more elements can be inserted without a reallocation. Eg reserve_exact(100) on an empty Vec allocates space for 100 elements.

            By contrast reserve will allocate space for the extra elements following the growth strategy. If you reserve(100) on an empty Vec the allocation will be able to actually insert 128 (assuming the growth strategy is pow(n))

      • pornel 1 hour ago
        reserve() reallocates by at least doubling the capacity.

        reserve_exact() reallocates by exactly what you ask for.

        If you reserve() space for 1 more element a 1000 times, you will get ~30 reallocations, not 1000.

        This inexact nature is useful when the total size is unknown, but you append in batches. You could implement your own amortised growth strategy, but having one built-in makes it simple for different functions to cooperate.

      • charcircuit 44 minutes ago
        Hint means that the implementation is free to ignore it. It's just some extra metadata that the implementation can optionally make use of.
        • NobodyNada 24 minutes ago
          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.

      • mytailorisrich 3 hours ago
        The difference according to Rust doc itsef:

        reserve()

        > Reserves capacity for at least additional more elements to be inserted in the given Vec<T>. The collection may reserve more space to speculatively avoid frequent reallocations.

        reserve_exact()

        > Reserves the minimum capacity for at least additional more elements to be inserted in the given Vec<T>. Unlike reserve, this will not deliberately over-allocate to speculatively avoid frequent allocations... Prefer reserve if future insertions are expected.

        So if you know you'll need n now but will probably need more later, use reserve(). If you know n is all you'll need, use reserve_exact().

        Vec<T> is a contiguous growable array, so the optimization game is to minimise allocations, reallocations (to keep it contiguous), and unused allocated memory.

        • kstrauser 1 hour ago
          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?

          • tialaramex 47 minutes ago
            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 machine code will not "actually" call a function ten times, it'll call it once and then maybe emit a single capacity check inline, the subsequent checks even a crap 1980s optimiser will go "We do not need to check that again" and skip it.

            † If the Vec is full and can't grow, which really can happen for Zero Size Types in particular, then this panics, and what happens next is a compile time choice, in debug likely it just tells you what went wrong and suggests how to make a backtrace. In the ordinary case that we instead got to keep running then it succeeded.

            • kstrauser 30 minutes ago
              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.

    • lossolo 3 hours ago
      > 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 headroom from that call. If you guessed too small, you may pay an extra reallocation later. Rust's Vec still provides amortized O(1) push overall.

      > C++ only provides the equivalent of Vec::reserve_exact which makes this a footgun but if you use this call when you really needed the other one you trash your perf, as a result of which people teaching C++ tend to just say don't use reservation to uh, reserve capacity, but now you're leaving perf on the table so that's not great.

      It's not a reserve_exact only footgun. The standard says reserve(n) makes capacity at least n, implementations may allocate exactly n ( and many do), but they're allowed to overallocate and future growth remains amortized. Used properly (when you know or can bound the size), reserve is a common and recommended optimization.

      • khuey 2 hours ago
        > 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 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.

        From that point on in either case you would get the usual amortized growth of course.

        • NobodyNada 9 minutes ago
          > 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.

        • lossolo 1 hour ago
          Sure, but this isn't a job for reserve_exact. Vec::reserve is like a framing hammer made for speed and momentum. You might dent a little extra wood (overallocate), but you drive lots of nails fast and keep amortized O(1) growth. reserve_exact is a jeweler's hammer so great when you know the exact setting and won't touch it again, precise fit, zer slack etc.

          Now, try to frame a house with jeweler's hammer, tapping in tiny increments and resizing every few boards and you will crawl into quadratic time.

          Who is using jeweler's hammer to frame their house?

          So what I'm arguing is if you don't misuse reserve_exact, then as you said you still get amortized growth. The OP's example misuses the tool and then blames it for not behaving differently on that first append.

      • adgjlsfhk1 2 hours ago
        > The claim that using reserve_exact "throws away the amortized growth promise" is wrong

        The key part is that multiple calls reserve_exact will cause repeated allocations. The classic example is if someone defines a `pushfive` function that uses reserve_exact to increase the size by 5, and then pushes 5 times. Calling this function in a loop will take quadratic time since each reserve_exact call increases the array size. With reserve, the runtime is linear as expected.

        • lossolo 2 hours ago
          Quadratic pushfive is just a cautionary tale about misusing reserve_exact. Basically use reserve_exact when you know the final size (reserve once), or you're doing a one off tight sizing where memory footprint matters.

          Don't pre reserve inside pushfive, just push the 5 elements (Vec handles growth) or if you know how many times you'll call pushfive in a loop, reserve up front once vec.reserve(5 * times) or use reserve instead of reserve_exact for incremental growth.

          • adgjlsfhk1 2 hours ago
            that's exactly the footgun. reserve_exact usage needs to be analyzed globally, while `resere` has the extra fuzziness needed to ensure that you can't mess anything up too badly with it.
            • lossolo 1 hour ago
              Calling that a "footgun" feels really overstated.

              Yes, reserve is the safer default because it gives you slack and preserves amortized growth even if your usage pattern is messy.

              But "needs global analysis" isn't unique to reserve_exact. Lots of perf knobs do (chunk sizes, buffering, locking granularity etc). The fix to this is to use the tool where its preconditions actually hold, not to avoid the tool.

              So what I'm basically saying is that reserve_exact isn't inherently dangerous, it just assumes you know the final size and won't exceed it etc. If you keep bumping it in tiny steps (pushfive style), that's just misuse so treating it as a flaw is unwarranted.

  • veber-alex 5 hours ago
    This article feels too long and rambly considering how little actual technical information it provides.
    • fuckyah 4 hours ago
      [dead]
    • LoganDark 4 hours ago
      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.
      • germandiago 3 hours ago
        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.

        • spoiler 2 hours ago
          To be fair, that is just the typical Reddit (or any social media platform) experience
        • LoganDark 1 hour ago
          I won't deny that there are lots of emotions surrounding Rust, both for myself and for many others. But there are different ways to write about it, and this article looks more of an emotional style ("here's how my journey went") than a technical one ("here's how this works"). I still find it fun to read, but not everyone will, and that's okay.
        • echelon 2 hours ago
          It's what you call an "educational post".

          The Rust community is doing a fantastic job of training both the next generation as well as those looking to dip their toes in.

  • 5- 5 hours ago
    • James_K 4 hours ago
      Oh, I never knew that Rust had variance. I always just assumed everything was invariant. Strange that they've got no way to write it down in the type system.
      • pornel 4 hours ago
        In prehistoric Rust, variance used to be named more explicitly. However, the terminology of covariant and contravariant subtyping of lifetimes is a language theory jargon. This is the right perspective for language design, but programmers using the language don't necessarily use these terms.

        It's been replaced with a "by example" approach. It's much easier to teach it: just add a fake field that acts if you had this type in your struct. Rust then figures out all of the details it needs.

        • lpghatguy 3 hours ago
          Years ago, I introduced Flow gradual typing (JS) to a team. It has explicit annotations for type variance which came up when building bindings to JS libraries, especially in the early days.

          I had a loose grasp on variance then, didn't teach it well, and the team didn't understand it either. Among other things, it made even very early and unsound TypeScript pretty attractive just because we didn't have to annotate type variance!

          I'm happy with Rust's solution here! Lifetimes and Fn types (especially together) seem to be the main place where variance comes up as a concept that you have to explicitly think about.

          • ameliaquining 1 hour ago
            Note that this works because Rust doesn't have inheritance, so variance only comes up with respect to lifetimes, which don't directly affect behavior/codegen. In an object-oriented language with inheritance, the only type-safe way to do generics is with variance annotations.
        • kibwen 4 hours ago
          Variance is an absolute disaster when it comes to language pedagogy. One of the smartest things Rust ever did was avoiding mentioning it in the surface-level syntax.
          • branko_d 3 hours ago
            Why? It's just a property of type transformation.

            Assuming Parent <- Child ("<-" denotes inheritance):

            - If Generic<Parent> <- Generic<Child>: it's covariant.

            - If Generic<Parent> -> Generic<Child>: it's contravariant.

            - Otherwise: it's invariant.

            Or at least it's that straightforward in C#. Are there complications in Rust?

            • kannanvijayan 2 hours ago
              The difficulty is that even trivial generic types aren't cleanly one or the other. A mutable reference type is covariant on read, and contra on write.

              Scala was the first language in my exposure to try to simplify that away by lifting the variance annotations into the type parameter directly. It reduced some of the power but it made things easier to understand for developers. A full variance model would annotate specific features (methods) of a type as co/contra/invariant.

              I'm not sure what approach C# takes - I haven't looked into it.

              Rust doesn't expose variances for data structures at all. It exposes them for traits (type classes) and lifetimes, but neither of those are accessible in a higher order form. Trait usage is highly constrained and so is lifetime usage.

              Traits mainly can be used as bounds on types. Some subset of traits, characterized as object traits, can be used as types themselves, but only in an indirect context. These are highly restricted. For example, if you have traits T and U where U extends T, and you have a reference `&dyn U`, you can't convert that to a `&dyn T` without knowledge of the underlying concrete type. You _can_ convert `&A where A: U` to `&B where B: T`, but that just falls out of the fact that the compiler has access to all the concrete type and trait definitions there and can validate the transform.

              Rust's inheritance/variance story is a bit weird. They've kept it very minimal and constrained.

              • zozbot234 2 hours ago
                Note that the Rust folks are working on a "safe-transmute" facility that may end up introducing a kind of variance to the language.
            • Sharlin 3 hours ago
              It's not particularly easy to teach what that actually means and why it's a thing. It's quite easy to show why in general G<A> cannot be a subtype of G<B> even if A is a subtype of B, it's rather more involved pedagogically to explain when it can, and even more confusing when it's actually the other way around (contravariance).

              Anyway, Rust has no subtyping except for lifetimes ('a <: 'b iff 'a lasts at least as long as 'b) , so variance only arises in very advanced use cases.

              • smallstepforman 3 hours ago
                I’m getting old. I can understand the words, but not the content. At this point, show me dissassembly so that I can understand what actually happens on the fundamental byte/cpu instruction level, then I can figure out what you’re trying to explain.
                • Sharlin 2 hours ago
                  Nothing really happens on the instruction level because this is all type system logic.
                  • Grayskull 1 hour ago
                    I think this might be the issue. If something has zero effect in the end, why should I care about in the first place?
                    • bobbylarrybobby 23 minutes ago
                      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.

                    • Sharlin 9 minutes ago
                      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.
            • tialaramex 3 hours ago
              Sure, you can keep telling me that and it doesn't stay. I'm completely happy writing Rust, and I am aware it needs variance to work in principle and when I do need that information I know the magic words to type into doc search.

              It's like how I can hold in my head how classical DH KEX works and I can write a toy version with numbers that are too small - but for the actual KEX we use today, which is Elliptic Curve DH I'm like "Well, basically it's the same idea but the curves hurt my head so I just paste in somebody else's implementation" even in a toy.

              Sorry?

            • sesm 2 hours ago
              It wad easy to teach to Java devs just by using one mnemonic: PECS = Producer Extends, Consumer Super
        • lyu07282 4 hours ago
          > but programmers using the language don't necessarily use these terms.

          this always annoyed me about the python type annotations, you are supposed to already know what contravariant / covariant / invariant means, like: `typing.TypeVar(name, covariant=False, contravariant=False, infer_variance=False)`

          Its used in documentation and error messages too:

          > the SendType of Generator behaves contravariantly, not covariantly or invariantly.

          https://docs.python.org/3/library/typing.html#annotating-gen...

      • bobbylarrybobby 43 minutes ago
        Aside from better documentation (it would be quite nice if rustdoc automatically showed the computed variance for types where it mattered), what would writing it down in the type system get you?

        Separately, if everything were invariant, you wouldn't be able to use a `&'static T` where a `&'non_static T` was expected, which would be quite unpleasant!

      • tialaramex 4 hours ago
        https://doc.rust-lang.org/reference/subtyping.html#variance

        Lifetimes imply the need for the idea of variance.

        • kibwen 4 hours ago
          Rather, any language with both generics and subtyping needs to consider variance, and Rust doesn't have subtyping in general, but lifetimes do technically have subtyping relationships, so lifetimes are the only place where this matters, and fortunately Rust can just infer the right behavior based on some simple type-level heuristics.
  • blacklion 3 hours ago
    It is not clear, does compiler explicitly knows about properties of `NonNull` (for example, that it has neutral value to collapse `Option<NonNull<T>>`), so it is part of compiler, or it is expressed in type system, so it is part of standard library?

    Same question about other «signal» types.

    • dathinab 2 hours ago
      > part of compiler, or it is expressed in type system, so it is part of standard library

      compiler + standard library (core library to be more specific)

      `NonNull` is mostly "just" a library type in the core libary, but it uses two unstable/nightly attributes to tell the compiler about it's special properties (see other answers, also to be clear its not a `lang_item` attribute but something other types can use, too).

      The special thing about core/std in rust is that due to it being part of the specific compiler version it can use (some, careful considered) subset of unstable features as long as they work for that specific use case and don't "leak"/accidentally stabilize through it.

      But this attributes are in progress of getting replaces with a new mechanism of pattern annotations. I.e. NonNull will be annotated with attribute which contains a `1..` pattern (i.e. value is 1 or larger) but other patterns are getting support, too. (The second attribute to enforce/guarantee niche optimizations might still be needed for stability guarantees.)

      And I think??? there are plans to stabilize this new mechanism, but I'm absolutely not sure what the state of this is (RFC accepted?, stabilization open/likely/decided? implementation done/partial?). I'm currently not in the loop.

      So in the long future you probably can write your own types with other niches e.g. a u8 with `..127` (MSB unused).

    • zdimension 3 hours ago
      For anyone wondering, NonNull (along with other "constrained" types like NonZero integers) uses internal compiler attributes (https://doc.rust-lang.org/src/core/ptr/non_null.rs.html#72):

        #[rustc_layout_scalar_valid_range_start(1)]
      
      This gives rustc enough information to perform niche optimizations such as collapsing Option<NonNull<T>>.

      You can technically use those for your own types, but it requires nightly, obviously.

    • conradludgate 3 hours ago
      It's a mix of both. https://doc.rust-lang.org/src/core/ptr/non_null.rs.html#72-7...

        #[rustc_layout_scalar_valid_range_start(1)]
        #[rustc_nonnull_optimization_guaranteed]
      
      This declares that NonNull cannot be outside of the range (1..). These magic attributes are only allowed to be used by the stdlib, and they're direct compiler instructions. Nonnull is special because the optimisation is guaranteed so the compiler does have special code to handle it, but other types can be optimised without compiler changes.
  • germandiago 3 hours ago
    Can anyone compare it to C++ in terms of memory layout for typical implementations?
    • swiftcoder 2 hours ago
      It should be more-or-less the same on typical implementations. You might have 3 pointers instead of 1 pointer and 2 lengths, or some mix thereof, but they are all equivalent in memory layout.
    • username223 2 hours ago
      The vector implementation on my machine uses three pointers: start, end, and end-of-storage. It's basically the same, but uses addresses rather than counts. The valarray implementation uses a pointer and a single count.
    • sesm 2 hours ago
      That's one of major problems I have with Rust docs and community. The best way to explain most things in Rust to experienced dev, is by comparison to C++. But all the docs and blog posts explicitly target newbies and avoid comparison with C++ at all cost.
      • zozbot234 2 hours ago
        I disagree, Rust and C++ are very different languages with significant impedance mismatches once you go beyond the common C-like subset. Referencing C++ as a matter of course in docs and blog posts would just cause confusion. If you want a modern language that really is a lot closer to C++ you may want to check out Carbon.
        • sesm 2 hours ago
          Rust was created as a C++ replacement, borrows the 'zero cost abstractions' motto from C++, relies on RAII for resource management like C++ (not many languages do it), has the same approach to concurrency (in-place mutation guarded by locks), uses the codegen backend that was created for C++. It's mostly a C++ subset with more guardrails.

          Edit: RAII

          • zozbot234 1 hour ago
            It's nowhere near a C++ subset. The way it works under the hood is quite different, and even trying to add some of the underlying mechanisms to C++ (such as what they call "trivially relocatable types", or "destructing moves") has been quite difficult.
  • rossant 5 hours ago
    Tangent but nice web design.
    • herywort 5 hours ago
      First thing I did was toggle reader view, it's noticeably more responsive scrolling there.
  • gblargg 4 hours ago
    https://archive.is/Cqr4E (if their site blocks your browser)
  • assbuttbuttass 4 hours ago
    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

    • ameliaquining 1 hour ago
      "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.
    • ozgrakkurt 3 hours ago
      This is just how rust and c++ code is in my experience.
      • germandiago 3 hours ago
        C++ is bc of compatibility with god knows how many standards, but at the end it is simpler than it looks at first sight. The uglification of names in the std lib make it disgusting to read. If only that was removed it would improve a lot.
  • James_K 4 hours ago
    While safe Rust may be relatively simple to write, and certainly easier to write than safe C, this article has someone added to my belief that unsafe Rust is far too difficult to write. Perhaps some of this is deliberate, as a kind of defence mechanism against people using it willy-nilly, it still seems over-designed.
    • dathinab 2 hours ago
      It's a pretty normal experience for lookin at standard libraries (I mean look at cpython, where you now need to know C++, cpython interpreter internal,C++ std in addition to all the complicated meta class magic you normally don't need to care about.)

      Like in Vec we have following things most normal rust code wouldn't do:

      - Unique<u8> instead of Unique<T>, to micro optimize code size

      - RawVec, RawVecInner, due to the previous point, internal structure of libstd, relations to libcore/liballoc usage and more clean separation between unsafe and safe code etc. Which a "just my own vec" impl might not have

      - internal `Unique` helper type to reuse code between various data structures in std

      - the `NonNull` part is another micro optimization which might save 1-4 byte for `Option<Vec<T>>` (up to 4 due to alignment, but I think this optimization isn't guaranteed in this context)

      Like it could be just a `Vec<T> { ptr: *const T, cap: usize, len: usize, _marker: PhantomData<T> }`, but that wouldn't have quite the maximized (and micro optimized) fine tuning which tends to be in std libraries but not in random other code. (I mean e.g. the code size micro-opt. is irrelevant for most rust code, but std goes into _all_ (std using) rust code, so it's needed.

      But anyway, yes, unsafe rust can be hard. Especially if you go beyond "straight forward C bindings" and write clever micro optimized data structures. It's also normally not needed.

      But also "straight forward C bindings" can be very okay from complexity _as long as you don't try to be clever_ (which in part isn't even rust specific, it tends to just become visible in rust).

    • hmry 4 hours ago
      I think most of the complexity here comes from trying to write as little unsafe code as possible (and not repeating any unsafe operation in multiple places, hence the many layered abstractions).

      If you were to implement Vec without layering, it would be no more complicated than writing a dynamic array in C (but more verbose due to all the unsafe blocks and unabbreviated names)

      • kbolino 3 hours ago
        Notably, some of the abstraction is actually there to prevent the compiler from generating a lot of redundant code. The process of monomorphization (turning polymorphic generic code into usable machine code for particular types) can seriously bloat the size of binaries.
    • SonOfLilit 4 hours ago
      When I was young I peeked into the C++ stdlib, which is probably the best thing to compare this to. It was orders of magnitude worse.
      • tonyedgecombe 3 hours ago
        stdlib code was always impenetrable to me, this was one of the reasons I moved away from C++.
    • tialaramex 4 hours ago
      Unsafe Rust does have tooling to help you not make some kinds of mistakes, but by its nature you have to be able to press on after the tools can't help you.

      For example MIRI can understand pointer twiddling under strict provenance. It'll synthesize provenance for the not-really-pointers it is working with and check what you wrote works when executed. But if your algorithm can't work with strict provenance (and thus wouldn't work for CHERI hardware for example) but your target is maybe an x86-64 Windows PC so you don't care, you can write exposed or even just arbitrary "You Gotta Believe Me" pointer twiddling, MIRI just can't check it any more when you do that, so, hope you never make mistakes.

    • ozgrakkurt 3 hours ago
      Yes, it is indeed very difficult to write unsafe rust, also very difficult to write anything low level and reusable
  • constantcrying 4 hours ago
    I gave up half way through. The constant description of deep emotional feelings when it comes to something as sober as data types in the rust standard library make this rather hard to read. It is difficult to untangle the point the author is trying to make from the constant bombardment with deep emotional writing.
    • swiftcoder 2 hours ago
      If a little lighthearted banter counts as "deeply emotional", I guess I understand why your username is "constant crying"
      • constantcrying 1 hour ago
        Even if you do not expect a professional tone it is totally bizarre to write a technical article (which I would have liked to understand, since the question is interesting to me) as if you are talking to a toddler. The allusions to Grand conspiracy theories and grave emotional turns which are everywhere make the article sound as if it was written for children.

        I do not like lighthearted banter, but this is not the problem. The problem is that the author has a hard time writing a single paragraph without it. They should seriously consider doing some training in technical or scientific writing.

        >, I guess I understand why your username is "constant crying"

        What a novel comment. But yes, I do hate the constant positivity, especially in the face of a subpar product.

  • Vipsy 5 hours ago
    I love how it shows that you can have both performance and safety without jumping through hoops. The little details of memory management really shape how we end up writing code, often without even noticing. Rust makes it possible to get close to the hardware while still keeping you out of the usual trouble spots. It’s one of those things you appreciate more the longer you use it
    • johnisgood 4 hours ago
      Erm what? The article contradicts this. I'd push back on "without jumping through hoops". The article itself demonstrates 6 layers of abstraction (Vec<T> -> RawVec<T> -> RawVecInner -> Unique<u8> -> NonNull<u8> -> *const u8) built on unsafe primitives. The standard library does the hoop-jumping for you, which is valuable, but the hoops exist, they're just relocated.

      I bet Vec's implementation is full of unsafe blocks and careful invariant management. Users face different hoops: lifetime wrangling, fighting the borrow checker on valid patterns, Pin semantics, etc. Rust trades runtime overhead for upfront costs: compile-time complexity and developer time wrestling with the borrow checker which is often the right trade, but it's not hoop-free.

      The "close to hardware" claim needs qualification too. You're close to hardware through abstractions that hide significant complexity. Ada/SPARK gives formal correctness guarantees but requires proof effort and runtime checks (unless you use SPARK which is a subset of Ada). C gives you actual hardware access but manual memory management. Each has trade-offs - Rust's aren't magically absent.

      • zozbot234 3 hours ago
        It's fine https://github.com/model-checking/verify-rust-std/issues/283 https://github.com/model-checking/verify-rust-std/pull/481 (But until two weeks ago, it wasn't - you could in theory cause UB by writing purely safe code within the Vec<T> implementation.)
        • johnisgood 3 hours ago
          I had no idea, that is wild, especially considering how everyone has been outcrying "Rust is safe". Thanks for the info though.

          One should wonder what else is there... but they do not wonder, they are sadly rigid with their thinking that Rust is the perfect memory safe language with zero runtime overhead.

          • zozbot234 3 hours ago
            It's not real unsoundness because it only applies within the private implementation details of Vec<T> - you still can't cause UB from outside code. But it's a real oversight that only proves how hard writing unsafe code is.
          • Klonoar 3 hours ago
            You know you can discuss languages without the weird emotional labeling from your last paragraph, right?

            Plenty of people wonder. It’s part of building the language.

      • ozy 3 hours ago
        Most of those are abstractions, but not a runtime overhead. NonNull even enables an optimization not available to most other languages.

        And you can wonder, is this accidental complexity? Or is this necessary complexity?

    • alickz 4 hours ago
      I come from a Swift/Kotlin background and I've been learning Rust for fun in my spare time

      From what I heard online I was expecting it to be a lot harder to understand!

      The moving/borrowing/stack/heap stuff isn't simple by any means, and I'm sure as I go it will get even harder, but it's just not as daunting as I'd expected

      It helps that I love compiler errors and Rust is full of them :D Every error the compiler catches is an error my QA/users don't

      • Tade0 4 hours ago
        The language and associated tooling keep improving.

        Over the course of the last decade I've made several attempts to learn Rust, but was always frustrated with having code that reasonably should compile, but didn't and I had to run the build to even find that out.

        Now we have rust-analyzer and non-lexical lifetimes, both tremendously improving the overall experience.

        I still don't enjoy the fact that borrows are at the struct level, so you can't just borrow one field (there's even a discussion on that somewhere in Rust's) repo, but I can work around that.

        To drive the point home: I'm a frontend developer. This is a very different environment compared to what I'm used to, yet I can be productive in it, even if at a slower pace in comparison.

        • germandiago 3 hours ago
          Rust is not the most productive language by any means... it is hardened AF if you avoid unsafe, but productive?

          I can code much faster in almost any language compared to Rust. It creates mental overhead. For example, to compare it to siblings, Swift and C++ (yes, even C++, but with a bit of knowledge of good practices) lets you produce stuff more easily than Rust.

          It is just that Rust, compared to C++, comes extra-hardened. But now go get refactor your Rust code if you started to add lifetimes around... things get coupled quickly. It is particularly good at hardening and particularly bad at prototyping.

          They seem to be the opposite in the absence of borrowing without a garbage collector, since you need to mark the lifetime in some way.

          • zozbot234 2 hours ago
            Rust is not that bad at prototyping, you need to use the right featureset when writing quick prototype code. Don't use lifetimes, use clone() and Rc/Arc freely to get around borrow-checker issues. Use .unwrap() or .expect("etc.") whenever you're not sure how to pass an error back to the caller. Experiment with the "Any" trait for dynamic typing and downcasts, etc. The final code will still be very high-performance for prototype code, and you can use the added boilerplate to guide a refactoring into a "proper" implementation once the design stabilizes.
      • LoganDark 4 hours ago
        > It helps that I love compiler errors and Rust is full of them :D Every error the compiler catches is an error my QA/users don't

        amen! I despise Python even though I used to love it, and that's because it's full of runtime errors and unchecked exceptions. The very instant I learned more strongly typed languages, I decided never to go back.

        • zelphirkalt 4 hours ago
          And then comes the point, where you get a scenario, which is actually, you'd think, at least, something simple, but then gets really tough to express in strongly, statically typed languages, and you might take a step back and consider for a moment, how simple it would be to express this in a language like Python, while still being memory safe.

          Statically typing things is great, and I enjoy it too, when it is practical, but I don't enjoy it, when it becomes more complicated than the actual thing I want to express, due to how the type system of that particular language or third party tool (in Python) works.

          • LoganDark 1 hour ago
            I find it fun to statically prove correctness of difficult problems. To each their own, though, of course.
        • germandiago 3 hours ago
          Python properly paired with typing is very competent IMHO.
          • mring33621 2 hours ago
            I would like to learn how to do this well!