Tree shaking for Box<dyn Trait> values

Hi everyone!

I have a project where I build multiple binaries from a single Rust crate. However, I've noticed that functions specific to a single binary get included in the other ones. Those functions are defined on a struct that implements a trait, and I pass this struct through another struct that holds a Box<dyn Trait>.

My question is: how can I prevent those functions from being present in the binary, since they're never called for that binary?

Here's a sample that reproduces that behavior (in main.rs):

pub trait MyTrait {
    fn func1(&self);
    fn func2(&self);
}

pub struct MyStruct {
}

impl MyStruct {
    pub fn new() -> MyStruct {
        MyStruct {}
    }
}

impl MyTrait for MyStruct {
    fn func1(&self) {
        // Nothing is calling this function, but I can find 'mystruct_func1'
        // in the binary.
        println!("mystruct_func1");
    }

    fn func2(&self) {
        // This function is called in main, so that's fine.
        println!("mystruct_func2");
    }
}

pub struct MainStruct {
    pub my_val: Box<dyn MyTrait>,
}

impl MainStruct {
    pub fn new(my_val: Box<dyn MyTrait>) -> MainStruct {
        MainStruct {
            my_val
        }
    }

    pub fn func3(&self) {
        // This is not included in the binary
        println!("mainstruct_func3");
    }
}

fn main() {
    let my_struct = MyStruct::new();
    let main_struct = MainStruct::new(Box::new(my_struct));

    main_struct.my_val.func2();
}

And the commands to reproduce it:

cargo build --release

# Returns "Binary file target/release/tree-shaking-test matches", but shouldn't
grep mystruct_func1 target/release/tree-shaking-test

# Returns "Binary file target/release/tree-shaking-test matches", which is expected
grep mystruct_func2 target/release/tree-shaking-test

The Box<dyn MyTrait> has a pointer to a vtable which looks roughly like this:

struct MyTraitVTable {
  drop_in_place: unsafe fn(*mut ()),
  size: usize,
  alignment: usize,
  func1: fn(*const ()),
  func2: fn(*const ()),
}

So even though func1 is never called, the compiled binary will still need to contain MyStruct's func1() method in order to construct a valid MyTrait vtable.

The linker already does tree shaking (it's often associated with some sort of --gc-sections flag) with functions and methods when they are known to be unused. It's probably that removing unused fields (and in turn, pointers to functions that were stored in that field) isn't an optimisation the linker/LLVM is allowed to make because it has a noticeable effect on things like an object's size or alignment.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.