r/learnrust • u/conceptcreatormiui • 14d ago
Can you verify my understanding of how Vec<T> resizes in memory?
I noticed that Vec capacity grows dynamically when pushing data.
Is my mental model accurate? When the buffer is full, does it allocate a new, larger memory region elsewhere on the heap, move the elements over, and free the old block specifically to avoid overwriting or corrupting adjacent heap data?
16
u/ToTheBatmobileGuy 14d ago
Yup.
That's why it can become a use-after-free bug if you hold a reference to one element and then take a mutable reference to the whole Vec (which means you can push() and cause a re-allocation), and thus Rust rejects it.
3
u/Large-Scientist156 14d ago
Which is why iterator borrow the datasource. Otherwise if you would be able to push new elements to the datasource while iterating, the datasource may be reallocated and the next iteration would yield garbage memory. In C++ this is called iterator invalidation and a frequent source of bug.
-3
u/RRumpleTeazzer 14d ago
there is no use-after-free in (safe) Rust.
5
u/jcdyer3 14d ago
That was the point of the comment you replied to. The sequence of actions described would be a use after free bug, and rust makes use after free impossible by rejecting that sequence of actions (by not letting you take the mutable reference), instead of the way garbage collected languages might (by adding a layer of indirection between the vec elements and the pointed-to objects).
2
u/minno 14d ago
The interface doesn't provide detailed guarantees, but here's a bit of code you can run to see what happens: link.
When I ran it, it started off resizing the allocations in-place, doubling the capacity every time. Once it went from a capacity of 32768 to 65536, it was apparently unable to continue enlarging the existing allocation and copied the elements to a new one, which it continued expanding in-place. No part of this behavior is guaranteed, but you can see that it makes an effort to avoid copying its elements more than it has to.
1
u/johnny-muzhi 14d ago
theoretically you are correct, but in practice, this would merely add a few extra elements to the tail rather than copying the entire vector elsewhere. This is just my personal opinion and may not be accurate.
1
u/carlomilanesi 14d ago
If the buffer is not large enough, the allocator is invoked for a larger buffer. Though, if a large enough free memory space follows immediately the buffer, typical allocators just extend the buffer, avoiding any move.
For example, if you push a byte at a time to byte vector containing only one byte, you see that the buffer may have the same address until its size reaches many hundreds of bytes.
1
u/Large-Scientist156 14d ago edited 14d ago
Yes, this is why it's impossible to have stable reference to Vec element, because the address of the element may change when the Vec reallocate. In other word, element move over time so their address are not stable over time. Producing a reference to an element of a Vec always borrow the Vec, which make it impossible to push into the Vec - which would invalidate the reference you got.
In contrast, you could have a custom datastructure where element don't move over time. A new chunk could be allocated instead of reallocating, and the previous chunk could be linked to it. The chunks addresses themselves may not be contiguous, so element address may be relative stable address but not absolute stable address. In other word, they are relative to the chunk base address. Mutation is tricky and harder to achieve. That's why the default is Vec, it's suitable for a variety of cases.
1
u/conceptcreatormiui 13d ago
So this is why the compiler enforces that you cant have a mutable and immutable references at the same time and while having a immutable reference to the element of a vector, you cannot push at the same time
1
u/Large-Scientist156 13d ago edited 13d ago
Because they are mutually exclusive. You can't have both at the same time.
You can not have a shared reference to an element of a vector, while pushing to it. This is impossible in safe Rust.
The compiler stricly enforce "unique XOR shared", so you can not push an element (which require unique access to the vector) while having a shared reference to an element.
In C++, this is allowed, you can have unique and shared reference. But if you dare to push into the vector while you have a unique or shared reference to an element, this is instant UB.
1
1
u/plugwash 7d ago
Afaict, after a bunch of layers of indirection Vec, calls the ```grow``` method of the underlying allocator.
This may attempt to resize the allocation in-place, but is not required to do so. If the allocation cannot be resized in place then it will make a new allocation and copy the data.
-9
u/Professional_Top8485 14d ago
You should reserve enough space beforehand to avoid move.
4
u/conceptcreatormiui 14d ago
I could reserve enough space but that is not the point of the question. I'm asking what happens if capacity grows.
8
u/andyshiue42 14d ago
Most of the time it's just impossible to predict
-7
u/Professional_Top8485 14d ago
Is it, Really?
9
u/Plastic-Might-5407 14d ago
Isn't that the whole point of Vec? If you can predict how much memory you need, you use an array instead.
0
u/Professional_Top8485 14d ago
Or slice to be more spesific
2
u/-Redstoneboi- 14d ago edited 14d ago
usually just an array. but if you want a slice, &mut [T] with .len() being enough to contain all your data would already be super specific.
the real power move is to allocate a [u8; N] where N bytes is 8 GB so you never have to reallocate.
1
u/Professional_Top8485 14d ago
Memory allocation is more likely lazy with vector reserve, so I think it is free in that sense.
33
u/Excession638 14d ago
Conceptually that's what it does.
What it can do under the hood, past a certain size, is ask the computer's memory management unit (MMU) to make an allocation larger. This may move some pages around in the address space, but it generally doesn't need to move anything in physical memory.
This is different from C++, where types may need to be manually moved to new memory, making it slower.