Skip to content

Eliminate redundant associative container lookups across the compiler#16606

Open
k06a wants to merge 2 commits intoargotorg:developfrom
k06a:feature/optimize-set-and-map-usage
Open

Eliminate redundant associative container lookups across the compiler#16606
k06a wants to merge 2 commits intoargotorg:developfrom
k06a:feature/optimize-set-and-map-usage

Conversation

@k06a
Copy link
Copy Markdown

@k06a k06a commented Apr 16, 2026

Summary

Bottom-up optimization of compiler internals. No compilation logic, code generation, or optimizer behavior changes. Bytecode is bit-for-bit identical to the baseline (verified with --metadata-hash none).

Commit 1 — Eliminate redundant associative container lookups

  • Replace ~40 double-lookup patterns (count/find + at/operator[]) with single find + iterator reuse, try_emplace, or insert().second across 42 files in libevmasm, libyul, libsolidity, libsolutil.
  • Switch membership-only std::set / std::map to std::unordered_set / std::unordered_map in 8 files where iteration order is never used (MultiUseYulFunctionCollector, FullInliner, InlinableExpressionFunctionFinder, EVMCodeTransform, AsmAnalysis, PathGasMeter, AST.cpp, TypeSystem.h).
  • Use std::erase_if (C++20) in KnownState.cpp.
  • Add section 13 "Associative Containers" to CODING_STYLE.md codifying these best practices.

Commit 2 — Convert hot YulName/FunctionHandle-keyed containers in the Yul optimiser pipeline

Extends the same refactor into the Yul optimiser's hot path, specifically the data-flow / semantics / control-flow analysis layers:

  • Infrastructure: add std::hash<BuiltinHandle> (in Builtins.h) and std::hash<FunctionHandle> (std::variant<YulName, BuiltinHandle>). Route Builtins.h through ASTForward.h so the specialisation is visible wherever FunctionHandle is used.
  • DataFlowAnalyzer: State::value, Environment::keccak (with a custom YulNamePairHash using boost::hash_combine), Scope::variables, m_functionSideEffects → unordered.
  • KnowledgeBase: m_offsets, m_lastKnownValue → unordered. m_groupMembers left as std::map<YulName, std::set<YulName>> on purpose — *group->begin() picks the minimum representative, which must stay deterministic.
  • Semantics / side-effect maps: std::map<FunctionHandle, SideEffects>std::unordered_map end-to-end (SideEffectsPropagator, SideEffectsCollector, MovableChecker, DataFlowAnalyzer, CommonSubexpressionEliminator, EqualStoreEliminator, LoadResolver, UnusedStoreEliminator, UnusedPruner, LoopInvariantCodeMotion, plus FunctionSideEffects test). Same for std::map<YulName, ControlFlowSideEffects> in ControlFlowSideEffectsCollector::functionSideEffectsNamed() and all its consumers.
  • Reference counters: ReferencesCounter::countReferences() and VariableReferencesCounter::countReferences() now return std::unordered_map; all call sites updated.
  • Translation / substitution maps: Substitution, FunctionCopier::m_translations, NameSimplifier::m_translations, VarNameCleaner (m_namesToKeep, m_usedNames, m_translatedNames), SyntacticalEquality::m_identifiers{LHS,RHS}, ExpressionJoiner::m_references, EquivalentFunction{Combiner,Detector}::m_duplicates, Disambiguator::m_translations, AssignmentCounter::m_assignmentCounters, Rematerialiser (m_referenceCounts, m_varsToAlwaysRematerialize), SSAValueTracker::ssaVariables() → unordered.
  • String-keyed and local containers: EVMDialect::m_reservedstd::unordered_set with a transparent hasher (heterogeneous string_view lookup preserved), Object::{objectPaths, dataPaths, subIndexByName}, CompilerContext::m_externallyUsedYulFunctions, Assembly::m_namedTags → unordered. Local std::set<YulName> in CodeTransform::assignedFunctionNames and LoopInvariantCodeMotion::{ssaVars, varsDefinedInScope} → unordered.
  • Double lookups: fix the remaining count()+at() patterns in ControlFlowSideEffectsCollector, ObjectOptimizer, Semantics::containsNonContinuingFunctionCall, Assembly::namedTag, NameSimplifier::findSimplification.

