How Rust free memory when functurn returned

fn tf (){
    let n = 1000000;
    let mut vec: Vec<String> = Vec::with_capacity(n);
    let content = "hello world".to_string();
    for i in 0..n{
         vec.push(content.clone());
    }
}

fn main() {
    tf();
    println!("function finished");
    thread::sleep(time::Duration::new(30, 0));
}

run this code , after function tf was finished , I check the memory the process is using about 32.1MB .
But when I change to n = 100000 in function tf , after tf function finished the memory in using is 3.3 MB

Why the memory in using when tf function finished is different.
Ps: I test On ubuntu 22.04 , Rust 1.64.0 (387270bc7 2022-09-16) check memory with Ubuntu System Monitor

What was your expectation?

I expect the memory usage has nothing to do with the size of n . because vec in function is a local variable

Are you saying you expected the optimizer to remove the entirety of tf because it has no side effects?

Memory freed by executable is generally not reclaimed by the system immediately, because it often is reused by the consequent allocations.

6 Likes
fn tf(){
    let n = 1000000;
    let mut vec: Vec<String> = Vec::with_capacity(n);
    let content = "hello world".to_string();
    for i in 0..n{
         vec.push(content.clone());
    }
}

fn main() {
    tf();
    tf();
    println!("function finished");
    thread::sleep(time::Duration::new(30, 0));
}

after call twice , the memory in using is 311kB , Maybe your are right there is something about system not Rust. But I can't understand what the detail is .

The detail is, the program generally uses less memory then the system is providing, and in the System Monitor you see the latter, not the former.

2 Likes

Note that while your vec is a local variable in the stack the content of the vec is allocated on the heap. The vec contains a pointer to that content on the heap, along with a length and capacity.

Similarly all those Strings have their actual content allocated on the heap.

1 Like

The missing piece is that when you allocate/deallocate memory you don't directly talk to the OS, but to the allocator. The allocator is not required to immediately release memory to the OS when freed, in particular it's quite common to keep additional memory to serve future allocations without requesting again memory from the OS. This happens because requesting memory is quite slow, while caching allocations in the allocator and returning them on demand is faster.

4 Likes

I was trying to figure out why calling tf twice results in less memory reserved than calling it once. I'm guessing that after the second time the allocator assumes that it has enough information to be more confident how much memory is needed and releases the rest to the OS.

The Rust library documentation is confusing:

The default memory allocator provided by the operating system.

In Linux, the "memory allocator provided by the operating system" would be brk or mmap. But then it says:

This is based on malloc on Unix platforms

But malloc isn't a system call provided by the OS, it's a function in the C standard library.

The Unix OS is written in C. Traditionally pretty much all the users space programs required for it's operation were written in C. Therefore Unix comes with a C compiler and the standard C library. Unix and C are a glorious whole. So we can say that the standard C library is part of the operating system. And hence malloc.

Those systems calls you refer to are provided by the OS kernel. That is only a part of a working operating system.

I understand, but this is in the context of the standard library of a different programming language, so directed at programmers not writing their programs in C. So it seems confusing to assume the C standard library as the basis without even mentioning C.

Ah yes. I guess that Rust, like many other languages rests on a lot of existing C infrastructure. The LLVM compiler and the Standard Library. And the operating system kernels.

We could imagine some future, over the rainbow, where we have an OS written in Rust, a Standard Library written in Rust and LLVM replaced with a Rust compiler.

All of that is an immense amount of work that will take a long time to create and gain adoption. If it ever would. If Rust were waiting on all those things we would not be using it today or any time soon.

What language the kernel or the compiler is written in is irrelevant for the Rust programmer using std because their code doesn't link to the kernel or to the compiler.

Building a memory allocator is much, much easier than building a kernel or a compiler. A lot of languages have their own memory allocators.

I'm not saying that Rust shouldn't be using the C malloc, just that the way it's described by the library reference was confusing to me.

malloc is provided by the OS, but not by the kernel. Libc is part of the OS. The OS is the combination of a kernel with libraries and programs you can rely on existing. While on Linux the interface between the kernel and libc is stable, this is not the case for other OSes. As such both the kernel and libc need to be updated in lock step during OS updates.

1 Like

C is not a programming language.

Modern OSes offer the only suppoerted way to access their facilities: via C dynamic library (NTDLL on Windows, libc on most POSIX platforms). Linux is the only exception.

Go tries to avoid C and thus fights constantly with macOS/Windows upgrades. And uses libc on some platforms anyway.

Rust doesn't need or want to control everything that happens in your process thus it's perfectly happy to build things on top of libc (like OS developers prescribe).

It can be used in standalone mode, but that's not a default.

You get this difference because allocator has a cache for memory blocks of various small-ish sizes, so it doesn't free them, but puts them in the cache instead. Very large allocations are usually not cached, and pages backing them can be released sooner.

Generally you can't measure memory usage accurately from outside of the executable. Use a custom Rust allocator to get byte-accurate numbers:

Wait a second, you did 10x less work and got 10x less memory usage. What's the issue here?

I don't think anyone else has pointed this out yet.

1 Like

The question seems to be "why does the size of Vec matter, if, when the memory usage is checked, it is already freed?"

3 Likes
use std::alloc::{System, GlobalAlloc, Layout};
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
use std::time;
use std::thread;

struct Counter;

static ALLOCATED: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for Counter {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let ret = System.alloc(layout);
        if !ret.is_null() {
            ALLOCATED.fetch_add(layout.size(), SeqCst);
        }
        ret
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        System.dealloc(ptr, layout);
        ALLOCATED.fetch_sub(layout.size(), SeqCst);
    }
}

#[global_allocator]
static A: Counter = Counter;


fn tf(){
    let n = 100000;
    let mut vec: Vec<String> = Vec::with_capacity(n);
    let content = "hello world".to_string();
    for _ in 0..n{
         vec.push(content.clone());
    }
}

fn main() {
    tf();
    println!("allocated bytes after function tf: {}", ALLOCATED.load(SeqCst));
    println!("function finished");
    thread::sleep(time::Duration::new(30, 0));
}

I found this method in std/src/alloc.rs to keeping track of the number of all bytes allocated, the allocated bytes is 53bytes . I Know more. But I can't explain all to others.