Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

derived Clone implementations for many-variant enums are unnecessarily large #47796

Open
bholley opened this issue Jan 27, 2018 · 18 comments
Open
Labels
C-enhancement Category: An issue proposing an enhancement or a PR with one. C-optimization Category: An issue highlighting optimization opportunities or PRs implementing such I-heavy Issue: Problems and improvements with respect to binary size of generated code. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@bholley
Copy link
Contributor

bholley commented Jan 27, 2018

The derived Clone implementations in Firefox weigh 192k [1], which is a lot.

Of that 192k, 79k of that is just for PropertyDeclaration::clone. PropertyDeclaration is a very large enum, but most of the variants are simple POD types. Ideally Rust would coalesce those together, and then generate special cases for the types that need it. Unfortunately, it appears that adding a single non-POD variant causes all the cases to be enumerated separately, as seen in the testcase at [2].

From IRC:

eddyb bholley: yeah I think this is a valid issue
eddyb: if you do generate that sort of "(partial) identity match" it's unlikely to have anything else collapse it
eddyb: LLVM might have a pass for switch-discriminant-dependent-values
eddyb: but it probably has serious limitations in practice
eddyb: bholley: but yeah this is one of those cases where not scattering in the first place is significantly easier than gathering later`

Ideally we'd do the same for PartialEq, since PropertyDeclaration::eq weighs another 61k.

[1] nm --print-size --size-sort --radix=d libxul.so | grep "..Clone" | grep -v Cloned | cut -d " " -f 2 | awk '{s+=$1} END {print s}'
[2] https://play.rust-lang.org/?gist=0207af76d2e05acdbed913b7df96aa77&version=stable

@bholley
Copy link
Contributor Author

bholley commented Jan 27, 2018

@Manishearth
Copy link
Member

I was thinking about this earlier:

The current Clone thing works at macro expansion time; which means that we don't know what is and isn't Copy. Except for types defined in other crates.

One way to fix this would be to introduce fn clone<T>(x: T) to intrinsics and make Clone a lang item (it should be already, really, because Copy: Clone). This intrinsic is expanded during (post-monomorphization?) MIR/mir-trans or something where we have all the type info. This also lets us "poke through" nested enums if we really want to (may not be worth it).

How high priority is this for Stylo? I could take a crack at it but it will take time. I'm also unsure if the Rust folks would want such a solution (imo this can be a speed win as well). @eddyb how well do you think this can work? Where do you think this should be introduced in the pipeline?

@Manishearth
Copy link
Member

An alternate way of doing it is to continue generating the match, but in a later MIR pass collapse all the copy arms into a memcpy. The pass is triggered by tagging the generated impl, and runs before other MIR opts. May be fragile, but I think it should work.

Needs to be post-monomorphization again but I think MIR is post-monomorphization anyway.

@pietroalbini pietroalbini added C-enhancement Category: An issue proposing an enhancement or a PR with one. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jan 27, 2018
@eddyb
Copy link
Member

eddyb commented Jan 27, 2018

Wait, I thought this was about emtpy enum variants, not Copy vs not Copy.
FWIW you don't need interaction with intrinsics and we already have shims in the compiler for clone (specifically closures), so those could be made to work on enums.

And no, MIR is pre-monomorphization (it's what we run the MIR borrowck on, which limits its design).

Collapsing correctly is harder than not generating the code in the first place.

@Manishearth
Copy link
Member

No, it's about enum variants that contain only POD.

Can you point me to these shims? The reason I'm going through an intrinsic is that derive() is fundamentally an expansion step; we have to expand to something, even if it's a "fill this in later" placeholder intrinsic.

Oh, also, if we do this we need to check for types where the clone impl is a copy (or is autogenerated), since clone impls for pod types can still do arbitrary things.

@bluss
Copy link
Member

bluss commented Jan 27, 2018

I just want to give a tip for a way to get clearer ASM/LLVM IR output on the playground, with no noise from formatting or main: (playground link)

@Manishearth
Copy link
Member

Oh, btw this can't be done for partialeq. derive(Clone) is allowed to copy Copy things (even if Copy fields have a different Clone impl), but PartialEq doesn't work the same way. Comparing things like enums for example involves comparing specific bits (other bits may get reused).

@eddyb
Copy link
Member

eddyb commented Jan 27, 2018

Also note that memcmp (for PartialEq) would do the wrong thing on padding, unlike memcpy.

@bholley
Copy link
Contributor Author

bholley commented Jan 27, 2018

@Manishearth I'm guessing this could end up saving ~100k in libxul, which is a reasonably big fish as far as these things go - even ignoring the impact on performance, build time, and everyone else in the ecosystem. If you can put in a week or so to make this work, I think that's very much worth doing from a Firefox perspective. If you think it's measured in months, we should evaluate the tradeoffs against your other priorities.

I'm not particularly informed about this stuff, but it does seem to me like it would make sense to spit out a placeholder in the derive step, and then have the compiler generate the right thing post-monomorphization once it's clear which types are Copy.

As for PartialEq, could we at least get the benefits here for Copy types of the same size?

@bluss - Nice trick, that will save me time. Thanks!

@eddyb
Copy link
Member

eddyb commented Jan 27, 2018

If you think it's measured in months

Only if it has to go through the RFC process, otherwise it's a few hours / days of hacking unless you're trying to implement an after-the-fact optimization.

have the compiler generate the right thing post-monomorphization once it's clear which types are Copy.

We already have something (used for Clone closures - @Manishearth is already looking into it) but it's pre-monomorphization - I hope you don't need this with type parameters. It'd be quite wasteful to have more than one MIR Clone::clone shim per type definition.

As for PartialEq, could we at least get the benefits here for Copy types of the same size?

No, if you have any padding bytes or any non-integer leaves, you can't use memcmp.

@Manishearth
Copy link
Member

@bholley yeah the impl work shouldn't take long. Pushing for it through the RFC process, as Eddy says, might.

Post-monomorphization would be nice, but harder to achieve. Right now we directly hand off to LLVM during monomorphization so inserting more stuff there may not be great). All this means is that if you derive(Clone) a generic enum, variants containing generic types will be assumed to not be Copy, even if after substitution it turns out they aren't.

This could affect us a bit; many of our generic enums are full of Copy types (usually Au) in computed mode. But I suspect it will still be worth it.

@SimonSapin
Copy link
Contributor

(Slightly off-topic for this issue: for other method like PropertyDeclaration::id I’ve been pondering whether we’d be better off making PropertyDeclaration a tagged union rather than a Rust enum. For Clone though that won’t help us much, unless maybe if we add Python/macro-level data for which properties’s values are known/supposed to be Copy.)

@eddyb
Copy link
Member

eddyb commented Jan 28, 2018

For generic enums, we again have a very important distinction, in whether the many-variant enums are generic over Copy types or just contain values of other generic types, instantiated at Copy types (the latter is much easier).
Oh and if you're generic but always over Copy types, T: Copy bounds (or any other trait implying Copy) would also work (with a MIR shim at least).

@SimonSapin
Copy link
Contributor

Some of the variants of PropertyDeclaration have fields that contains (concrete instantiations of) generic types, but it is not generic itself.

@Manishearth
Copy link
Member

Manishearth commented Jan 28, 2018

Yeah this is also enums with possibly-copy types in their generics. The main one that's problematic isn't of this type, but various generic property enums are. I think assuming all generics are !Copy is a good-enough solution for most of the problem though .

@eddyb
Copy link
Member

eddyb commented Jan 28, 2018

I think assuming all generics are !Copy is a good first step.

You don't get a choice if you work at the MIR level - if a bound implies T: Copy, fields using T will appear copyable. It's more manual at the syntactical level, but we're not doing that, right?

@Manishearth
Copy link
Member

Yeah, I mistyped and edited that, meant to say "good enough solution for most of the problem"

@Manishearth
Copy link
Member

There is a seemingly-working WIP at #47867

@jonas-schievink jonas-schievink added the I-heavy Issue: Problems and improvements with respect to binary size of generated code. label Apr 20, 2020
@workingjubilee workingjubilee added the C-optimization Category: An issue highlighting optimization opportunities or PRs implementing such label Oct 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-enhancement Category: An issue proposing an enhancement or a PR with one. C-optimization Category: An issue highlighting optimization opportunities or PRs implementing such I-heavy Issue: Problems and improvements with respect to binary size of generated code. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
8 participants