Containers that require ordered iteration (NameCollector::m_names, AssignmentsSinceContinue::m_names, assignedVariableNames(), KnowledgeBase::m_groupMembers inner set, IRGenerationContext::m_usedSourceNames, NameDispenser::usedNames() public API, OptimiserStepContext::reservedIdentifiers, OptimiserSuite::run(..., _externallyUsedIdentifiers)) are deliberately left untouched.

Motivation

Profiling via-IR compilation of OpenZeppelin 5.0.2 shows YulString::operator< at ~2.6% of samples and std::__tree operations on YulName/FunctionHandle at ~4.4% combined — pure overhead from using ordered containers for keys whose iteration order never mattered. Each lookup is O(log n) with O(key length) comparisons; unordered variants make it amortized O(1) with a single hash of the 64-bit handle.

These are mechanical, behavior-preserving changes — the compiler works exactly the same way, it just uses its data structures more efficiently.

Benchmark (via-IR, --optimize, 10 runs each, first run discarded as cold, median of 9)

Project develop this PR Change
OpenZeppelin 5.0.2 11.62 s 10.64 s −8.43%
OpenZeppelin 4.9.0 15.83 s 14.40 s −9.03%
Uniswap V4 (2022) 5.83 s 5.33 s −8.58%

(Absolute times are lower than in the previous revision because this run was done on a cooler machine — the relative deltas are what matters. Compared to v1 of this PR, the second commit adds roughly -1.7 p.p. on OZ 5.0.2 and -7 p.p. on Uniswap V4 / OZ 4.9.0 — the new optimizations target DataFlowAnalyzer / KnowledgeBase / Semantics, which are on the hot path of every via-IR compilation rather than project-specific hot paths.)

Run-to-run spread is also tight (≤ 0.5 s on all three projects), consistent with eliminating cache-miss-heavy tree traversals.

Test plan

  • Bytecode identical (verified with --metadata-hash none on several OpenZeppelin contracts)
  • Build succeeds with no warnings (-Werror enabled)
  • Existing test suite passes (7916 tests, 0 errors)

@github-actions
Copy link
Copy Markdown

Thank you for your contribution to the Solidity compiler! A team member will follow up shortly.

If you haven't read our contributing guidelines and our review checklist before, please do it now, this makes the reviewing process and accepting your contribution smoother.

If you have any questions or need our help, feel free to post them in the PR or talk to us directly on the #solidity-dev channel on Matrix.

…ser pipeline from std::map/set to std::unordered_map/set
Copy link
Copy Markdown
Member

@clonker clonker left a comment

Choose a reason for hiding this comment

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

Thanks! Very cool, this brings indeed some nice performance gains:

Benchmark           Pipeline  Metric         Base             Target           Delta  
------------------  --------  -------------  ---------------  ---------------  -------
openzeppelin-5.6.1  evmasm    bytecode_size  746,436          746,682          +0.03% 
                              cpu_time       9.41s            8.89s            -5.6%  
                              cycles         51,343,509,758   48,467,228,284   -5.6%  
                              instructions   139,638,007,045  136,854,559,330  -1.99% 
                              peak_rss       1027 MiB         1028 MiB         +0.09% 
                              wall_time      9.46s            8.93s            -5.61% 
                                                                                      
openzeppelin-5.6.1  ir        bytecode_size  696,529          696,775          +0.04% 
                              cpu_time       28.20s           24.38s           -13.54%
                              cycles         155,040,244,671  134,366,041,115  -13.33%
                              instructions   319,299,139,415  301,667,743,203  -5.52% 
                              peak_rss       1530 MiB         1531 MiB         +0.1%  
                              wall_time      28.36s           24.50s           -13.61%

Note the bytecode size increase is just metadata. Beyond the results are bytecode-identical.

