Live data from Hacker News

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

blog.polybdenum.com

101–110 of 129 posts

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

#101
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

Indeed: https://go.googlesource.com/go/+/master/src/runtime/slice.go...

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

#102

I don't think it's a memory leak. It's re-using the same space of the underling array. It's allocated. Dropping the Vec will release the memory. In v1 you put in 128 containers is with each 1024 boxes. Then v2 is taking out the first box out of each container, tossing the container and putting the box at the space where the container was, packing them. The fact that capacity doubles when you remove the as u8 is... no…

In the Haskell world there's a bit of a cheap differentiation for this kind of leak.

Memory leak: classical C style, fully lost in memgrind (runtime bug) Space leak: Some memory structure requires significantly more memory than it should. E.g. keeping a (lazy) ref to a large blob, around that keeps it in memory.

The effect is largely the same, but the latter is almost harder to spot, since the tooling cannot be as general as memgrind.

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

#103
This is a good example of the type of challenges you face as an author of widely used library. I can see a lot of scenarios where an optimization like this would bring benefits. But there are also many where it would hurt performance (not to mention memory usage), including most "collect once read many times" use-cases.

But I think the real thing for me is that this violates the principle of least surprise. If I wanted the type of memory reuse / lazy transformation behavior this optimization introduces, I would be looking at working with an iterator with a bunch of functinoal transforms. And if I'm calling .collect() it's because I want to convert the iterator into a data structure optimized for reads.

But I can also see how others would land on the other end, and hence the challenges for the library authors.

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

#104

Earlier quoted context omitted.

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 hav…

The question is how much overhead would adding a check to determine to shrink if > % of freedom to all code that doesn’t need this optimization entail?

The reason it’s important to consider is that I can always add a shrink_to_fit even if it’s a sharp edge. I can’t remove the conditional within the std library even if I know it doesn’t apply. And adding explicit APIs to control this nuance is a bit much (whether through a dedicated collect_maybe_shrink function or as a new argument to collect to control shrinkage) and there are usability implications to complicating the UI. It may be that ultimately this should be fixed purely through documentation even though defensive shrink_to_fit sucks. Not all technical problems can be solved and it’s all trade offs.

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

#107

Earlier quoted context omitted.

… 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.…

… that's really not the idea I'm trying to convey with the comment.

Clearly, if you feed a program a larger file that it is going to read into memory to process, it is then expected that it will consume more resources on account of it doing more work. But that is memory being expended on visible, useful work. All of the examples in the comment are referring to memory being "allocated" (in the sense of being assigned to the program) but not fulfilling any visibly useful function insofar as the operator/programmer can see: Java's GC being unable to effectively reclaim unused memory prior to killing a machine, the OP's example of a Vec allocating without (seemingly) have a purpose (…as it is excess of what is required to allow for amortized appends).

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

#108
post #102

I don't think it's a memory leak. It's re-using the same space of the underling array. It's allocated. Dropping the Vec will release the memory. In v1 you put in 128 containers is with each 1024 boxes. Then v2 is taking out the first box out of each container, tossing the container and putting the box at the space where the container was, packing them. The fact that capacity doubles when you remove the as u8 is... no…

In the Haskell world there's a bit of a cheap differentiation for this kind of leak. Memory leak: classical C style, fully lost in memgrind (runtime bug) Space leak: Some memory structure requires significantly more memory than it should. E.g. keeping a (lazy) ref to a large blob, around that keeps it in memory. The effect is largely the same, but the latter is almost harder to spot, since the tooling cannot be as ge…

Also the latter is the bane of lazy languages, which make space leaks both commonplace and extremely difficult to notice.

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

#109
post #86

Earlier quoted context omitted.

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,…

You can use slices.Grow for that purpose in Go. Exact sizing is only possible during slice creation.

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

#110
post #109

Earlier quoted context omitted.

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,…

You can use slices.Grow for that purpose in Go. Exact sizing is only possible during slice creation.

It's unclear which policy slices.Grow exhibits, and so maybe it's not guaranteed whether you get the equivalent of Vec::reserve or Vec::reserve_exact ? My guess since it's not mentioned and no alternative is supplied is that you get Vec::reserve_exact - the same pessimisation footgun.
Post reply on HN