Pathological compile time with `--via-ssa-cfg --optimize-yul` on long `new C()` chains
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 21
Description
# 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()` 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 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(); } }` 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`](https://gist.github.com/msooseth/a4162d51fd0907b75b1258b62e30831c#file-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::_M_get_insert_unique_pos` |
| 2.67 % | `malloc` |
| 1.82 % | `boost::container::flat_map::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` 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`. 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::print` → `Object::toString` → `AsmPrinter` (13.4 % incl.)
followed by `YulStack::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 %) 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` 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).
Contributor guide
Assessment
This issue has not been assessed yet.