Beyond that, please go through the changes and check that

  • includes are fine (by changing the containers some set and map includes might have gone stale. i think i found some of them but maybe not all)
  • you have applied const where possible. many of the iterators (or tuple of iterator and bool whether something was emplaced) can be consted. clang-tidy can help you here.
  • if-with-initializer is formatted differently. it does introduce a bit of churn but increases readability and makes the set of changes more uniform with the rest of the codebase
  • please restore the rationale comments that the refactors removed (e.g. on storeInStorage/storeInMemory short-circuits)
  • you can probably get away with a few of these not changing their container type. especially the ones that have a low amount of elements and/or are in cold codepaths are perhaps better off if they are left as map/set. the biggest wins are presumably from optimizer-internal containers that are iterated over and/or looked up per (subset of) AST node. that should shrink the diff significantly while maintaining the reported performance figures.


int CSECodeGenerator::classElementPosition(Id _id) const
{
auto it = m_classPositions.find(_id);
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
auto it = m_classPositions.find(_id);
auto const it = m_classPositions.find(_id);

case BasicBlock::EndType::JUMPI:
case BasicBlock::EndType::HANDOVER:
{
auto it = blockByBeginPos.find(block.end);
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
auto it = blockByBeginPos.find(block.end);
auto const it = blockByBeginPos.find(block.end);

continue;
BlockId nextId(push.data());
if (m_blocks.count(nextId) && m_blocks.at(nextId).prev)
auto nextIt = m_blocks.find(nextId);
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
auto nextIt = m_blocks.find(nextId);
auto const nextIt = m_blocks.find(nextId);

//@todo we might have to do something like incrementing the sequence number for each JUMPDEST
assertThrow(!!item.blockId, OptimizerException, "");
if (!m_blocks.count(item.blockId))
auto blockIt = m_blocks.find(item.blockId);
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
auto blockIt = m_blocks.find(item.blockId);
auto const blockIt = m_blocks.find(item.blockId);

Comment thread libevmasm/KnownState.cpp
int stackDiff = m_stackHeight - _other.m_stackHeight;
for (auto it = m_stackElements.begin(); it != m_stackElements.end();)
if (_other.m_stackElements.count(it->first - stackDiff))
if (auto otherIt = _other.m_stackElements.find(it->first - stackDiff); otherIt != _other.m_stackElements.end())
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
if (auto otherIt = _other.m_stackElements.find(it->first - stackDiff); otherIt != _other.m_stackElements.end())
if (
auto otherIt = _other.m_stackElements.find(it->first - stackDiff);
otherIt != _other.m_stackElements.end()
)

Comment thread libyul/ASTForward.h

#pragma once

#include <libyul/Builtins.h>
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.

I don't love this, this pulls in a bunch of stuff which, imo, has no place in a forwarding header (esp YulName.h). From my point of view it would be better to move FunctionHandle (and its hash specialization) out of this header into its own thing that is only included where needed. The AST itself doesn't require it. That leaves it with the BuiltinHandle POD in here and you don't have to drag the whole YulName machinery into ASTForward.

Comment thread libyul/Builtins.h
}
};

template<> struct hash<std::variant<solidity::yul::YulName, solidity::yul::BuiltinHandle>>
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.

Ideally, this would be hash<FunctionHandle> and together with the typedef, see https://github.com/argotorg/solidity/pull/16606/changes#r3195152704

Comment thread libyul/Object.h

std::vector<std::shared_ptr<ObjectNode>> subObjects;
std::map<std::string, size_t, std::less<>> subIndexByName;
std::unordered_map<std::string, size_t> subIndexByName;
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.

This loses the transparent comparator, perhaps you can do something here akin to the change in EVMDialect. Anyways these members should be quite cold, I don't expect any performance win from even touching them.

Comment thread CODING_STYLE.md
Comment on lines +239 to +240
if (auto it = m_map.find(key); it != m_map.end())
use(it->second);
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
if (auto it = m_map.find(key); it != m_map.end())
use(it->second);
if (
auto const it = m_map.find(key);
it != m_map.end()
)
use(it->second);

Comment thread CODING_STYLE.md
Comment on lines +253 to +254
if (auto [it, inserted] = m_set.insert(value); inserted)
onNewElement();
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
if (auto [it, inserted] = m_set.insert(value); inserted)
onNewElement();
if (
auto const [it, inserted] = m_set.insert(value);
inserted
)
onNewElement();

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants