Drop flags in Rust 1.12.0


#1

Hi, I have got a couple of questions about MIR and the drop flags in Rust 1.12.0.

I read this in the 1.12.0 release announcement on the blog.

MIR exposes perfect information about the program’s control flow, so the compiler knows exactly whether types are moved or not. This means that it knows statically whether or not the value’s destructor needs to be run. In cases where a value may or may not be moved at the end of a scope, the compiler now simply uses a single bitflag on the stack, which is in turn easier for optimization passes in LLVM to reason about. The end result is less work for the compiler and less bloat at runtime.

I believe this is talking about the drop flag. Here is relevant explanation from Rustonomicon.

https://doc.rust-lang.org/nomicon/drop-flags.html

It turns out that Rust actually tracks whether a type should be dropped or not at runtime. As a variable becomes initialized and uninitialized, a drop flag for that variable is toggled. When a variable might need to be dropped, this flag is evaluated to determine if it should be dropped.

Of course, it is often the case that a value’s initialization state can be statically known at every point in the program. If this is the case, then the compiler can theoretically generate more efficient code! For instance, straight-line code has such static drop semantics:

As of Rust 1.0, the drop flags are actually not-so-secretly stashed in a hidden field of any type that implements Drop. Rust sets the drop flag by overwriting the entire value with a particular bit pattern. This is pretty obviously Not The Fastest and causes a bunch of trouble with optimizing code. It’s legacy from a time when you could do much more complex conditional initialization.

So my questions are:

  1. Does Rust 1.12.0 store the drop flags in bitflags on the stack?
  2. Does Rust 1.12.0 know statically whether or not the value’s destructor needs to be run? (e.g. not to have the drop flag at runtime for the codes with such static drop semantics)

I wrote a very simple program with such static drop semantics, and compiled it with rustc 1.12.0 and 1.13.0-beta.1. I think the answers for above questions will be:

  1. No. But 1.13.0 will do.
  2. Unknown. With rustc --emit asm src/main.rs (without -O), I see jne in drop section of generated assembly codes (both 1.12.0 and 1.13.0-beta.1). If I add -O, the generated assembly codes do not seems to move values between variables at all in my program, so I cannot tell.

Here is the program.

use std::mem;

#[derive(Debug)]
struct S {
    a: usize,
}

impl Drop for S {
    fn drop(&mut self) {
        println!("  {:?} is being dropped.", self);
    }
}

fn main() {
    println!("\nsize_of::<S>(): {}\n", mem::size_of::<S>());

    println!("x was un-initialized; just overwrite.");
    let mut x = S { a: 1 };
    println!("  x: {:?}\n", x);

    {
        println!("y was un-initialized; just overwrite and make x un-initialized");
        let mut y = x;
        println!("  y: {:?}\n", y);

        println!("x was un-initialized; just overwrite.");
        x = S { a: 2 };
        println!("  x: {:?}\n", x);

        println!("y was init; Drop y, overwrite it, and make x un-initialized!");
        y = x;
        println!("  y: {:?}\n", y);

        println!("y goes out of scope; y was init; Drop y!");
    }
    println!("\nx goes out of scope; x was un-initialized; do nothing.");
}

and the results.

1.12.0: size_of::<S>() is 8 bytes larger than its definition. So it has the hidden drop flag?

% rustup run 1.12.0 cargo run --release
   Compiling drop-flag v0.1.0 (file:/// ... /drop-flag)
    Finished release [optimized] target(s) in 0.40 secs
     Running `target/release/drop-flag`

size_of::<S>(): 16

x was un-initialized; just overwrite.
  x: S { a: 1 }

y was un-initialized; just overwrite and make x un-initialized
  y: S { a: 1 }

x was un-initialized; just overwrite.
  x: S { a: 2 }

y was init; Drop y, overwrite it, and make x un-initialized!
  S { a: 1 } is being dropped.
  y: S { a: 2 }

y goes out of scope; y was init; Drop y!
  S { a: 2 } is being dropped.

x goes out of scope; x was un-initialized; do nothing.

1.13.0-beta.1: size_of::<S>() is the same size to its definition.

% rustup run beta cargo run --release
   Compiling drop-flag v0.1.0 (file:/// ... /drop-flag)
    Finished release [optimized] target(s) in 0.33 secs
     Running `target/release/drop-flag`

size_of::<S>(): 8

# Outputs Omitted. The same outputs to 1.12.0.

Thanks,

EDIT: Sorry about the indents in main() function. I cloud not figure out a way to make it work with the markups.


#2

Memo:

  • [MIR] non-zeroing drop #33622
    • When drop a value, set the drop flag in the stack instead of zeroing the data. (runtime performance improvement)
    • This PR will be in 1.13.0-beta.1
    • Actually this was already in 1.12.0, but since old AST-based backend still existed in 1.12.0, the hidden drop flag field remained
  • Introducing MIR – Drops and Stack Flags
    • Mentions about non-zeroing drop and also optimization based on compile-time code-path analysis.