-
Notifications
You must be signed in to change notification settings - Fork 115
Propose polymorphization project goal #751
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| # Polymorphic code generation experiment | ||
|
|
||
|
|
||
| | Metadata | | | ||
| | :--------------- | -------------------------------------------------------------------------------- | | ||
| | Point of contact | @camelid | | ||
| | Status | Proposed | | ||
| | Tracking issue | | | ||
|
|
||
| ## Summary | ||
|
|
||
| Implement experimental support in the Rust compiler for polymorphic code generation. Instead of monomorphizing generic code, the compiler will generate type-agnostic instances and add hidden vtable parameters where needed, which should reduce compile times and generated binary sizes. | ||
|
|
||
| ## Motivation | ||
|
|
||
| ### The status quo | ||
|
|
||
| One of the major issues facing Rust users is slow build times, especially during the development cycle. Check builds, which skip codegen, allow users to iterate faster to fix compile errors, but a full build is still needed to run tests or manually try out a change. In full builds, monomorphization is a significant contributor to slow compilation times. Each instance of a generic function must have trait solving and const evaluation performed on its monomorphized body and, more importantly, go through LLVM's entire codegen pipeline. As the performance of Rust's frontend improves through parallel-rustc and other initiatives, monomorphization and codegen will become an even bigger bottleneck since they are already heavily parallelized. | ||
|
|
||
| In the past, there was another project, also called "polymorphization", led by @davidtwco that attempted to make progress on this issue. However, it was restricted to eliminating only "unused" parameters that didn't participate in codegen. Moreover, the project faced roadblocks to success relating to the detection of which parameters are actually unused. [`TypeId` and its breaking of parametricity][typeid-poly] was a contributing factor, and [closures added complications][closures-poly-orig] as well. | ||
|
|
||
| [typeid-poly-orig]: https://github.com/rust-lang/rust/issues/75325 | ||
| [closures-poly-orig]: https://github.com/rust-lang/rust/issues/74636 | ||
|
|
||
| ### What we propose to do about it | ||
|
|
||
| I plan to implement support for truly polymorphic code generation in Rustc. Unlike the previous polymorphization project, my design works for unused and used generic parameters alike, significantly expanding its potential benefits and sidestepping issues with determining whether parameters are relevant to codegen. The axiom is "monomorphic data, polymorphic code". In other words, unlike languages like Haskell and OCaml that generally rely on uniform representations (boxing) to enable polymorphic code generation, data layouts will stay the same under polymorphization, and only function ABIs will change. Polymorphized instances can coexist with monomorphized instances, enabling incremental improvements to the kinds of functions supported by polymorphization. In fact, polymorphization operates at the (type) parameter level, so some parameters can be polymorphized (becoming "erased types"), while others remain monomorphized, within a single function. | ||
|
|
||
| I estimate that the full version of polymorphization could speed up codegen and linking by between 30% and 2x, by eliminating up to half of generated instances in many cases. Binary sizes should also be reduced as a result, without requiring link-time optimization, helping shrink the size of artifact directories containing debug builds. As mentioned, incremental progress can be made toward this full vision, providing checkpoints with smaller but noticeable speedups and binary size reductions. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see we mention binary sizes here, i think it is worth mentioning size bloat in the status quo as well
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would hoist this into an "expected outcomes" section, right now it is buried in implementation details |
||
|
|
||
| To avoid runtime layout computations, Rustc will still monomorphize parameters based on their data layout. Thus, for example, if we have `fn foo<T>(...)`, then `foo::<&u32>` and `foo::<&String>` become the same instance since in both cases their parameter is just a pointer, with a niche at zero. However, `foo::<usize>` is a separate instance because it lacks a niche. This matters because `foo`'s signature, body, or callees may construct types like `Option<T>` that attempt to make use of the niche. We may also consider disabling the niche optimization when polymorphization is enabled to make more instances shareable. | ||
|
|
||
| Data layouts are not the only thing generic code needs to know about its type parameters. Trait methods and associated types also come into play, as do drop glue and `TypeId` information. The key is that all of this information already must be known statically at some point in the call graph. Thus, it can be threaded through implicit function arguments inserted during codegen. Essentially, each type parameter will generate a hidden extended-vtable argument containing the type's `TypeId`, its drop glue function pointer, and each of its trait methods' function pointers. Type parameters whose bounded traits have associated types will generate additional vtable arguments, one per associated type. | ||
|
|
||
| #### Prior art | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is great context thanks |
||
|
|
||
| Rust is somewhat unusual in its use of monomorphization for operationalizing generics. Other functional languages with polymorphic type systems, like Haskell and OCaml, usually rely on uniform representations (aka boxed, aka type erased) for data, which means that every type has the same ABI (essentially just a pointer) and polymorphic code can be generated. Most Java types also have a uniform representation based on pointers and vtables. C++ is most similar to Rust in that it heavily uses monomorphization via templates to implement generics. | ||
|
|
||
| In general, systems languages that prioritize runtime performance and zero-cost abstractions prefer monomorphization, while higher-level languages that may already have heavier runtimes such as garbage collectors prefer type erasure. In addition to the effects on runtime, compile-time, and binary size, the choice of generics implementation notably also affects the expressiveness of the type system, with type erasure enabling more powerful features. | ||
|
|
||
| However, there are also hybrid implementation strategies for generics, which are used by languages like Swift and Go. Both of these languages generate polymorphic code by default, with monomorphization (also called specialization) being an optimization that they may apply. [Swift threads hidden witness tables](https://faultlore.com/blah/swift-abi/) (similar to Rust vtables) through the generated code with information about each type parameter's ABI and methods available. [Go groups generated instances](https://github.com/golang/proposal/blob/58e952a6dd29795163bdff3de8b2d77329ff995b/design/generics-implementation-dictionaries-go1.18.md) by their type parameters' gcshape (how they appear to the allocator and the garbage collector). Go's gcshape is somewhat analogous to our concept of data layouts. | ||
|
|
||
| Recently, Swift has been walking back their embrace of full dynamism through the introduction of [Embedded Swift](https://www.swift.org/get-started/embedded/), which requires monomorphization and necessarily does away with language features like Library Evolution (ABI stability) that depend on dynamism. However, their focus is on supporting "escape hatches" from polymorphic code for the rare cases when developers need low-level control and maximum runtime performance. On a related note, there is a [research paper from the 1990s](https://dl.acm.org/doi/10.1145/143165.143205) that describes a prototype ML compiler that compiles code using unboxed representations as an optimization where possible. | ||
|
|
||
| Finally, there is currently an accepted Rust Project Goal for a [Dictionary Passing Style Experiment](https://rust-lang.github.io/rust-project-goals/2026/dictionary-passing-style-experiment.html) in rustc, which is loosely related to this work. Currently, rustc recomputes the concrete impls to use at trait call sites in codegen. This project goal instead plans to compute this information ahead of time during type-checking and simply reuse it later during codegen. My work aims to eventually delay the _use_ of this information even further, to runtime, at least in debug builds. | ||
|
|
||
| ### Work items over the next year | ||
|
|
||
| | Task | Owner(s) | Notes | | ||
| | ----------- | -------- | ----- | | ||
| | Implement basic polymorphization proof-of-concept | @camelid | Partially working already on real-world code like `std` | | ||
| | Support `TypeId`, drop glue, and similar features | @camelid | | | ||
| | Support simple dyn-compatible traits | @camelid | | | ||
| | Support some non-dyn-compatible traits | @camelid | | | ||
|
Comment on lines
+53
to
+54
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These seem like they would need types review, probably at least small. Separately, the approach of adding a |
||
|
|
||
|
|
||
| ## Team asks | ||
|
|
||
|
|
||
| | Team | Support level | Notes | | ||
| | ---------- | ------------- | --------------------------------------- | | ||
| | [compiler] | medium | reviewers who have expressed interest in helping: @davidtwco, @bjorn3, @oli-obk | | ||
|
|
||
|
|
||
| ## Funding | ||
|
|
||
|
|
||
| | Purpose | Cost | Funded | Sponsor(s) | | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interested in learning more about funding needs, but I'll ask as instructed :)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be worth including a loose timeline (assuming full funding)? Even just a "total X months" granularity helps, though something like "MCP after X, start of impl lands after Y, full experimental MVP Z" is even nicer. https://rust-lang.github.io/rust-project-goals/2026/incremental-system-rethought.html#target-timeline is a great example though it's ok if that level of detail is not known and step 0 is just discovery. |
||
| |---------|------|--------|------------| | ||
| | Contributor (part-time) | Ask | No | | | ||
| | Reviews | TBD | No | | | ||
|
|
||
|
|
||
| ## Frequently asked questions | ||
|
|
||
| ### How does this interact with specialization? | ||
|
|
||
| Specialization in and of itself does not conflict with polymorphization. However, specialized _blanket impls_ do cause problems. I am actively working with @nikomatsakis and @jackh726 to figure out the best path forward. The design of specialization itself is in flux, and the new designs being explored will likely work better with polymorphization. Unlike the current specialization design, they avoid breaking the type system parametricity that is assumed by polymorphization. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is well laid out for the build speed case. No binary size benefits though? Are we not focused on release artifacts yet?