How does rust allocate string when returned from a function?

Hello everyone

i have the following code (simplified to show my case) Rust Playground

use std::collections::HashMap;

fn my_data() -> HashMap<String, String> {
    let mut files = HashMap::new();
    
    files.insert("file_one".to_string(), "big_content_of_file_one".to_string());
    files.insert("file_two".to_string(), "big_content_of_file_two".to_string());
    
    files
}

fn main() {
    let data = my_data();
    
    println!("{:?}", data);
}

when i return the hashmap from my_data does rust copies all the data inside the hashmap so it can be available in the main function? or since strings are stored in the heap does rust only copies the references?

what is the possible performing code that i can write to read big files, and return them in a hashmap, assuming that inside my_data function i do read the files inside a string using read_to_string

It just copies the physical representation of the HashMap to the caller's stack. So your intuition is correct that since the strings are stored in the heap, rust only copies the references. The way to think about it is that moving a value is literally a memcpy.

Your approach seems fine from a performance perspective, given the constraints you've described.

2 Likes

I should be a little more precise on this statement. In fact, the String values aren't even referenced directly in the HashMap struct - there would be further indirection in the data structure to, for example, store the lists of keys and values.

2 Likes

If you want to know exactly how many bytes will be copied, try std::mem::size_of on your hashmap type. This is independent of the number of elements in the hashmap, as they are not stored on the stack.

use std::collections::HashMap;

fn main()
{
    let size = std::mem::size_of::< HashMap<String, String> >();
    
    println!( "Size of HashMap<String, String> is {} bytes", size );
}
3 Likes

It sounds like you are coming from a C++ background where returning items invoke some sort of copy constructor. This does not happen in Rust — copies that are not just a memcpy of the struct fields never happen implicitly.

3 Likes

In C++, you should actually get RVO for similar code. But yes, there are a lot of cases where you get surprising, implicit copies in C++.

2 Likes

Good point. Moving in rust is about ownership and not necessarily about copying stuff.

That makes me wonder in this case. The hashmap is returned from a function. So where on the stack in terms of both functions stack frames does that live or does it get copied? Rust doesn't seem to have a well defined calling convention if I understand.

From what I can see in godbolt, it seems rust just puts a pointer in eax for returning values rather than copying them, even for an i32, but my assembly is to rusty to draw a decent conclusion...

I'm a bit confused by the lines 5-6:

mov     dword ptr [rsp + 4], eax
mov     eax, dword ptr [rsp + 4]

Looks like a nop unless some dereferencing is happening here.

The HashMap type is a struct that points to some kind of list of buckets. That list of buckets lives on the heap and does not get copied - only the struct that points to it (maybe) gets copied

Playground - according to this, the HashMap struct type is 56 bytes, regardless of what type gets stored in the hashmap. Those 56 bytes are the only ones that (maybe) get copied when transferring the HashMap by value (parameter, return value, by-value assignment, etc.)

The definition of HashMap is: map.rs.html -- source
Where RawTable is defined at: mod.rs.html -- source

(the hashbrown crate is used by libstd to implement std::collections::HashMap)

As another little bit of the puzzle:

The low level ABI of Rust is undefined (implementation defined). Typically it'll be the system ABI, but that's not a given.

Along with that, RVO is never guaranteed (and unlike C++, is not observable), but is allowed. Depending on how exactly the function is optimized and what the calling convention is, the inline part of the HashMap may be moved through stack frames in a number of ways, be it in registers, placed directly in the caller's stack frame, out pointers, or any other convention.

However, it is guaranteed that moving a String (for example) only moves the (ptr, len, cap) triple, while the data of the string remains in the same allocation (on the heap).

Could the compiler do something like:

  1. leave the value on the stack frame of callee
  2. put address of that in rax
  3. pop the stack frame
  4. ret
  5. use the value pointed to by rax, as it knows it hasn't pushed anything on the stack, so the memory shouldn't be overwritten?

I'm just thinking, if you don't follow any calling convention, for code not being generated for a dylib, some "creative" optimizations might be possible.

so would it spread 56 bytes in like 7 x64 registers to pass that through? Just curious, since I haven't looked under the hood in a long time.

Could the [callee return a value in its own stack space]?

It allowed to, given complete control of calling convention*. Does it? ¯\_(ツ)_/¯

* including what interrupts trample. (With unknown interrupts in the same stack space, this probably requires the interrupt to respect the "red zone". Typically, though, an interrupt runs on a different stack than user (non OS) code.)

so would it spread 56 bytes in like 7 x64 registers

Probably not; most calling conventions have admissions for the first two-ish pointer-like arguments to be passed/returned in registers, but after that they tend to spill to the stack. (The reason being that one of the values would've needed to be put on the stack anyway.) But it's allowed to (for extern "Rust").

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.