Skip to content

Add a TypeOrdering pass#7879

Merged
tlively merged 5 commits intomainfrom
type-ordering
Sep 4, 2025
Merged

Add a TypeOrdering pass#7879
tlively merged 5 commits intomainfrom
type-ordering

Conversation

@tlively
Copy link
Copy Markdown
Member

@tlively tlively commented Sep 4, 2025

The binary writer already takes care to order rec groups to minimize the
size taken to encode type indices throughout the module, but it cannot
order types within a rec group because that would change their identity.
MinimizeRecGroups can be used to split the type section into small
recursion groups that can be ordered by the binary writer, but that pass
does not optimize the order of types within a rec group. Furthermore, if
the minimal early rec groups are large, all the extra types taking up
valuable index space can cause a large code size overhead.

To get the best possible outcome, add a TypeOrdering pass that creates a
single large rec group, giving the types the most possible flexibility
to be moved around, and orders the types within it. As we do in
ReorderGlobals, try multiple tolological sorts with different factors
for adjusting weights based on successor types, then choose the best
ordering. Refactoring so that ReorderGlobals and ReorderTypes can share
more code is left as future work.

To avoid reimplementing all of the GlobalTypeRewriter utility, which
already rewrites all the private types into a single rec group, factor
out just the part that sorts the types and make it overrideable in
subclasses.

The binary writer already takes care to order rec groups to minimize the
size taken to encode type indices throughout the module, but it cannot
order types within a rec group because that would change their identity.
MinimizeRecGroups can be used to split the type section into small
recursion groups that can be ordered by the binary writer, but that pass
does not optimize the order of types within a rec group. Furthermore, if
the minimal early rec groups are large, all the extra types taking up
valuable index space can cause a large code size overhead.

To get the best possible outcome, add a TypeOrdering pass that creates a
single large rec group, giving the types the most possible flexibility
to be moved around, and orders the types within it. As we do in
ReorderGlobals, try multiple tolological sorts with different factors
for adjusting weights based on successor types, then choose the best
ordering. Refactoring so that ReorderGlobals and ReorderTypes can share
more code is left as future work.

To avoid reimplementing all of the GlobalTypeRewriter utility, which
already rewrites all the private types into a single rec group, factor
out just the part that sorts the types and make it overrideable in
subclasses.
@tlively tlively requested a review from kripken September 4, 2025 05:36

// Given the predecessor graph of the types to be rebuilt, return a list of
// the types in the order in which they will be rebuilt. Users can override
// this to inject placeholders for extra types ore use custom logic to sort
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// this to inject placeholders for extra types ore use custom logic to sort
// this to inject placeholders for extra types or use custom logic to sort

// contribution from successors, then pick the best result.
static constexpr float minFactor = 0.0;
static constexpr float maxFactor = 1.0;
static constexpr Index buckets = 21;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps explain what "bucket" is?

Copy link
Copy Markdown
Member Author

@tlively tlively Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed buckets to numFactors, which is hopefully more self-explanatory.


// Accumulate weights. Start with the use counts for each type, then add the
// adjusted weight for each successor to the weights of its predecessors.
std::vector<float> weights(numTypes * buckets);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My guess is that each bucket is an entirely separate sorting attempt. If so, why store all of them in the same vector? I would expect either

  • Iteration over attempts, not storing them all in memory at once, or
  • std::vector<std::vector<..>> if they do need to be in memory at once for some reason I'm missing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. I've refactored it to do one sort at a time and not keep all the weights in memory.

std::vector<Index> bestSort;
Index bestCost = 0;
for (Index i = 0; i < buckets; ++i) {
auto curr = TopologicalSort::minSort(succs, [&](Index a, Index b) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this guaranteed to be deterministic when there are ties?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the traversal order on the graph is deterministic and the topological sort utility does not introduce any non-determinism.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to also add a test file with the non-testing variant, just for some coverage. Maybe just with something minimal there.

Copy link
Copy Markdown
Member

@kripken kripken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might also be good to have a test with enough types for the LEB size to matter, so we cover the "7" in the source. Not urgent, and I guess it would be covered well enough if we refactor to use the same sorting as in ReorderGlobals (as that one is heavily tested).

@tlively tlively enabled auto-merge (squash) September 4, 2025 21:31
@tlively tlively merged commit de315aa into main Sep 4, 2025
15 of 16 checks passed
@tlively tlively deleted the type-ordering branch September 4, 2025 22:12
@kripken
Copy link
Copy Markdown
Member

kripken commented Sep 5, 2025

The fuzzer hits a V8 failure:

CompileError: WebAssembly.Module(): type 64: subtyping depth is greater than allowed @+704

😄

We can either skip this file, or add that error to known_issues in fuzz_opt.py. I'm not sure offhand which is better.

@kripken
Copy link
Copy Markdown
Member

kripken commented Sep 8, 2025

Should we document this on the GC optimizer cookbook?

https://github.com/WebAssembly/binaryen/wiki/GC-Optimization-Guidebook

@tlively
Copy link
Copy Markdown
Member Author

tlively commented Sep 8, 2025

Sure, I've just added a section on "Type Reordering and Rec Group Minimization."

@kripken
Copy link
Copy Markdown
Member

kripken commented Sep 8, 2025

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants