Live data from Hacker News

Identifying Rust's collect: >() memory leak footgun

blog.polybdenum.com

91–100 of 129 posts

Re: Identifying Rust's collect:<Vec<_>>() memory leak footgun

#91
post #87
post #43

This is a pretty surprising behavior. Reusing the allocation without shrinking when the resulting capacity will be within 1.0-2.0x the length: seems reasonable, not super surprising. Reusing the allocation without shrinking when the resulting capacity will be a large multiple of the (known) length: pretty surprising! My intuition is that this is too surprising (at capacity >2) to be worth the possible optimization bu…

Is reusing an allocation while changing its size a thing you expect to be able to do? I would believe that some languages/systems/etc can do that, but it certainly feels like an exception rather than a rule. Reuse generally means the whole block of memory is retained, from what I've seen, because you'd have to track that half-freed memory for reuse somehow and that has some associated cost. (A compacting-GC language…

> Is reusing an allocation while changing its size a thing you expect to be able to do?

It's something you should expect to be able to try to do. The underlying allocator may reject the request depending on context (maybe it is works for large sizes only, for example). This is provided by Rust's Allocator trait realloc_in_place() API, which returns CannotReallocInPlace if it isn't possible.

For Vec::collect, in the event that the storage cannot be reused smaller in place, I think it would be reasonable to free it and allocate an appropriately sized buffer instead.

Re: Identifying Rust's collect:<Vec<_>>() memory leak footgun

#92

Earlier quoted context omitted.

Clickbait (in the context of Rust). In languages with managed memory there are no true memory leaks so such wastes are called leaks. In lower-level languages, we should stay more strict with what we call things.

… Box::leak¹ is a function that exists. That seems like a memory leak, no? Less tongue-in-cheek, if a program allocates far more memory than expected of it, I going to colloquially called that a "memory leak". If I see a Java program whose RSS is doing nothing but "up and to the right" until the VM runs out of memory and dies a sweet sweet page thrashing death, I'm going to describe that as a "memory leak". Having so…

> "If I see a Java program whose RSS is doing nothing but "up and to the right" until the VM runs out of memory and dies a sweet sweet page thrashing death, I'm going to describe that as a "memory leak"."

By this definition, if a program reads in a file and you point it to a small file then the program does not have a memory leak, but if you point it to a large enough file, then the program does have a memory leak. Whether or not a program has a memory leak doesn't depend on the code of the program, but how you use it. But then on a bigger computer, the program doesn't have a memory leak anymore.

That seems a less useful definition than the parent poster's / the common definition.

Re: Identifying Rust's collect:<Vec<_>>() memory leak footgun

#93

Earlier quoted context omitted.

I think the behavior is that if you have a Vec of u128 (say 1000), filter that to fewer elements (say 10), and then collect it into a Vec of u32 you might expect the resulting value to be around 40 bytes but in beta Rust it is 16000 bytes. In current Rust the collect would cause a new 10 element Vec of u32 to be allocated, in beta it reuses the original larger allocation. The author's code is doing a bit more but ess…

Ok. I missed that there’s a filter step that compounds the problem. The more I read the less this sounds like a bug and more like application code is missing a shrink_to_fit and was relying on a pessimization. That being said, it’s also not an unreasonable expectation on the user’s behalf that the size and capacity don’t get crazy different in code as innocuous and idiomatic as this. I wonder how the open bug will en…

Someone here linked an open ticket for this issue. In the comments at least one person made basically the same argument that holding on to a potentially large % of memory is a surprising sharp edge, meanwhile shrinking the Vec and perhaps allocating is unsurprising behavior. Requiring many additional defensive shrink_to_fit calls to avoid this problem seems like the wrong tradeoff but I don't write enough Rust to have a strong opinion.

Re: Identifying Rust's collect:<Vec<_>>() memory leak footgun

#94

Earlier quoted context omitted.

Then using a language without a published standard is a fools errand.

No? How does that even follow? That a standard should codify common expectations insofar possible (without breaking rigor) is irrelevant to whether you "should" only use standardized languages or not.

It's a statement of opinion. I think attempting to use a language without a standard causes problems that can be avoided by using one with a standard. As evidenced in this exact post, the author spent a fair amount of time trying to figure out if the nightly, devel or production versions of the complier were implicated.

He wasn't even able to fully test a theory because he relied on a feature that wasn't even available in older compilers in a completely unrelated system.

So, if you want to ignore these costs, that's fine, I'm not prescribing your actions for you, just sharing an opinion. Do you follow?

Re: Identifying Rust's collect:<Vec<_>>() memory leak footgun

#95
post #86

Earlier quoted context omitted.

C# doubles the size of a List each time you hit the capacity (if you didn't set it correctly when you created the list or left it at the default). Does Go do something similar or does it only increase it by 1 each time?

It grows by 2x for small sizes, then it transitions to growing it by 1.25x

In many of these languages (and C++) you can inadvertently pessimise by trying to provide useful size hints, Rust has a smarter API here.

Here's how that goes, suppose I receives batches of 10 Doodads. My growable array type has an API to reserve more space for Doodads, so before pushing each onto my growable array I reserve enough space for 10 extra. At small sizes this helps. Instead of 1, 2, 4, 8, 16, 32 Doodads, for two batches we grow to 10 and then 20 Doodads, we're only allocating twice and copying 10 items, instead of allocating 6 times and copying 31 items. But at bigger sizes it hurts instead, we're doing far more allocations and exponentially more copying.

Rust provides two distinct functions, Vec::reserve and Vec::reserve_exact. With reserve we get to avoid the pessimisation, it will grow faster than the exponential amortization if asked but never slower - while reserve_exact still allows us to give a specific final size in cases where we actually know that before growing too far.

Re: Identifying Rust's collect:<Vec<_>>() memory leak footgun

#97

Earlier quoted context omitted.

No? How does that even follow? That a standard should codify common expectations insofar possible (without breaking rigor) is irrelevant to whether you "should" only use standardized languages or not.

It's a statement of opinion. I think attempting to use a language without a standard causes problems that can be avoided by using one with a standard. As evidenced in this exact post, the author spent a fair amount of time trying to figure out if the nightly, devel or production versions of the complier were implicated. He wasn't even able to fully test a theory because he relied on a feature that wasn't even availab…

[deleted]

Re: Identifying Rust's collect:<Vec<_>>() memory leak footgun

#98

Earlier quoted context omitted.

No, if that's all it was, the excess memory usage would be 2x. But it's 200x. Right?

Looking at this example: fn dbg_vec (v: &Vec ) { println!( "vec data ptr={:?} len={} cap={}", v.as_ptr(), v.len(), v.capacity() ); } fn main() { { let v1 = (0u16..128).map(|i| [i; 1024]).collect:: >(); dbg_vec(&v1); let v2 = v1.into_iter().map(|x| x[0] as u8).collect:: >(); dbg_vec(&v2); } { let v1 = (0u16..128).map(|i| [i; 1024]).collect:: >(); dbg_vec(&v1); let v2 = v1.into_iter().map(|x| x[0]).collect:: >(); dbg_v…

Thank you for explaining it this way. I’ve been seeing this bit of news off and on all day and knew there was something I wasn’t understanding. This was the key bit

Re: Identifying Rust's collect:<Vec<_>>() memory leak footgun

#99

Earlier quoted context omitted.

Clickbait (in the context of Rust). In languages with managed memory there are no true memory leaks so such wastes are called leaks. In lower-level languages, we should stay more strict with what we call things.

… Box::leak¹ is a function that exists. That seems like a memory leak, no? Less tongue-in-cheek, if a program allocates far more memory than expected of it, I going to colloquially called that a "memory leak". If I see a Java program whose RSS is doing nothing but "up and to the right" until the VM runs out of memory and dies a sweet sweet page thrashing death, I'm going to describe that as a "memory leak". Having so…

>I don't care? You're just forcing me to wordsmith the problem description

Yes, because if you don't define the problem clearly, the problem won't be solved. Java being inefficient with memory use doesn't mean any memory was leaked.

Memory leaks can be tricky to track down, and if I spent 6 hours looking for a memory leak only to come back and found out you meant it uses more memory than what's efficient I'd be pissed I wasted 6 hours because you wanted to save 5 minutes.

Re: Identifying Rust's collect:<Vec<_>>() memory leak footgun

#100
post #43

This is a pretty surprising behavior. Reusing the allocation without shrinking when the resulting capacity will be within 1.0-2.0x the length: seems reasonable, not super surprising. Reusing the allocation without shrinking when the resulting capacity will be a large multiple of the (known) length: pretty surprising! My intuition is that this is too surprising (at capacity >2) to be worth the possible optimization bu…

> This is a pretty surprising behavior.

This optimisation crosses the line into "too clever by half". Once a Rust programmers groks how Vec's work their mental model of how capacity is allocated simple, so simple they hardly need thing about it's implications as they write code. This optimisation breaks that simple model badly, and you would have to be really focused to realise it's about to bite you as the code streams off your finger tips. Consequently this "optimisation" is almost certainly going to cause a lot of code to using far more memory than expected.

You get the same results with filter():

    let mut v = vec![1,2,3];
    v.push(4);
    println!("len {} cap {}", v.len(), v.capacity());
    let v = v.into_iter().map(|x| x+1).filter(|_x| false).collect::>();
    println!("len {} cap {}", v.len(), v.capacity());

    len 4 cap 6
    len 0 cap 6
Despite being in some sense "correct", I'd consider that result a bug. One fix would be to examine the result, and if it breaks the programmers expectations badly (by say using more than twice the memory needed) it does a shink_to_fit() for you. Since the programmer is probably expecting a new vector to be generated it isn't a surprising outcome.
Post reply on HN