Code bloat: why can't call Self: Sized function from non-sized

I'm trying to fight with code bloat caused by all functions declared in the interface become virtual functions and get reinstantiated in each implementation.

Consider this example:

trait Foo {
  fn baz(&self) {}
  fn bar(&self) { self.baz(); }
}

struct Qux;
struct Quux(u32);

impl Foo for Qux {}
impl Foo for Quux {}

Looking at compiler explorer output it is clear that:

  • bar function is instantiated for each of implementation of Foo
  • baz function is clearly inlined in bar in each implementation

But I don't need baz function to be virtual. I want to reduce some code bloat:

trait Foo {
  // baz function is callable, but not generated for each instantiation
  fn baz(&self) where Self: Sized {}
  fn bar(&self) { self.baz(); }
}

And this code does not compile:

the size for values of type Self cannot be known at compilation time

Which makes no sense to me: each time bar is instantiated, Foo is sized.

So I'm tempted to call this compiled bug.

Can someone explain please what's going on?

Edit: this part of the riddle is solved: Foo could be impemented for unsized types. But still, it is unknown how to solve the generated code bloat.

Because bar() doesn't have a where Self:Sized bound, it can't call any method that requires Self:Sized: in the case that self is an unsized type, there's no implementation of baz to call.

"Virtual" in the context of methods usually refers to its presence in a dynamic-dispatch vtable. As you're never generating any dyn Foo instances, there's never a vtable and so this doesn't apply.

If you want to prevent inlining in a non-dynamic-dispatch context, you can use the #[inline(never)] attribute, and not touch the type bounds for the method.

That’s what compiler says. But why? Each time the bar function is instantiated, theres an instance of baz function to call.

I want to fix code bloat, marking a function as inline never won’t help it.

If I implement Foo for an unsized type, like str, there will be no baz() function, because it's only defined for sized types. The rust compiler always treats the types that implement a trait as an open set that could be arbitrarily extended at any time, so it won't ever rely on the fact that all implementations right now happen to be for Sized types.


I don't understand how to reconcile these two statements of yours. Can you try describing what you're trying to accomplish in different terms?

Because a trait may be implemented by any type and not all types that implement Foo in the future may be Sized. For those that aren't, the bar function wouldn't be able to call baz.
The actual concrete instantiations of the trait don't matter at its definition.
You could work around that problem by adding the Sized bound to Foo itself, in which case, it would be guaranteed that every implementation of Foo is for a Sized type.

I'm assuming you're talking about the generated code and not the source code.
I'm not sure that trying to fix that at the Rust language level is the right solution.
Maybe it's better to use the linker to strip unneeded functions (something like --gc-sections for ld) from your resulting code, if it's really a problem, but I'm no expert at these things.

That makes sense. Thanks!

AFAIU there’s no way to tell the compiler: I want the trait ?Sized but all implementations must be Sized.

Short version. Compiler generates four instances of functions: two functions for two structs. Can I make compiler generate only two functions: two instances of bar without baz in vtable (so compiler could skip generating an instance of baz, eg inline it? (That baz function I don’t even need in the interface.)

This statement looks somewhat contradictory. What do mean by "want the trait ?Sized"?

There is no vtable unless you're generating a dyn Foo instance somewhere. As far as I know, there's no way to prevent the compiler from generating the code for baz, but the linker should be able to strip it from the final executable if it's never used.

I can't because it will prevent casting Qux to &dyn Foo.

Yes.

My understanding is that linker cannot strip vtable entries, even if functions are never called. Vtables do not exist as linker objects, they are just structs, and linker cannot alter it.

AFAIU linker can remove unused functions, but as long as a function is referenced in vtable, the linker can strip whole vtable, but not individual vtable entries.

Because linker does not understand vtables. It does not know which vtable entries are actually used, AFAIU.

I see, it wasn't obvious to me that you were using trait objects.

The only way I see to make Foo object safe and only have bar in its vtable is to make baz not a function of the trait but an inherent method. That would however mean that there could not be a default implementation for bar.
You could work around that by providing a macro to implement Foo (untested):

macro_rules! impl_foo {
    ($name:ident) => {
        impl Foo for $name {
            fn bar(&self) { self.baz() }
        }
    };
}

This macro would fail for types which don't have an inherent baz method, but would prevent that method from appearing in the vtable.

With trait Foo: Sized it is not possible to cast Qux to &dyn Foo.
With trait Foo without : Sized it is not possible to assert that all implementations of Foo are sized.

So we either:

  • make Foo non-dynamic trait
  • or cannot restrict implementation of Foo to be Sized.

In other words, we cannot make trait Foo dynamic and at the same type require all implementations to be Sized.

You may be interested in reading more about trait objects and Sizedness.
In short: Since trait objects are !Sized and they implement their trait, the trait and all used methods must be implementable for !Sized types.

Since you define the baz in the trait, it is part of that traits API and can be used anywhere the trait is used.
Making it require Self: Sized restricts its usage to Sized implementations (and therefore not dyn Foo).
That also means that it cannot be used from methods that are called through dyn Foo, since its not available to them.

The way to work around that is to not make it part of the traits API, as mentioned above.
If you need to use it through the dyn Foo however, there is no way to remove it from the vtable.

1 Like

OK, after reading very helpful comments in this thread, I think I understand what the issue is.

It is not possible to mark a function in a trait as static-dispatch only. (except for making it sized with all limitations)

where Self: Sized is often used for that (at least by me), that's only partially correct because:

  • it doesn't work for unsized trait implementations
  • it prevents calling such function from another trait functions

So in another world it would be possible to write something like:

trait Foo {
  // this function is not callable using trait interface
  !dyn fn baz(&self);
  // this is callable using `&dyn Foo` and `&Qux`
  fn bar(&self) { self.baz(); }
}

The following code works fine and bar calls baz.

trait Foo {
    fn baz(&self)
    where
        Self: Sized,
    {
    }

    fn bar(&self);
}

struct Qux;
struct Quux(u32);

impl Foo for Qux {
    fn bar(&self) {
        self.baz();
    }
}
impl Foo for Quux {
    fn bar(&self) {
        self.baz();
    }
}

What doesn't work is trying to call baz from a non Self: Sized default implementation, because that may not always be possible.
So what you're trying to achieve is definitely possible, with the inconvenience that every implementation of Foo will have to duplicate the default implementation of bar.

1 Like

I'm not convinced this is a good way to deal with a code bloat issue, but it seems to me that if baz isn't part of the interface, it shouldn't be part of the trait. Maybe it gets its own trait?

trait Foo {
    fn bar(&self);
}

trait Baz {
    fn baz(&self);
}

impl<T> Foo for T where T: Baz {
    fn bar(&self) {
        self.baz()
    }
}

You can't guarantee baz will be inlined, but dyn Foo vtables only contain one method entry.

You can add : Sized to Baz if you want but I don't believe it matters for code size if you don't create dyn Baz trait objects.

1 Like

This seems to work!

Now I need to think how to make a nice API from two interface, but it should be possible.

Thanks!

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.