From e0fbdd029e308d69eb431f38670873ddfa67d693 Mon Sep 17 00:00:00 2001 From: Mate Soos Date: Mon, 30 Mar 2026 11:53:22 +0200 Subject: [PATCH 01/10] =?UTF-8?q?Fix=20O(n=C2=B2)=20in=20VarNameCleaner::f?= =?UTF-8?q?indCleanName=20using=20per-base-name=20counter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When many variables share the same stripped base name, findCleanName would probe name_1, name_2, ..., name_N sequentially for each variable, resulting in O(n²) calls to isUsedName (which involves regex matching via isRestrictedIdentifier). This was the dominant cost in fuzzer timeouts where the optimizer generated many variables. Track a counter per base name (m_nextSuffix) so we resume from the last assigned suffix instead of reprobing from 1 each time. Co-Authored-By: Claude Opus 4.6 (1M context) Fix more Update More tests More tests --- Changelog.md | 1 + libyul/optimiser/VarNameCleaner.cpp | 16 ++- libyul/optimiser/VarNameCleaner.h | 3 + test/CMakeLists.txt | 1 + test/libyul/VarNameCleanerPerf.cpp | 124 ++++++++++++++++++ .../double_underscore_suffix.yul | 15 +++ .../varNameCleaner/interleaved_bases.yul | 25 ++++ .../varNameCleaner/many_same_base.yul | 25 ++++ .../many_same_base_in_function.yul | 29 ++++ .../varNameCleaner/nested_suffixes.yul | 17 +++ .../references_after_rename.yul | 15 +++ .../varNameCleaner/same_base_with_gap.yul | 19 +++ .../suffix_skips_function_name.yul | 20 +++ .../varNameCleaner/suffix_skips_reserved.yul | 19 +++ .../two_functions_same_base.yul | 32 +++++ 15 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 test/libyul/VarNameCleanerPerf.cpp create mode 100644 test/libyul/yulOptimizerTests/varNameCleaner/double_underscore_suffix.yul create mode 100644 test/libyul/yulOptimizerTests/varNameCleaner/interleaved_bases.yul create mode 100644 test/libyul/yulOptimizerTests/varNameCleaner/many_same_base.yul create mode 100644 test/libyul/yulOptimizerTests/varNameCleaner/many_same_base_in_function.yul create mode 100644 test/libyul/yulOptimizerTests/varNameCleaner/nested_suffixes.yul create mode 100644 test/libyul/yulOptimizerTests/varNameCleaner/references_after_rename.yul create mode 100644 test/libyul/yulOptimizerTests/varNameCleaner/same_base_with_gap.yul create mode 100644 test/libyul/yulOptimizerTests/varNameCleaner/suffix_skips_function_name.yul create mode 100644 test/libyul/yulOptimizerTests/varNameCleaner/suffix_skips_reserved.yul create mode 100644 test/libyul/yulOptimizerTests/varNameCleaner/two_functions_same_base.yul diff --git a/Changelog.md b/Changelog.md index 5c1420ca346c..03368b9171d7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,6 +15,7 @@ Compiler Features: * Standard JSON Interface: Introduce `settings.experimental` setting required for enabling the experimental mode. * Standard JSON Interface: Replace the top-level ``ethdebug`` output with ``ethdebug.resources`` and ``ethdebug.compilation``. Decouple ethdebug outputs from binary compilation so that global ethdebug outputs can be produced without generating bytecode. * Yul Optimizer: Improve performance of control flow side effects collector and function references resolver. +* Yul Optimizer: Fix O(n²) performance in ``VarNameCleaner`` when many variables share the same base name. Bugfixes: * Yul: Fix incorrect serialization of Yul object names containing double quotes and escape sequences, producing output that could not be parsed as valid Yul. diff --git a/libyul/optimiser/VarNameCleaner.cpp b/libyul/optimiser/VarNameCleaner.cpp index 8d34ce85e408..ad95e7ca4585 100644 --- a/libyul/optimiser/VarNameCleaner.cpp +++ b/libyul/optimiser/VarNameCleaner.cpp @@ -55,6 +55,8 @@ void VarNameCleaner::operator()(FunctionDefinition& _funDef) m_usedNames = m_namesToKeep; std::map globalTranslatedNames; swap(globalTranslatedNames, m_translatedNames); + std::map globalNextSuffix; + swap(globalNextSuffix, m_nextSuffix); renameVariables(_funDef.parameters); renameVariables(_funDef.returnVariables); @@ -62,6 +64,7 @@ void VarNameCleaner::operator()(FunctionDefinition& _funDef) swap(globalUsedNames, m_usedNames); swap(globalTranslatedNames, m_translatedNames); + swap(globalNextSuffix, m_nextSuffix); m_insideFunction = false; } @@ -99,12 +102,19 @@ YulName VarNameCleaner::findCleanName(YulName const& _name) const if (!isUsedName(newName)) return newName; - // create new name with suffix (by finding a free identifier) - for (size_t i = 1; i < std::numeric_limits::max(); ++i) + // Use a per-base-name counter to avoid O(n²) probing when many + // variables share the same stripped base name. + size_t& nextSuffix = m_nextSuffix[newName]; + if (nextSuffix == 0) + nextSuffix = 1; + for (; nextSuffix < std::numeric_limits::max(); ++nextSuffix) { - YulName newNameSuffixed = YulName{newName.str() + "_" + std::to_string(i)}; + YulName newNameSuffixed = YulName{newName.str() + "_" + std::to_string(nextSuffix)}; if (!isUsedName(newNameSuffixed)) + { + ++nextSuffix; return newNameSuffixed; + } } yulAssert(false, "Exhausted by attempting to find an available suffix."); } diff --git a/libyul/optimiser/VarNameCleaner.h b/libyul/optimiser/VarNameCleaner.h index 47f3aca48db9..f61426008f30 100644 --- a/libyul/optimiser/VarNameCleaner.h +++ b/libyul/optimiser/VarNameCleaner.h @@ -89,6 +89,9 @@ class VarNameCleaner: public ASTModifier /// Set of names that are in use. std::set m_usedNames; + /// Next suffix to try per stripped base name, avoids O(n²) probing. + mutable std::map m_nextSuffix; + /// Maps old to new names. std::map m_translatedNames; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c6c8040f8536..7fc395c55226 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -165,6 +165,7 @@ set(libyul_sources libyul/StackShufflingTest.h libyul/SyntaxTest.h libyul/SyntaxTest.cpp + libyul/VarNameCleanerPerf.cpp libyul/YulInterpreterTest.cpp libyul/YulInterpreterTest.h libyul/YulOptimizerTest.cpp diff --git a/test/libyul/VarNameCleanerPerf.cpp b/test/libyul/VarNameCleanerPerf.cpp new file mode 100644 index 000000000000..d78c11a2e488 --- /dev/null +++ b/test/libyul/VarNameCleanerPerf.cpp @@ -0,0 +1,124 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * Performance tests for VarNameCleaner to verify O(n) behaviour + * when many variables share the same stripped base name. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +using namespace solidity::langutil; + +namespace solidity::yul::test +{ + +namespace +{ + +/// Build a Yul block with @a n variables all sharing the same base name "x": +/// { let x_100 := 1 let x_200 := 2 ... let x_(n*100) := n } +/// Using large suffix gaps so stripSuffix strips them all to "x". +std::string buildManyVarsYul(size_t n) +{ + std::ostringstream src; + src << "{\n"; + for (size_t i = 1; i <= n; ++i) + src << " let x_" << (i * 100) << " := " << i << "\n"; + src << "}\n"; + return src.str(); +} + +/// Run VarNameCleaner on the given Yul source and return the elapsed time. +std::chrono::microseconds runVarNameCleaner(std::string const& _source) +{ + auto block = disambiguate(_source); + Dialect const& d = EVMDialect::strictAssemblyForEVMObjects(EVMVersion{}, std::nullopt); + std::set reserved; + NameDispenser dispenser(d, block, reserved); + OptimiserStepContext context{d, dispenser, reserved, 0}; + FunctionHoister::run(context, block); + FunctionGrouper::run(context, block); + + auto start = std::chrono::high_resolution_clock::now(); + VarNameCleaner::run(context, block); + auto end = std::chrono::high_resolution_clock::now(); + + return std::chrono::duration_cast(end - start); +} + +} + +BOOST_AUTO_TEST_SUITE(YulVarNameCleanerPerf) + +// Verify that VarNameCleaner scales linearly, not quadratically. +// We compare the runtime of N=2000 vs N=500. With the old O(n^2) code +// the ratio would be ~16x; with the fix it should be close to 4x (linear). +// We use a generous 8x threshold to avoid flaky failures. +BOOST_AUTO_TEST_CASE(linear_scaling) +{ + size_t const smallN = 500; + size_t const largeN = 2000; + + std::string smallSrc = buildManyVarsYul(smallN); + std::string largeSrc = buildManyVarsYul(largeN); + + // Warm up + runVarNameCleaner(smallSrc); + + auto smallTime = runVarNameCleaner(smallSrc); + auto largeTime = runVarNameCleaner(largeSrc); + + // With O(n) scaling and n ratio of 4x, we expect time ratio ~4x. + // With O(n^2), the ratio would be ~16x. + // Allow up to 8x to avoid flaky test failures while still catching quadratic. + double ratio = static_cast(largeTime.count()) / + static_cast(std::max(smallTime.count(), decltype(smallTime.count()){1})); + + BOOST_TEST_MESSAGE("VarNameCleaner: N=" << smallN << " took " << smallTime.count() << " us"); + BOOST_TEST_MESSAGE("VarNameCleaner: N=" << largeN << " took " << largeTime.count() << " us"); + BOOST_TEST_MESSAGE("Ratio: " << ratio << "x (expected ~4x for linear, ~16x for quadratic)"); + + BOOST_CHECK_MESSAGE(ratio < 8.0, + "VarNameCleaner appears to scale worse than linearly: " + "ratio=" << ratio << "x for " << largeN << "/" << smallN << " variables. " + "Expected <8x for O(n), got " << ratio << "x." + ); +} + +BOOST_AUTO_TEST_SUITE_END() + +} diff --git a/test/libyul/yulOptimizerTests/varNameCleaner/double_underscore_suffix.yul b/test/libyul/yulOptimizerTests/varNameCleaner/double_underscore_suffix.yul new file mode 100644 index 000000000000..3f67b621e871 --- /dev/null +++ b/test/libyul/yulOptimizerTests/varNameCleaner/double_underscore_suffix.yul @@ -0,0 +1,15 @@ +{ + let x__1 := 1 + let x__2 := 2 + let x___3 := 3 +} +// ---- +// step: varNameCleaner +// +// { +// { +// let x := 1 +// let x_1 := 2 +// let x_2 := 3 +// } +// } diff --git a/test/libyul/yulOptimizerTests/varNameCleaner/interleaved_bases.yul b/test/libyul/yulOptimizerTests/varNameCleaner/interleaved_bases.yul new file mode 100644 index 000000000000..1f7136ee6491 --- /dev/null +++ b/test/libyul/yulOptimizerTests/varNameCleaner/interleaved_bases.yul @@ -0,0 +1,25 @@ +{ + let x_5 := 1 + let y_3 := 2 + let x_10 := 3 + let y_7 := 4 + let z_1 := 5 + let x_20 := 6 + let y_15 := 7 + let z_8 := 8 +} +// ---- +// step: varNameCleaner +// +// { +// { +// let x := 1 +// let y := 2 +// let x_1 := 3 +// let y_1 := 4 +// let z := 5 +// let x_2 := 6 +// let y_2 := 7 +// let z_1 := 8 +// } +// } diff --git a/test/libyul/yulOptimizerTests/varNameCleaner/many_same_base.yul b/test/libyul/yulOptimizerTests/varNameCleaner/many_same_base.yul new file mode 100644 index 000000000000..b7fd6a09d998 --- /dev/null +++ b/test/libyul/yulOptimizerTests/varNameCleaner/many_same_base.yul @@ -0,0 +1,25 @@ +{ + let x_10 := 1 + let x_20 := 2 + let x_30 := 3 + let x_40 := 4 + let x_50 := 5 + let x_60 := 6 + let x_70 := 7 + let x_80 := 8 +} +// ---- +// step: varNameCleaner +// +// { +// { +// let x := 1 +// let x_1 := 2 +// let x_2 := 3 +// let x_3 := 4 +// let x_4 := 5 +// let x_5 := 6 +// let x_6 := 7 +// let x_7 := 8 +// } +// } diff --git a/test/libyul/yulOptimizerTests/varNameCleaner/many_same_base_in_function.yul b/test/libyul/yulOptimizerTests/varNameCleaner/many_same_base_in_function.yul new file mode 100644 index 000000000000..b3fe26a9c68a --- /dev/null +++ b/test/libyul/yulOptimizerTests/varNameCleaner/many_same_base_in_function.yul @@ -0,0 +1,29 @@ +{ + let x_1 := 1 + function f() + { + let x_10 := 1 + let x_20 := 2 + let x_30 := 3 + let x_40 := 4 + let x_50 := 5 + } + let x_2 := 2 +} +// ---- +// step: varNameCleaner +// +// { +// { +// let x := 1 +// let x_1 := 2 +// } +// function f() +// { +// let x := 1 +// let x_1 := 2 +// let x_2 := 3 +// let x_3 := 4 +// let x_4 := 5 +// } +// } diff --git a/test/libyul/yulOptimizerTests/varNameCleaner/nested_suffixes.yul b/test/libyul/yulOptimizerTests/varNameCleaner/nested_suffixes.yul new file mode 100644 index 000000000000..c3fcd27401aa --- /dev/null +++ b/test/libyul/yulOptimizerTests/varNameCleaner/nested_suffixes.yul @@ -0,0 +1,17 @@ +{ + let x_1_2 := 1 + let x_3_4_5 := 2 + let x_6 := 3 + let x_1_2_3_4 := 4 +} +// ---- +// step: varNameCleaner +// +// { +// { +// let x := 1 +// let x_1 := 2 +// let x_2 := 3 +// let x_3 := 4 +// } +// } diff --git a/test/libyul/yulOptimizerTests/varNameCleaner/references_after_rename.yul b/test/libyul/yulOptimizerTests/varNameCleaner/references_after_rename.yul new file mode 100644 index 000000000000..4bdec45697bd --- /dev/null +++ b/test/libyul/yulOptimizerTests/varNameCleaner/references_after_rename.yul @@ -0,0 +1,15 @@ +{ + let x_5 := 42 + let y_3 := x_5 + let z_1 := add(x_5, y_3) +} +// ---- +// step: varNameCleaner +// +// { +// { +// let x := 42 +// let y := x +// let z := add(x, y) +// } +// } diff --git a/test/libyul/yulOptimizerTests/varNameCleaner/same_base_with_gap.yul b/test/libyul/yulOptimizerTests/varNameCleaner/same_base_with_gap.yul new file mode 100644 index 000000000000..b8bc8e6eaa46 --- /dev/null +++ b/test/libyul/yulOptimizerTests/varNameCleaner/same_base_with_gap.yul @@ -0,0 +1,19 @@ +{ + let x := 1 + let x_1 := 2 + let x_5 := 3 + let x_10 := 4 + let x_20 := 5 +} +// ---- +// step: varNameCleaner +// +// { +// { +// let x := 1 +// let x_1 := 2 +// let x_2 := 3 +// let x_3 := 4 +// let x_4 := 5 +// } +// } diff --git a/test/libyul/yulOptimizerTests/varNameCleaner/suffix_skips_function_name.yul b/test/libyul/yulOptimizerTests/varNameCleaner/suffix_skips_function_name.yul new file mode 100644 index 000000000000..5ca68d9b65a4 --- /dev/null +++ b/test/libyul/yulOptimizerTests/varNameCleaner/suffix_skips_function_name.yul @@ -0,0 +1,20 @@ +{ + // x_1 is a function name, so the suffix counter must skip it + // when cleaning x_100 and x_200 which both strip to "x" + function x_1() {} + let x := 1 + let x_100 := 2 + let x_200 := 3 +} +// ---- +// step: varNameCleaner +// +// { +// { +// let x := 1 +// let x_2 := 2 +// let x_3 := 3 +// } +// function x_1() +// { } +// } diff --git a/test/libyul/yulOptimizerTests/varNameCleaner/suffix_skips_reserved.yul b/test/libyul/yulOptimizerTests/varNameCleaner/suffix_skips_reserved.yul new file mode 100644 index 000000000000..33922df8b87d --- /dev/null +++ b/test/libyul/yulOptimizerTests/varNameCleaner/suffix_skips_reserved.yul @@ -0,0 +1,19 @@ +{ + let x := 1 + let x_2 := 2 + let x_100 := 3 + let x_200 := 4 + let x_300 := 5 +} +// ---- +// step: varNameCleaner +// +// { +// { +// let x := 1 +// let x_1 := 2 +// let x_2 := 3 +// let x_3 := 4 +// let x_4 := 5 +// } +// } diff --git a/test/libyul/yulOptimizerTests/varNameCleaner/two_functions_same_base.yul b/test/libyul/yulOptimizerTests/varNameCleaner/two_functions_same_base.yul new file mode 100644 index 000000000000..b468b7671504 --- /dev/null +++ b/test/libyul/yulOptimizerTests/varNameCleaner/two_functions_same_base.yul @@ -0,0 +1,32 @@ +{ + function f() + { + let x_10 := 1 + let x_20 := 2 + let x_30 := 3 + } + function g() + { + let x_40 := 4 + let x_50 := 5 + let x_60 := 6 + } +} +// ---- +// step: varNameCleaner +// +// { +// { } +// function f() +// { +// let x := 1 +// let x_1 := 2 +// let x_2 := 3 +// } +// function g() +// { +// let x := 4 +// let x_1 := 5 +// let x_2 := 6 +// } +// } From 7b976b0bdfc0accf90c94f9390ce8de4cbedf9fa Mon Sep 17 00:00:00 2001 From: "Mate Soos @ Argot" Date: Wed, 1 Apr 2026 11:02:11 +0200 Subject: [PATCH 02/10] Update libyul/optimiser/VarNameCleaner.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nikola Matić --- libyul/optimiser/VarNameCleaner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libyul/optimiser/VarNameCleaner.cpp b/libyul/optimiser/VarNameCleaner.cpp index ad95e7ca4585..b8fb5acf6e12 100644 --- a/libyul/optimiser/VarNameCleaner.cpp +++ b/libyul/optimiser/VarNameCleaner.cpp @@ -104,7 +104,7 @@ YulName VarNameCleaner::findCleanName(YulName const& _name) const // Use a per-base-name counter to avoid O(n²) probing when many // variables share the same stripped base name. - size_t& nextSuffix = m_nextSuffix[newName]; + size_t& nextSuffix = m_nextSuffix.at(newName); if (nextSuffix == 0) nextSuffix = 1; for (; nextSuffix < std::numeric_limits::max(); ++nextSuffix) From ca90aeeac552b9c44007639be619813e354544c5 Mon Sep 17 00:00:00 2001 From: Mate Soos Date: Wed, 1 Apr 2026 11:06:21 +0200 Subject: [PATCH 03/10] No need for mutable, Remove flaky tests --- libyul/optimiser/VarNameCleaner.cpp | 4 +- libyul/optimiser/VarNameCleaner.h | 2 +- test/libyul/VarNameCleanerPerf.cpp | 124 ---------------------------- 3 files changed, 2 insertions(+), 128 deletions(-) delete mode 100644 test/libyul/VarNameCleanerPerf.cpp diff --git a/libyul/optimiser/VarNameCleaner.cpp b/libyul/optimiser/VarNameCleaner.cpp index b8fb5acf6e12..5dffb18c5177 100644 --- a/libyul/optimiser/VarNameCleaner.cpp +++ b/libyul/optimiser/VarNameCleaner.cpp @@ -104,9 +104,7 @@ YulName VarNameCleaner::findCleanName(YulName const& _name) const // Use a per-base-name counter to avoid O(n²) probing when many // variables share the same stripped base name. - size_t& nextSuffix = m_nextSuffix.at(newName); - if (nextSuffix == 0) - nextSuffix = 1; + size_t nextSuffix = util::valueOrDefault(m_nextSuffix, newName, static_cast(1)); for (; nextSuffix < std::numeric_limits::max(); ++nextSuffix) { YulName newNameSuffixed = YulName{newName.str() + "_" + std::to_string(nextSuffix)}; diff --git a/libyul/optimiser/VarNameCleaner.h b/libyul/optimiser/VarNameCleaner.h index f61426008f30..74af9b2b303b 100644 --- a/libyul/optimiser/VarNameCleaner.h +++ b/libyul/optimiser/VarNameCleaner.h @@ -90,7 +90,7 @@ class VarNameCleaner: public ASTModifier std::set m_usedNames; /// Next suffix to try per stripped base name, avoids O(n²) probing. - mutable std::map m_nextSuffix; + std::map m_nextSuffix; /// Maps old to new names. std::map m_translatedNames; diff --git a/test/libyul/VarNameCleanerPerf.cpp b/test/libyul/VarNameCleanerPerf.cpp deleted file mode 100644 index d78c11a2e488..000000000000 --- a/test/libyul/VarNameCleanerPerf.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/* - This file is part of solidity. - - solidity is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - solidity is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with solidity. If not, see . -*/ -// SPDX-License-Identifier: GPL-3.0 -/** - * Performance tests for VarNameCleaner to verify O(n) behaviour - * when many variables share the same stripped base name. - */ - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -using namespace solidity::langutil; - -namespace solidity::yul::test -{ - -namespace -{ - -/// Build a Yul block with @a n variables all sharing the same base name "x": -/// { let x_100 := 1 let x_200 := 2 ... let x_(n*100) := n } -/// Using large suffix gaps so stripSuffix strips them all to "x". -std::string buildManyVarsYul(size_t n) -{ - std::ostringstream src; - src << "{\n"; - for (size_t i = 1; i <= n; ++i) - src << " let x_" << (i * 100) << " := " << i << "\n"; - src << "}\n"; - return src.str(); -} - -/// Run VarNameCleaner on the given Yul source and return the elapsed time. -std::chrono::microseconds runVarNameCleaner(std::string const& _source) -{ - auto block = disambiguate(_source); - Dialect const& d = EVMDialect::strictAssemblyForEVMObjects(EVMVersion{}, std::nullopt); - std::set reserved; - NameDispenser dispenser(d, block, reserved); - OptimiserStepContext context{d, dispenser, reserved, 0}; - FunctionHoister::run(context, block); - FunctionGrouper::run(context, block); - - auto start = std::chrono::high_resolution_clock::now(); - VarNameCleaner::run(context, block); - auto end = std::chrono::high_resolution_clock::now(); - - return std::chrono::duration_cast(end - start); -} - -} - -BOOST_AUTO_TEST_SUITE(YulVarNameCleanerPerf) - -// Verify that VarNameCleaner scales linearly, not quadratically. -// We compare the runtime of N=2000 vs N=500. With the old O(n^2) code -// the ratio would be ~16x; with the fix it should be close to 4x (linear). -// We use a generous 8x threshold to avoid flaky failures. -BOOST_AUTO_TEST_CASE(linear_scaling) -{ - size_t const smallN = 500; - size_t const largeN = 2000; - - std::string smallSrc = buildManyVarsYul(smallN); - std::string largeSrc = buildManyVarsYul(largeN); - - // Warm up - runVarNameCleaner(smallSrc); - - auto smallTime = runVarNameCleaner(smallSrc); - auto largeTime = runVarNameCleaner(largeSrc); - - // With O(n) scaling and n ratio of 4x, we expect time ratio ~4x. - // With O(n^2), the ratio would be ~16x. - // Allow up to 8x to avoid flaky test failures while still catching quadratic. - double ratio = static_cast(largeTime.count()) / - static_cast(std::max(smallTime.count(), decltype(smallTime.count()){1})); - - BOOST_TEST_MESSAGE("VarNameCleaner: N=" << smallN << " took " << smallTime.count() << " us"); - BOOST_TEST_MESSAGE("VarNameCleaner: N=" << largeN << " took " << largeTime.count() << " us"); - BOOST_TEST_MESSAGE("Ratio: " << ratio << "x (expected ~4x for linear, ~16x for quadratic)"); - - BOOST_CHECK_MESSAGE(ratio < 8.0, - "VarNameCleaner appears to scale worse than linearly: " - "ratio=" << ratio << "x for " << largeN << "/" << smallN << " variables. " - "Expected <8x for O(n), got " << ratio << "x." - ); -} - -BOOST_AUTO_TEST_SUITE_END() - -} From f040e42e1f550779f7804cfd6cde402cc4bc1d9d Mon Sep 17 00:00:00 2001 From: Mate Soos Date: Wed, 1 Apr 2026 11:11:06 +0200 Subject: [PATCH 04/10] Cleaner changelog --- Changelog.md | 1 - 1 file changed, 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 03368b9171d7..5c1420ca346c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,7 +15,6 @@ Compiler Features: * Standard JSON Interface: Introduce `settings.experimental` setting required for enabling the experimental mode. * Standard JSON Interface: Replace the top-level ``ethdebug`` output with ``ethdebug.resources`` and ``ethdebug.compilation``. Decouple ethdebug outputs from binary compilation so that global ethdebug outputs can be produced without generating bytecode. * Yul Optimizer: Improve performance of control flow side effects collector and function references resolver. -* Yul Optimizer: Fix O(n²) performance in ``VarNameCleaner`` when many variables share the same base name. Bugfixes: * Yul: Fix incorrect serialization of Yul object names containing double quotes and escape sequences, producing output that could not be parsed as valid Yul. From 5d66e17004bceee1f7352afcbb2122e3ab391418 Mon Sep 17 00:00:00 2001 From: Mate Soos Date: Wed, 1 Apr 2026 11:16:53 +0200 Subject: [PATCH 05/10] Fixing --- test/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7fc395c55226..c6c8040f8536 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -165,7 +165,6 @@ set(libyul_sources libyul/StackShufflingTest.h libyul/SyntaxTest.h libyul/SyntaxTest.cpp - libyul/VarNameCleanerPerf.cpp libyul/YulInterpreterTest.cpp libyul/YulInterpreterTest.h libyul/YulOptimizerTest.cpp From 6789d7e068dac13642f3b5c63b79f83cabdb8900 Mon Sep 17 00:00:00 2001 From: Mate Soos Date: Thu, 9 Apr 2026 15:35:03 +0200 Subject: [PATCH 06/10] Revert "No need for mutable, Remove flaky tests" This reverts commit 4b13837f392f53c152f44d5617764d104531f284. --- libyul/optimiser/VarNameCleaner.cpp | 4 +- libyul/optimiser/VarNameCleaner.h | 2 +- test/libyul/VarNameCleanerPerf.cpp | 124 ++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 test/libyul/VarNameCleanerPerf.cpp diff --git a/libyul/optimiser/VarNameCleaner.cpp b/libyul/optimiser/VarNameCleaner.cpp index 5dffb18c5177..b8fb5acf6e12 100644 --- a/libyul/optimiser/VarNameCleaner.cpp +++ b/libyul/optimiser/VarNameCleaner.cpp @@ -104,7 +104,9 @@ YulName VarNameCleaner::findCleanName(YulName const& _name) const // Use a per-base-name counter to avoid O(n²) probing when many // variables share the same stripped base name. - size_t nextSuffix = util::valueOrDefault(m_nextSuffix, newName, static_cast(1)); + size_t& nextSuffix = m_nextSuffix.at(newName); + if (nextSuffix == 0) + nextSuffix = 1; for (; nextSuffix < std::numeric_limits::max(); ++nextSuffix) { YulName newNameSuffixed = YulName{newName.str() + "_" + std::to_string(nextSuffix)}; diff --git a/libyul/optimiser/VarNameCleaner.h b/libyul/optimiser/VarNameCleaner.h index 74af9b2b303b..f61426008f30 100644 --- a/libyul/optimiser/VarNameCleaner.h +++ b/libyul/optimiser/VarNameCleaner.h @@ -90,7 +90,7 @@ class VarNameCleaner: public ASTModifier std::set m_usedNames; /// Next suffix to try per stripped base name, avoids O(n²) probing. - std::map m_nextSuffix; + mutable std::map m_nextSuffix; /// Maps old to new names. std::map m_translatedNames; diff --git a/test/libyul/VarNameCleanerPerf.cpp b/test/libyul/VarNameCleanerPerf.cpp new file mode 100644 index 000000000000..d78c11a2e488 --- /dev/null +++ b/test/libyul/VarNameCleanerPerf.cpp @@ -0,0 +1,124 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +/** + * Performance tests for VarNameCleaner to verify O(n) behaviour + * when many variables share the same stripped base name. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +using namespace solidity::langutil; + +namespace solidity::yul::test +{ + +namespace +{ + +/// Build a Yul block with @a n variables all sharing the same base name "x": +/// { let x_100 := 1 let x_200 := 2 ... let x_(n*100) := n } +/// Using large suffix gaps so stripSuffix strips them all to "x". +std::string buildManyVarsYul(size_t n) +{ + std::ostringstream src; + src << "{\n"; + for (size_t i = 1; i <= n; ++i) + src << " let x_" << (i * 100) << " := " << i << "\n"; + src << "}\n"; + return src.str(); +} + +/// Run VarNameCleaner on the given Yul source and return the elapsed time. +std::chrono::microseconds runVarNameCleaner(std::string const& _source) +{ + auto block = disambiguate(_source); + Dialect const& d = EVMDialect::strictAssemblyForEVMObjects(EVMVersion{}, std::nullopt); + std::set reserved; + NameDispenser dispenser(d, block, reserved); + OptimiserStepContext context{d, dispenser, reserved, 0}; + FunctionHoister::run(context, block); + FunctionGrouper::run(context, block); + + auto start = std::chrono::high_resolution_clock::now(); + VarNameCleaner::run(context, block); + auto end = std::chrono::high_resolution_clock::now(); + + return std::chrono::duration_cast(end - start); +} + +} + +BOOST_AUTO_TEST_SUITE(YulVarNameCleanerPerf) + +// Verify that VarNameCleaner scales linearly, not quadratically. +// We compare the runtime of N=2000 vs N=500. With the old O(n^2) code +// the ratio would be ~16x; with the fix it should be close to 4x (linear). +// We use a generous 8x threshold to avoid flaky failures. +BOOST_AUTO_TEST_CASE(linear_scaling) +{ + size_t const smallN = 500; + size_t const largeN = 2000; + + std::string smallSrc = buildManyVarsYul(smallN); + std::string largeSrc = buildManyVarsYul(largeN); + + // Warm up + runVarNameCleaner(smallSrc); + + auto smallTime = runVarNameCleaner(smallSrc); + auto largeTime = runVarNameCleaner(largeSrc); + + // With O(n) scaling and n ratio of 4x, we expect time ratio ~4x. + // With O(n^2), the ratio would be ~16x. + // Allow up to 8x to avoid flaky test failures while still catching quadratic. + double ratio = static_cast(largeTime.count()) / + static_cast(std::max(smallTime.count(), decltype(smallTime.count()){1})); + + BOOST_TEST_MESSAGE("VarNameCleaner: N=" << smallN << " took " << smallTime.count() << " us"); + BOOST_TEST_MESSAGE("VarNameCleaner: N=" << largeN << " took " << largeTime.count() << " us"); + BOOST_TEST_MESSAGE("Ratio: " << ratio << "x (expected ~4x for linear, ~16x for quadratic)"); + + BOOST_CHECK_MESSAGE(ratio < 8.0, + "VarNameCleaner appears to scale worse than linearly: " + "ratio=" << ratio << "x for " << largeN << "/" << smallN << " variables. " + "Expected <8x for O(n), got " << ratio << "x." + ); +} + +BOOST_AUTO_TEST_SUITE_END() + +} From 01086823688499d8e20a16228e08dba69e4a2d6d Mon Sep 17 00:00:00 2001 From: Mate Soos Date: Thu, 9 Apr 2026 17:03:46 +0200 Subject: [PATCH 07/10] Fixing --- libyul/optimiser/VarNameCleaner.cpp | 2 +- libyul/optimiser/VarNameCleaner.h | 2 +- test/libyul/VarNameCleanerPerf.cpp | 124 ---------------------------- 3 files changed, 2 insertions(+), 126 deletions(-) delete mode 100644 test/libyul/VarNameCleanerPerf.cpp diff --git a/libyul/optimiser/VarNameCleaner.cpp b/libyul/optimiser/VarNameCleaner.cpp index b8fb5acf6e12..d4d1476faa01 100644 --- a/libyul/optimiser/VarNameCleaner.cpp +++ b/libyul/optimiser/VarNameCleaner.cpp @@ -96,7 +96,7 @@ void VarNameCleaner::operator()(Identifier& _identifier) _identifier.name = name->second; } -YulName VarNameCleaner::findCleanName(YulName const& _name) const +YulName VarNameCleaner::findCleanName(YulName const& _name) { auto newName = stripSuffix(_name); if (!isUsedName(newName)) diff --git a/libyul/optimiser/VarNameCleaner.h b/libyul/optimiser/VarNameCleaner.h index f61426008f30..35e419b19896 100644 --- a/libyul/optimiser/VarNameCleaner.h +++ b/libyul/optimiser/VarNameCleaner.h @@ -75,7 +75,7 @@ class VarNameCleaner: public ASTModifier /// Looks out for a "clean name" the given @p name could be trimmed down to. /// @returns a trimmed down and "clean name" in case it found one, none otherwise. - YulName findCleanName(YulName const& name) const; + YulName findCleanName(YulName const& name); /// Tests whether a given name was already used within this pass /// or was set to be kept. diff --git a/test/libyul/VarNameCleanerPerf.cpp b/test/libyul/VarNameCleanerPerf.cpp deleted file mode 100644 index d78c11a2e488..000000000000 --- a/test/libyul/VarNameCleanerPerf.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/* - This file is part of solidity. - - solidity is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - solidity is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with solidity. If not, see . -*/ -// SPDX-License-Identifier: GPL-3.0 -/** - * Performance tests for VarNameCleaner to verify O(n) behaviour - * when many variables share the same stripped base name. - */ - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -using namespace solidity::langutil; - -namespace solidity::yul::test -{ - -namespace -{ - -/// Build a Yul block with @a n variables all sharing the same base name "x": -/// { let x_100 := 1 let x_200 := 2 ... let x_(n*100) := n } -/// Using large suffix gaps so stripSuffix strips them all to "x". -std::string buildManyVarsYul(size_t n) -{ - std::ostringstream src; - src << "{\n"; - for (size_t i = 1; i <= n; ++i) - src << " let x_" << (i * 100) << " := " << i << "\n"; - src << "}\n"; - return src.str(); -} - -/// Run VarNameCleaner on the given Yul source and return the elapsed time. -std::chrono::microseconds runVarNameCleaner(std::string const& _source) -{ - auto block = disambiguate(_source); - Dialect const& d = EVMDialect::strictAssemblyForEVMObjects(EVMVersion{}, std::nullopt); - std::set reserved; - NameDispenser dispenser(d, block, reserved); - OptimiserStepContext context{d, dispenser, reserved, 0}; - FunctionHoister::run(context, block); - FunctionGrouper::run(context, block); - - auto start = std::chrono::high_resolution_clock::now(); - VarNameCleaner::run(context, block); - auto end = std::chrono::high_resolution_clock::now(); - - return std::chrono::duration_cast(end - start); -} - -} - -BOOST_AUTO_TEST_SUITE(YulVarNameCleanerPerf) - -// Verify that VarNameCleaner scales linearly, not quadratically. -// We compare the runtime of N=2000 vs N=500. With the old O(n^2) code -// the ratio would be ~16x; with the fix it should be close to 4x (linear). -// We use a generous 8x threshold to avoid flaky failures. -BOOST_AUTO_TEST_CASE(linear_scaling) -{ - size_t const smallN = 500; - size_t const largeN = 2000; - - std::string smallSrc = buildManyVarsYul(smallN); - std::string largeSrc = buildManyVarsYul(largeN); - - // Warm up - runVarNameCleaner(smallSrc); - - auto smallTime = runVarNameCleaner(smallSrc); - auto largeTime = runVarNameCleaner(largeSrc); - - // With O(n) scaling and n ratio of 4x, we expect time ratio ~4x. - // With O(n^2), the ratio would be ~16x. - // Allow up to 8x to avoid flaky test failures while still catching quadratic. - double ratio = static_cast(largeTime.count()) / - static_cast(std::max(smallTime.count(), decltype(smallTime.count()){1})); - - BOOST_TEST_MESSAGE("VarNameCleaner: N=" << smallN << " took " << smallTime.count() << " us"); - BOOST_TEST_MESSAGE("VarNameCleaner: N=" << largeN << " took " << largeTime.count() << " us"); - BOOST_TEST_MESSAGE("Ratio: " << ratio << "x (expected ~4x for linear, ~16x for quadratic)"); - - BOOST_CHECK_MESSAGE(ratio < 8.0, - "VarNameCleaner appears to scale worse than linearly: " - "ratio=" << ratio << "x for " << largeN << "/" << smallN << " variables. " - "Expected <8x for O(n), got " << ratio << "x." - ); -} - -BOOST_AUTO_TEST_SUITE_END() - -} From 69b488ff5aebf013241b185e1ee573e895dd5be4 Mon Sep 17 00:00:00 2001 From: Mate Soos Date: Thu, 9 Apr 2026 17:30:13 +0200 Subject: [PATCH 08/10] Fixing --- libyul/optimiser/VarNameCleaner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libyul/optimiser/VarNameCleaner.cpp b/libyul/optimiser/VarNameCleaner.cpp index d4d1476faa01..9150d8a26474 100644 --- a/libyul/optimiser/VarNameCleaner.cpp +++ b/libyul/optimiser/VarNameCleaner.cpp @@ -104,7 +104,7 @@ YulName VarNameCleaner::findCleanName(YulName const& _name) // Use a per-base-name counter to avoid O(n²) probing when many // variables share the same stripped base name. - size_t& nextSuffix = m_nextSuffix.at(newName); + size_t& nextSuffix = m_nextSuffix[newName]; if (nextSuffix == 0) nextSuffix = 1; for (; nextSuffix < std::numeric_limits::max(); ++nextSuffix) From 2ee692d89740a0ecb0c6589a45ecbbb17390d55f Mon Sep 17 00:00:00 2001 From: "Mate Soos @ Argot" Date: Mon, 20 Apr 2026 13:57:06 +0200 Subject: [PATCH 09/10] Update libyul/optimiser/VarNameCleaner.cpp Co-authored-by: clonker <1685266+clonker@users.noreply.github.com> --- libyul/optimiser/VarNameCleaner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libyul/optimiser/VarNameCleaner.cpp b/libyul/optimiser/VarNameCleaner.cpp index 9150d8a26474..ad95e7ca4585 100644 --- a/libyul/optimiser/VarNameCleaner.cpp +++ b/libyul/optimiser/VarNameCleaner.cpp @@ -96,7 +96,7 @@ void VarNameCleaner::operator()(Identifier& _identifier) _identifier.name = name->second; } -YulName VarNameCleaner::findCleanName(YulName const& _name) +YulName VarNameCleaner::findCleanName(YulName const& _name) const { auto newName = stripSuffix(_name); if (!isUsedName(newName)) From 2a24a9c692c03653b8430dc8bf2f890e3f45826e Mon Sep 17 00:00:00 2001 From: Mate Soos Date: Mon, 20 Apr 2026 14:30:22 +0200 Subject: [PATCH 10/10] Fix const --- libyul/optimiser/VarNameCleaner.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libyul/optimiser/VarNameCleaner.h b/libyul/optimiser/VarNameCleaner.h index 35e419b19896..f61426008f30 100644 --- a/libyul/optimiser/VarNameCleaner.h +++ b/libyul/optimiser/VarNameCleaner.h @@ -75,7 +75,7 @@ class VarNameCleaner: public ASTModifier /// Looks out for a "clean name" the given @p name could be trimmed down to. /// @returns a trimmed down and "clean name" in case it found one, none otherwise. - YulName findCleanName(YulName const& name); + YulName findCleanName(YulName const& name) const; /// Tests whether a given name was already used within this pass /// or was set to be kept.