#16745·solidity

Pathological compile time with `--via-ssa-cfg --optimize-yul` on long `new C()` chains

Author: msoosethCreated May 20, 2026Updated Sep 14, 2026

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::toStringAsmPrinterprefixLines/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 submodule 090bc8ff23ccebda382a87f6f3871299ad94fb96, top of develop after PR #16709 "ssa-cfg-printer"), release build.
  • OS: Linux 7.0.5-arch1-1, AMD Ryzen 9 3900 (24 hw threads).

Steps to reproduce

bash
perf record --call-graph=fp \
    solc --bin --experimental --via-ssa-cfg --optimize-yul source.sol

Recorded 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>(); } } for i = 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 and solc proceeds 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:

  1. Object::summarizeStructure blow-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 a std::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_pos rows are all on this path.
  2. Repeated Yul pretty-printing and re-parsing (≈ 33 % of total). YulStack::printObject::toStringAsmPrinter (13.4 % incl.) followed by YulStack::reparseObjectParser::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 %) and reindent (4.5 %) are responsible for most of the __memmove_avx_unaligned_erms self time.
  3. SSA-CFG stack layout / codegen (~16 % incl. for CodeTransform::run, ~13 % for StackLayoutGenerator). findOptimalTarget + StackShuffler<*>::shuffle together are ~19 %; the flat_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).