Skip to content

[wasm-reduce] Empty functions with delta debugging#8640

Open
tlively wants to merge 4 commits intomainfrom
delta-debugging
Open

[wasm-reduce] Empty functions with delta debugging#8640
tlively wants to merge 4 commits intomainfrom
delta-debugging

Conversation

@tlively
Copy link
Copy Markdown
Member

@tlively tlively commented Apr 22, 2026

Delta debugging is an algorithm for finding the minimal set of items necessary to preserve a condition. It generally works by using increasingly fine partitions of the orignal set of items and alternating trying to keep just one of the partitions to make rapid progress and trying to keep the complement of one of the partitions to make smaller changes that are more likely to work.

Add a header containing a templatized delta debugging implementation, then use it in wasm-reduce to preserve the minimal number of function bodies necessary to reproduce the reduction condition. This should allow wasm-reduce to make much faster progress on emptying out functions in the common case and leave it much less work to do afterwards.

Using delta debugging for deleting functions and performing other reduction operations is left as future work. Deleting functions in particular is challenging because it can involve reloading the module from the working file, potentially changing function names and invalidating the function names that would be stored in the delta debugging partitions.

Delta debugging is an algorithm for finding the minimal set of items necessary to preserve a condition. It generally works by using increasingly fine partitions of the orignal set of items and alternating trying to keep just one of the partitions to make rapid progress and trying to keep the complement of one of the partitions to make smaller changes that are more likely to work.

Add a header containing a templatized delta debugging implementation, then use it in wasm-reduce to preserve the minimal number of function bodies necessary to reproduce the reduction condition. This should allow wasm-reduce to make much faster progress on emptying out functions in the common case and leave it much less work to do afterwards.

Using delta debugging for deleting functions and performing other reduction operations is left as future work. Deleting functions in particular is challenging because it can involve reloading the module from the working file, potentially changing function names and invalidating the function names that would be stored in the delta debugging partitions.
@tlively tlively requested a review from a team as a code owner April 22, 2026 05:42
@tlively tlively requested review from kripken and stevenfontanella and removed request for a team April 22, 2026 05:42
@tlively
Copy link
Copy Markdown
Member Author

tlively commented Apr 22, 2026

Currently validating this approach overnight by reducing a 200MB file with a reduction script that takes over four minutes to crash. Let's see how far it gets by the morning!

Comment thread src/tools/wasm-reduce/wasm-reduce.cpp Outdated
[&](Index partitionIndex,
Index numPartitions,
const std::vector<Index>& partition) {
std::cerr << "| try partition " << partitionIndex + 1 << " / "
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.

Why add 1 here?

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.

Printing 1-based indices is slightly more intuitive than 0-based indices.

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.

Agree to disagree 😄

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.

Nice! lgtm % you find it is faster

@tlively
Copy link
Copy Markdown
Member Author

tlively commented Apr 22, 2026

In practice I had to do additional hacks to make sure this ran before the usual destructive visitModule, since that starts out by trying to remove individual instructions and does not make fast enough progress. But it works!

I'd like to make two changes here before landing this:

  1. I want to stop delta debugging early when the partition size is less than the square root of the last successful partition size. This will prevent wasting significant time going through tiny partitions when switching to a different reduction strategy (e.g. running passes or destructively removing instructions) might make more progress.
    1. I can do this early exit with exceptions (gross) or by turning the delta debugging implementation into an iterator (also gross). The ideal end state would be to do the latter using coroutines, but until we're ready for that I will probably just use an exception.
  2. I'd like to be more precise and efficient by excluding functions that already have emptied bodies. Right now if there are lots of functions with empty bodies, we waste time by including them in partitions and trying to empty them out again.

@tlively
Copy link
Copy Markdown
Member Author

tlively commented Apr 23, 2026

@kripken PTAL at the latest changes. I would be happy landing this version. On the 200MB binary, this quickly reduces it to just 2MB (by removing all of the function bodies), but the reducer fails to make quick progress after that. I will add more uses of delta debugging as follow-on work to hopefully make more progress.

@kripken
Copy link
Copy Markdown
Member

kripken commented Apr 23, 2026

How does the speed of reducing functions compare to before this PR? (previous code uses exponential growth, so I'm curious)

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.

lgtm %

  1. With the ordering part landed separately (see comment)
  2. If it is significantly faster (seems worth the TODOs in the code, in that case)

reducer.loadWorking();
reducer.reduceFunctionBodies();
first = false;
}
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.

How about landing this part first? (also, measurement should be independent of it)

@tlively
Copy link
Copy Markdown
Member Author

tlively commented Apr 24, 2026

I updated the old code to also remove function bodies before doing anything else, using the existing algorithm. I only modified it so that it would not try removing functions as well in that first pass. That initial removing function bodies step took 2 hours, 7 minutes. It started out by removing 1, 2, 4, ... functions bodies, but the step size capped out at about 31k for some reason. After it had removed almost all the function bodies that way, it spent a huge amount of time continuing to try to remove one function at a time, and at one point found some work to do and worked back up to a step size of 8192.

In contrast, the new code does this in under 5 minutes in a single step that removes all the function bodies at once. If I remove the code that tries to remove everything at once, it instead uses a logarithmic number of steps and takes 1 hour, 21 minutes (which would be more realistic for a crash that required keeping some function bodies).

I think the new code being so much faster (and making so much more progress up front) is a large part of the reason why it now makes sense to remove function bodies before doing anything else, so I would suggest not splitting that part out of this PR.

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