Pathological compile time with `--via-ssa-cfg --optimize-yul` on long `new C()` chains
Pathological compile time with --via-ssa-cfg --optimize-yul on long new C() chains
Summary
A ~16 KB / 850-line Solidity file with ~250 contracts where each constructor
does new C<n+1>() causes solc to spend ~43 s of CPU time compiling. The
Yul object tree mirrors the chain of new expressions, so it ends up one
long chain ~250 levels deep. Most of the time is spent in operations whose
cost scales with that depth: yul::Object::summarizeStructure, repeated Yul
pretty-printing (Object::toString → AsmPrinter → prefixLines/reindent)
and re-parsing (YulStack::reparse), plus the SSA-CFG stack solver.
Found via AFL fuzzing of solc.
Environment
- Compiler:
0.8.36-develop.2026.5.19+commit.090bc8ff(solidity submodule090bc8ff23ccebda382a87f6f3871299ad94fb96, top ofdevelopafter PR #16709 "ssa-cfg-printer"), release build. - OS:
Linux 7.0.5-arch1-1, AMD Ryzen 9 3900 (24 hw threads).
Steps to reproduce
perf record --call-graph=fp \
solc --bin --experimental --via-ssa-cfg --optimize-yul source.solRecorded run time: 42.84 s of CPU time, ~166 K cycles:P samples.
The relevant shape of the input:
contract D { function f() public { new C(); } }contract Ci { constructor() { new C<i+1>(); } }fori = 1..255, with two contracts (C40,C255) creating an additional / back-edge instance, and a few "real" contracts (C144,C173,C227) sprinkled in.- The comment at the bottom of the file (
// TypeError 7864 …) suggests the case was lifted from Solidity's own cyclic-dependency test corpus; here the chain has been extended and renamed so the cycle validator does not fire andsolcproceeds to full code generation.
Full reproducer (850 lines, 16 185 bytes):
source.sol.
Profile
Top self-cost symbols:
| Self % | Symbol |
|---|---|
| 10.26 % | __memcmp_avx2_movbe (libc) |
| 5.16 % | __memmove_avx_unaligned_erms (libc) |
| 3.79 % | yul::Object::summarizeStructure() const |
| 3.38 % | _int_free |
| 2.88 % | std::string::append |
| 2.82 % | std::_Rb_tree<std::string, …>::_M_get_insert_unique_pos |
| 2.67 % | malloc |
| 1.82 % | boost::container::flat_map<StackSlot, unsigned long…>::priv_subscript |
| 1.75 % | std::string::compare |
| 1.12 % | yul::reindent(std::string const&) |
Allocator + memmove (malloc/_int_malloc/_int_free/cfree/__memmove_*)
together account for ~17 % of CPU time, mostly attributed to
std::string operations and std::set<std::string> rebalancing on the
summarizeStructure / AsmPrinter / prefixLines paths.
Top inclusive symbols (selected):
| Incl. % | Symbol |
|---|---|
| 64.41 | CompilerStack::generateIR |
| 37.80 | yul::Object::summarizeStructure |
| 36.42 | yul::YulStack::optimize |
| 34.27 | CompilerStack::generateEVMFromIR |
| 29.52 | YulStack::analyzeParsed |
| 19.86 | YulStack::reparse |
| 18.27 | EVMObjectCompiler::run |
| 16.23 | yul::ssa::CodeTransform::run |
| 13.38 | YulStack::print / Object::toString |
| 12.89 | yul::ssa::StackLayoutGenerator::generate |
| 9.74 | yul::ssa::findOptimalTarget |
| 9.35 | yul::ssa::StackShuffler<…>::shuffle |
| 7.37 | solidity::util::prefixLines |
| 6.81 | yul::AsmPrinter::operator()(yul::Block const&) |
| 4.53 | yul::reindent |
The summarizeStructure self-recursion is visible ~50 levels deep in the
callee graph even at a 0.5 % cutoff, and CompilerStack::generateIR appears
recursively in its own caller graph — each contract's IR generation pulls in
IR for the next contract it references.
Analysis
In rough order of contribution:
Object::summarizeStructureblow-up (≈ 38 % of total). Recursive walk of the Yul object tree that, at every level, builds dotted-path strings (name + "." + subSubObj) whose length grows with depth and inserts them into astd::set<std::string>. It is invoked from several call sites (AsmAnalysis.cpp,YulStack.cpp,CompilabilityChecker.cpp,StackLimitEvader.cpp,StackCompressor.cpp) and none of them appear to cache the result, so every pass that needs the structure re-walks the whole ~250-level chain. The hot__memcmp_avx2_movbe/std::string::compare/_M_get_insert_unique_posrows are all on this path.- Repeated Yul pretty-printing and re-parsing (≈ 33 % of total).
YulStack::print→Object::toString→AsmPrinter(13.4 % incl.) followed byYulStack::reparse→ObjectParser::parseObject(19.9 % incl.) happen inside IR generation. The full nested object — which includes the bytecode/object for every contract reachable in the chain — is serialised and re-parsed.prefixLines(7.4 %) andreindent(4.5 %) are responsible for most of the__memmove_avx_unaligned_ermsself time. - SSA-CFG stack layout / codegen (~16 % incl. for
CodeTransform::run, ~13 % forStackLayoutGenerator).findOptimalTarget+StackShuffler<*>::shuffletogether are ~19 %; theflat_map<StackSlot, …>subscript is 1.8 % self. Per-function cost is non-trivial and total cost grows with the number of contracts assembled together.
The --via-ssa-cfg --optimize-yul combination makes both the SSA-CFG backend
and the Yul optimizer active; the captured trace is dominated by (1) and (2).
Source: argotorg/solidity