7

Measuring Memory Usage in Rust

 3 years ago
source link: https://rust-analyzer.github.io/blog/2020/12/04/measuring-memory-usage-in-rust.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Measuring Memory Usage in Rust

@matklad, Dec 4, 2020

rust-analyzer is a new "IDE backend" for the Rust programming language. Support rust-analyzer on Open Collective or GitHub Sponsors.

This post documents a couple of fun tricks we use in rust-analyzer for measuring memory consumption.

In general, there are two broad approaches to profiling the memory usage of a program.

The first approach is based on “heap parsing”. At a particular point in time, the profiler looks at all the memory currently occupied by the program (the heap). In its raw form, the memory is just a bag of bytes, Vec<u8>. However the profiler, using some help from the language’s runtime, is able to re-interpret these bytes as collections of object (“parse the heap”). It then traverses the graph of objects and computes how many instances of each object are there and how much memory they occupy. The profiler also tracks the ownership relations, to ferret out facts like “90% of strings in this program are owned by the Config struct”. This is the approach I am familiar with from the JVM ecosystem. Java’s garbage collector needs to understand the heap to search for unreachable objects, and the same information is used to analyze heap snapshots.

The second approach is based on instrumenting the calls to allocation and deallocation routines. The profiler captures backtraces when the program calls malloc and free and constructs a flamegraph displaying “hot” functions which allocate a lot. This is how, for example, heaptrack works (see also alloc geiger).

The two approaches are complementary. If the problem is that the application does too many short-lived allocations (instead of re-using the buffers), it would be invisible for the first approach, but very clear in the second one. If the problem is that, in a steady state, the application uses too much memory, the first approach would work better for pointing out which data structures need most attention.

In rust-analyzer, we are generally interested in keeping the overall memory usage small, and can make better use of heap parsing approach. Specifically, most of the rust-analyzer’s data is stored in the incremental computation tables, and we want to know which table is the heaviest.

Unfortunately, Rust does not use garbage collection, so just parsing the heap bytes at runtime is impossible. The best available alternative is instrumenting data structures for the purposes of measuring memory size. That is, writing a proc-macro which adds fn total_size(&self) → usize method to annotated types, and calling that manually from the root of the data. There is Servo’s malloc_size_of crate for doing that, but it is not published to crates.io.

Another alternative is running the program under valgrind to gain runtime introspectability. Massif and and DHAT work that way. Running with valgrind is pretty slow, and still doesn’t give Java-level fidelity.

Instead, rust-analyzer mainly relies on a much simpler approach for figuring out which things are heavy. This is the first trick of this article:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK