Pathological compile time under `--via-ir` (and `--via-ssa-cfg`) on `L.add(sum = L.add(…), …)` library-call chains
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 29
Description
## Summary
A 5713-byte / 47-line Solidity file containing a single constructor expression that nests 282 `L1.add(sum = L1.add(…), 2)` external-library calls (with intermediate `sum = …` assignments interleaved inside the call arguments) causes `solc`'s via-IR pipeline to spend **3.6 s** without the optimizer / **4.9 s** with the optimizer / **7.0 s** under `--experimental --via-ssa-cfg --optimize`. The legacy backend compiles the same file in **60 ms** (no `--optimize`) / **280 ms** (`--optimize`) — i.e. via-IR is ~60–115× slower than legacy on this input.
The SSA-CFG path is the slowest, and shows a unique hotspot in `ssa::InstructionStore::appendInst` → `std::vector::reserve` (23.6 % self) that is *not* present in the plain `--via-ir --optimize` profile — looks like quadratic vector growth as the SSA builder appends instructions for the long nested-assignment chain. The optimizer pass (`DataFlowAnalyzer::clearValues`, 32.8 % self) is the other dominant cost and is shared with the other long-chain via-IR slow cases.
Found via AFL fuzzing of `solc`. One of 5 corroborating instances in the same campaign sharing the same source shape (deeply nested `L.add(sum = L.add(…))` chain). For this representative file, the AFL run was the *only* case in the dataset where ssaCFG hit the 12 s budget while every other config (legacy ± `--optimize`, `--via-ir` ± `--optimize`) finished cleanly — i.e. the purest SSA-CFG-only signal in the run. The other 4 corroborating cases also hang `--via-ir` (no `--optimize`) and ssaCFG, with legacy and `--via-ir --optimize` completing.
## Environment
- Compiler: `0.8.36-develop.2026.5.18+commit.090bc8ff`, Release build (solidity submodule at `090bc8ff`, top of `develop`).
- OS: `Linux 7.0.5-arch1-1`, AMD Ryzen 9 3900.
## Steps to reproduce
Reproducer (5713 bytes, 47 lines, 282 nested `L1.add(sum = L1.add(…))` calls + 8 interleaved `L2.add(1, …)` calls):
[`source.sol`](https://gist.github.com/msooseth/0dbf2786240cb8557816c22035d9efac).
Shape:
```solidity
library L1 { function add(uint256 a, uint256 b) external pure returns (uint256) { return 1; } }
library L2 { function add(uint256 a, uint256 b) external pure returns (uint256) { return a + b + 2; } }
contract A {
uint256 sum;
constructor() {
// 282 nested L1.add(sum = L1.add(...)) calls, interleaved with 8 L2.add(1, ...)
// on one ~5.6 KB line
sum = L1.add(sum = L1.add(sum = L1.add( ... sum = L1.add(1, 2) ..., 2), 2), 2);
}
function getSum() external view returns(uint256) { return sum; }
}
contract B { uint256 sum; constructor() { /* tiny */ } function getSum() external view returns(uint256) { return sum; } }
contract C { A a = new A(); B b = new B(); function aSum() external view returns(uint256) { return a.getSum(); } function bSum() external view returns(uint256) { return b.getSum(); } }
```
```bash
# 15 s wall-clock cap per invocation:
solc --bin --evm-version osaka source.sol # 0.06 s, peak RSS 28 MB
solc --bin --evm-version osaka --optimize source.sol # 0.28 s, peak RSS 33 MB
solc --bin --evm-version osaka --via-ir source.sol # 3.62 s, peak RSS 140 MB
solc --bin --evm-version osaka --via-ir --optimize source.sol # 4.94 s, peak RSS 89 MB
solc --bin --evm-version osaka --via-ir --optimize --experimental --via-ssa-cfg source.sol # 6.95 s, peak RSS 88 MB
```
## Profile — `--via-ir --optimize --experimental --via-ssa-cfg` (6.95 s, 88 MB peak)
| Incl. % | Self % | Symbol |
|---|---|---|
| 76.33 | 0.00 | `frontend::CompilerStack::compile` |
| 59.46 | 0.00 | `CompilerStack::generateIR` |
| 57.04 | 0.00 | `yul::YulStack::optimize` |
| 54.81 | 0.00 | `yul::ObjectOptimizer::optimize` |
| 50.55 | 0.00 | `OptimiserSuite::runSequence` |
| 41.97 | 0.03 | `DataFlowAnalyzer::operator()(yul::Block&)` |
| **35.28** | **32.84** | **`yul::DataFlowAnalyzer::clearValues`** |
| 23.78 | 0.07 | `yul::ssa::InstructionStore::appendInst` |
| **23.71** | **23.57** | **`std::vector::reserve`** |
| 20.79 | 0.00 | `ssa::SSACFGBuilder::readVariableRecursive` |
| 19.95 | 0.01 | `ssa::SSACFGBuilder::addPhiOperands` |
| 19.75 | 0.00 | `CompilerStack::generateEVMFromIR` |
| 18.85 | 0.15 | `DataFlowAnalyzer::handleAssignment` |
| 15.50 | 0.00 | `CommonSubexpressionEliminator::run` |
| 13.15 | 0.00 | `LiteralRematerialiser::run` |
| 11.67 | 0.00 | `ssa::SSACFGBuilder::emitUpsilon` |
| 9.84 | 0.01 | `ssa::SSACFGBuilder::build` |
| 9.28 | 0.00 | `ssa::SSACFGBuilder::visitFunctionCall` |
Two dominant cost centres:
1. **Optimizer DataFlowAnalyzer** (~42 % incl) — `clearValues` running per-block over a long chain of `sum = …` assignments. The same hotspot dominates the `--via-ir --optimize` (non-SSA-CFG) run too.
2. **SSA-CFG construction** (~24 % incl) — `InstructionStore::appendInst` repeatedly calling `std::vector::reserve` during SSA construction; self time of the `reserve` call alone is 23.57 %, which looks like quadratic vector growth as the SSA builder appends each instruction of the long chain.
The second pattern is **unique to the SSA-CFG path** — the plain `--via-ir --optimize` run (4.9 s) does not show `InstructionStore::reserve` in its profile at all; instead its budget goes proportionally more into `DataFlowAnalyzer::clearValues` and `CommonSubexpressionEliminator::run`.
## Notes / possibly related
- **#16704** (`findStackTooDeep` aborts on long chained library-call expressions): related source family (chained library calls), distinct failure mode. #16704 needs ≥998 straight chained `.double()` calls and crashes with a YulAssertion in the legacy stack shuffler at the 1000-iteration cap; this report uses only 282 calls but adds interleaved `sum = …` assignments and hits a *slow-path*, not an assertion. The shapes are different enough that the two likely have different root causes.
- **#16747** (long `++x+x+x+…` arithmetic chain): shares the `DataFlowAnalyzer::clearValues` hotspot but the chain is bare arithmetic (no external calls, no intermediate assignments) and `--via-ir` (no `--optimize`) is much slower there (>12 s vs 3.6 s here). This report's defining feature is the external-library-call + nested `sum = …` combination, which also activates the SSA-CFG `InstructionStore::reserve` path that #16747 doesn't.
- **#14885** (`StackCompressor` infinite loop on inline assembly): same broad family (long dependent-assignment chain stresses optimizer), but assembly input rather than a Solidity expression.
Contributor guide
Assessment
This issue has not been assessed yet.