The Day const Betrayed Me

143 points TanixLu 49 comments

I love performance, so I love the const keyword. I wrote something like this in my real project:

mod static_data {
    pub const DATA: &'static [u8] = include_bytes!("data.bin");

    pub fn hash_bytes(bytes: &[u8]) -> u64 {
        use std::hash::{Hash, Hasher};
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        bytes.hash(&mut hasher);
        hasher.finish()
    }

    pub fn data_hash() -> u64 {
        hash_bytes(DATA)
    }
}

fn main() {
    const OUTPUT_PATH: &str = "output.bin";

    if !std::fs::read(OUTPUT_PATH)
        .is_ok_and(|data| static_data::hash_bytes(&data) == static_data::data_hash())
    {
        std::fs::write(OUTPUT_PATH, static_data::DATA).unwrap()
    }
}

DATA is a reference with a 'static lifetime, so the content should be embedded only once. Right? That's what I thought, until one day I changed some unrelated code, and the output binary size suddenly dropped. I used AI to find the reason. It turned out that DATA had been embedded twice before. Huh? After some research, I found the reason:

Constants are essentially inlined wherever they are used, meaning that they are copied directly into the relevant context when used. This includes use of constants from external crates, and non-Copy types. References to the same constant are not necessarily guaranteed to refer to the same memory address.

It ruined my perfect dream about const. The solution is simple: use static instead. Lesson learned.

Note: To reproduce this with the example above, you need to explicitly set codegen-units to a value greater than 1 (e.g. codegen-units = 2), because rustc may merge small CGUs by default. Setting lto = "fat" will also optimize the duplicate data away.