Skip to content

Commit 543990b

Browse files
authored
wasm-merge: Avoid quadratic time in validation [NFC] (#7845)
The actual merging logic in wasm-merge is careful to run in linear time (since #5709). However, validation could actually be quadratic, since we do it after each module that we merge in. Since we validate by default, this made wasm-merge very slow sometimes. To fix that, validate by default once at the very end. When in pass-debug mode, which validates after each pass, validate after each module (so we keep the option for precise debugging of problems). Also, move some validation code being the validation flag, so that --no-validation is even faster. On a large testcase I am investigating (350 modules, 41 MB wasm after merge), this makes us go from 5 minutes down to just 19 seconds. With --no-validation, we drop to 17. (That final number is about how long it takes to run wasm-opt 350 times to round-trip each of those modules, so it is shows wasm-merge's merging is very fast.)
1 parent 00aae63 commit 543990b

File tree

1 file changed

+95
-73
lines changed

1 file changed

+95
-73
lines changed

src/tools/wasm-merge.cpp

Lines changed: 95 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@
9191
// merged, and at the end we traverse the entire merged module once to fuse
9292
// imports and exports.
9393
//
94+
// Debugging: Set BINARYEN_PASS_DEBUG=1 in the env to get validation after each
95+
// merging of a module (like pass-debug mode for the pass runner, this does
96+
// expensive work after each incremental operation). This can take quadratic
97+
// time, so we do not do it by default.
98+
//
9499

95100
#include "ir/module-utils.h"
96101
#include "ir/names.h"
@@ -418,7 +423,7 @@ void checkLimit(bool& valid, const char* kind, T* export_, T* import) {
418423

419424
// Find pairs of matching imports and exports, and make uses of the import refer
420425
// to the exported item (which has been merged into the module).
421-
void fuseImportsAndExports() {
426+
void fuseImportsAndExports(const PassOptions& options) {
422427
// First, scan the exports and build a map. We build a map of [module name] to
423428
// [export name => internal name]. For example, consider this module:
424429
//
@@ -466,80 +471,86 @@ void fuseImportsAndExports() {
466471
}
467472
});
468473

469-
// Make sure that the export types match the import types.
470-
bool valid = true;
471-
ModuleUtils::iterImportedFunctions(merged, [&](Function* import) {
472-
auto internalName = kindModuleExportMaps[ExternalKind::Function]
473-
[import->module][import->base];
474-
if (internalName.is()) {
475-
auto* export_ = merged.getFunction(internalName);
476-
if (!HeapType::isSubType(export_->type, import->type)) {
477-
reportTypeMismatch(valid, "function", import);
478-
std::cerr << "type " << export_->type << " is not a subtype of "
479-
<< import->type << ".\n";
480-
}
481-
}
482-
});
483-
ModuleUtils::iterImportedTables(merged, [&](Table* import) {
484-
auto internalName =
485-
kindModuleExportMaps[ExternalKind::Table][import->module][import->base];
486-
if (internalName.is()) {
487-
auto* export_ = merged.getTable(internalName);
488-
checkLimit(valid, "table", export_, import);
489-
if (export_->type != import->type) {
490-
reportTypeMismatch(valid, "table", import);
491-
std::cerr << "export type " << export_->type
492-
<< " is different from import type " << import->type << ".\n";
493-
}
494-
}
495-
});
496-
ModuleUtils::iterImportedMemories(merged, [&](Memory* import) {
497-
auto internalName =
498-
kindModuleExportMaps[ExternalKind::Memory][import->module][import->base];
499-
if (internalName.is()) {
500-
auto* export_ = merged.getMemory(internalName);
501-
if (export_->is64() != import->is64()) {
502-
reportTypeMismatch(valid, "memory", import);
503-
std::cerr << "index type should match.\n";
474+
if (options.validate) {
475+
// Make sure that the export types match the import types.
476+
bool valid = true;
477+
ModuleUtils::iterImportedFunctions(merged, [&](Function* import) {
478+
auto internalName = kindModuleExportMaps[ExternalKind::Function]
479+
[import->module][import->base];
480+
if (internalName.is()) {
481+
auto* export_ = merged.getFunction(internalName);
482+
if (!HeapType::isSubType(export_->type, import->type)) {
483+
reportTypeMismatch(valid, "function", import);
484+
std::cerr << "type " << export_->type << " is not a subtype of "
485+
<< import->type << ".\n";
486+
}
504487
}
505-
checkLimit(valid, "memory", export_, import);
506-
}
507-
});
508-
ModuleUtils::iterImportedGlobals(merged, [&](Global* import) {
509-
auto internalName =
510-
kindModuleExportMaps[ExternalKind::Global][import->module][import->base];
511-
if (internalName.is()) {
512-
auto* export_ = merged.getGlobal(internalName);
513-
if (export_->mutable_ != import->mutable_) {
514-
reportTypeMismatch(valid, "global", import);
515-
std::cerr << "mutability should match.\n";
488+
});
489+
ModuleUtils::iterImportedTables(merged, [&](Table* import) {
490+
auto internalName =
491+
kindModuleExportMaps[ExternalKind::Table][import->module][import->base];
492+
if (internalName.is()) {
493+
auto* export_ = merged.getTable(internalName);
494+
checkLimit(valid, "table", export_, import);
495+
if (export_->type != import->type) {
496+
reportTypeMismatch(valid, "table", import);
497+
std::cerr << "export type " << export_->type
498+
<< " is different from import type " << import->type
499+
<< ".\n";
500+
}
516501
}
517-
if (export_->mutable_ && export_->type != import->type) {
518-
reportTypeMismatch(valid, "global", import);
519-
std::cerr << "export type " << export_->type
520-
<< " is different from import type " << import->type << ".\n";
502+
});
503+
ModuleUtils::iterImportedMemories(merged, [&](Memory* import) {
504+
auto internalName = kindModuleExportMaps[ExternalKind::Memory]
505+
[import->module][import->base];
506+
if (internalName.is()) {
507+
auto* export_ = merged.getMemory(internalName);
508+
if (export_->is64() != import->is64()) {
509+
reportTypeMismatch(valid, "memory", import);
510+
std::cerr << "index type should match.\n";
511+
}
512+
checkLimit(valid, "memory", export_, import);
521513
}
522-
if (!export_->mutable_ && !Type::isSubType(export_->type, import->type)) {
523-
reportTypeMismatch(valid, "global", import);
524-
std::cerr << "type " << export_->type << " is not a subtype of "
525-
<< import->type << ".\n";
514+
});
515+
ModuleUtils::iterImportedGlobals(merged, [&](Global* import) {
516+
auto internalName = kindModuleExportMaps[ExternalKind::Global]
517+
[import->module][import->base];
518+
if (internalName.is()) {
519+
auto* export_ = merged.getGlobal(internalName);
520+
if (export_->mutable_ != import->mutable_) {
521+
reportTypeMismatch(valid, "global", import);
522+
std::cerr << "mutability should match.\n";
523+
}
524+
if (export_->mutable_ && export_->type != import->type) {
525+
reportTypeMismatch(valid, "global", import);
526+
std::cerr << "export type " << export_->type
527+
<< " is different from import type " << import->type
528+
<< ".\n";
529+
}
530+
if (!export_->mutable_ &&
531+
!Type::isSubType(export_->type, import->type)) {
532+
reportTypeMismatch(valid, "global", import);
533+
std::cerr << "type " << export_->type << " is not a subtype of "
534+
<< import->type << ".\n";
535+
}
526536
}
527-
}
528-
});
529-
ModuleUtils::iterImportedTags(merged, [&](Tag* import) {
530-
auto internalName =
531-
kindModuleExportMaps[ExternalKind::Tag][import->module][import->base];
532-
if (internalName.is()) {
533-
auto* export_ = merged.getTag(internalName);
534-
if (export_->type != import->type) {
535-
reportTypeMismatch(valid, "tag", import);
536-
std::cerr << "export type " << export_->type
537-
<< " is different from import type " << import->type << ".\n";
537+
});
538+
ModuleUtils::iterImportedTags(merged, [&](Tag* import) {
539+
auto internalName =
540+
kindModuleExportMaps[ExternalKind::Tag][import->module][import->base];
541+
if (internalName.is()) {
542+
auto* export_ = merged.getTag(internalName);
543+
if (export_->type != import->type) {
544+
reportTypeMismatch(valid, "tag", import);
545+
std::cerr << "export type " << export_->type
546+
<< " is different from import type " << import->type
547+
<< ".\n";
548+
}
538549
}
550+
});
551+
if (!valid) {
552+
Fatal() << "import/export mismatches";
539553
}
540-
});
541-
if (!valid) {
542-
Fatal() << "import/export mismatches";
543554
}
544555

545556
// Update the things we found.
@@ -730,18 +741,29 @@ Input source maps can be specified by adding an -ism option right after the modu
730741
// This is a later module: do a full merge.
731742
mergeInto(*currModule, inputFileName);
732743

733-
if (options.passOptions.validate) {
734-
if (!WasmValidator().validate(merged)) {
744+
// Validate after each merged module, when we are in pass-debug mode
745+
// (this can be quadratic time).
746+
if (PassRunner::getPassDebug()) {
747+
std::cerr << "[WasmMerge] merged : " << inputFile << '\n';
748+
if (options.passOptions.validate && !WasmValidator().validate(merged)) {
735749
std::cout << merged << '\n';
736-
Fatal() << "error in validating merged after: " << inputFile;
750+
Fatal() << "error in validating after: " << inputFile;
737751
}
738752
}
739753
}
740754
}
741755

756+
// If we didn't validate after each merged module, validate once at the very
757+
// end. This won't catch problems at the earliest point, but is still useful.
758+
if (!PassRunner::getPassDebug() && options.passOptions.validate &&
759+
!WasmValidator().validate(merged)) {
760+
std::cout << merged << '\n';
761+
Fatal() << "error in validating final merged";
762+
}
763+
742764
// Fuse imports and exports now that everything is all together in the merged
743765
// module.
744-
fuseImportsAndExports();
766+
fuseImportsAndExports(options.passOptions);
745767

746768
{
747769
PassRunner passRunner(&merged);

0 commit comments

Comments
 (0)