argotorg / argotorg/solidity

Pathological compile time (and ~2 GB peak RSS without optimizer) under `--via-ir` on long `++x+x+x+x+…` arithmetic chain (`--via-ssa-cfg` also affected)

Open
#16,747 1 comment 0 reactions 0 assignees View on GitHub
high effort medium impact must have eventually performance :racehorse:
Dominant language
C++
Stars
25.7k
Forks
6.2k
Avg merge
1d 11h
Merged PRs (30d)
21

Description

## Summary

A ~6.6 KB / 14-line Solidity file with a single function whose return expression is one ~3000-term `+` chain (mostly bare `x` operands, with 41 mid-chain `++x` pre-increments) causes `solc`'s via-IR pipeline to consume the full 12 s budget *without* the optimizer and ~9–10 s *with* the optimizer (also under `--experimental --via-ssa-cfg`). The legacy backend compiles the same file in 82 ms (no `--optimize`) / 302 ms (`--optimize`).

Without the Yul optimizer, peak RSS reaches **~1.87 GB** (vs ~32–37 MB on legacy and ~180 MB on the optimized via-IR / ssa-cfg paths). The two pipelines hit different hot paths:

- `--via-ir` (no optimizer): dominated by `StackCompressor::run` → `StackLayoutGenerator::reportStackTooDeep` → `findStackTooDeep`, with `Multiplicity::operator[]` at 39 % self.
- `--via-ir --optimize` and `--via-ssa-cfg --optimize`: dominated by `DataFlowAnalyzer::clearValues` (~48–50 % self), `CommonSubexpressionEliminator::run`, and `LiteralRematerialiser::run`.

Found via AFL fuzzing of `solc`. This is one of 6 corroborating instances in the same campaign sharing the same source shape (single line, ~3000-term `+` chain).

## Environment

- Compiler: `0.8.36-develop.2026.5.18+commit.090bc8ff` (solidity submodule at `090bc8ff2`, top of `develop` after PR #16709), Release build.
- OS: `Linux 7.0.5-arch1-1`, AMD Ryzen 9 3900.

## Steps to reproduce

Reproducer (6591 bytes, 14 lines):
[`source.sol`](https://gist.github.com/msooseth/786a7a65b47d3537cab20b647fd48093).

Shape:

```solidity
contract C layout at 2**256 - 2 {
uint public x;
function f(uint a) public returns (uint) {
++x+x+x+x+...+x; // ~3000-term '+' chain on one line, 41 mid-chain '++x'
++x;
--x;
x++;
x--;
return x;
}
}
```

Line 4 of the file contains the long chain: ~3000 `+` operators / `x` operands, plus 41 `+++` substrings (i.e. mid-chain `++x` pre-increments). Total line length: 6370 characters.

```bash
# Times measured with a 12 s wall-clock cap per invocation:
solc --bin --evm-version osaka --via-ir source.sol # >12 s (timeout), peak RSS 1868 MB
solc --bin --evm-version osaka --via-ir --optimize source.sol # 9277 ms, peak RSS 179 MB
solc --bin --evm-version osaka source.sol # 82 ms, peak RSS 32 MB
solc --bin --evm-version osaka --optimize source.sol # 302 ms, peak RSS 37 MB
solc --bin --evm-version osaka --via-ir --optimize --experimental --via-ssa-cfg source.sol # 9782 ms, peak RSS 179 MB
```

## Profile — `solc --bin --via-ir source.sol` (no `--optimize`, truncated at 12 s, ~1.87 GB peak)

| Incl. % | Self % | Symbol |
|---|---|---|
| 99.00 | 0.00 | `frontend::CompilerStack::generateIR` |
| 97.65 | 0.00 | `yul::ObjectOptimizer::optimize` |
| 97.46 | 0.00 | `yul::OptimiserSuite::run` |
| **97.31** | 0.00 | **`yul::StackCompressor::run`** |
| 97.09 | 0.00 | `yul::StackLayoutGenerator::reportStackTooDeep` |
| 97.08 | 0.01 | `yul::StackLayoutGenerator::processEntryPoint` |
| **84.52** | 1.63 | **`(anonymous namespace)::findStackTooDeep`** |
| **51.44** | **38.97** | **`yul::Multiplicity::operator[]`** |
| 11.63 | 0.22 | `StackLayoutGenerator::propagateStackThroughOperation` |
| 11.56 | 0.03 | `yul::Multiplicity::~Multiplicity` |
| 8.67 | 0.23 | `yul::Shuffler<…findStackTooDeep…>` |
| 8.58 | 8.58 | `yul::Multiplicity::at` |
| 4.82 | 4.82 | `std::_Rb_tree_insert_and_rebalance` |
| 4.02 | 2.23 | `yul::createStackLayout<…findStackTooDeep…>` |

## Profile — `solc --bin --via-ir --optimize source.sol` (9.3 s, ~179 MB peak)

| Incl. % | Self % | Symbol |
|---|---|---|
| 93.97 | 0.00 | `CompilerStack::generateIR` |
| 89.37 | 0.00 | `yul::YulStack::optimize` |
| 82.66 | 0.00 | `OptimiserSuite::runSequence` |
| 72.93 | 0.22 | `ASTModifier::operator()(yul::Block&)` |
| 71.61 | 0.00 | `DataFlowAnalyzer::operator()(yul::Block&)` |
| 56.18 | 0.14 | `DataFlowAnalyzer::handleAssignment` |
| 54.91 | 0.00 | `DataFlowAnalyzer::operator()(yul::FunctionDefinition&)` |
| 51.20 | 0.01 | `DataFlowAnalyzer::operator()(yul::Assignment&)` |
| **47.94** | **47.73** | **`yul::DataFlowAnalyzer::clearValues`** |
| **38.23** | 0.00 | **`yul::CommonSubexpressionEliminator::run`** |
| 29.45 | 0.00 | `yul::LiteralRematerialiser::run` |
| 16.49 | 0.00 | `DataFlowAnalyzer::operator()(yul::If&)` |
| 15.84 | 0.51 | `DataFlowAnalyzer::operator()(yul::VariableDeclaration&)` |
| 5.87 | 0.00 | `Object::toString` / `AsmPrinter::operator()` |
| 3.48 | 0.00 | `yul::YulStack::reparse` |
| 2.45 | 0.00 | `StackLayoutGenerator::reportStackTooDeep` (small at this point) |

The `--via-ssa-cfg --optimize` profile is essentially identical to `--via-ir --optimize` (`DataFlowAnalyzer::clearValues` ~49.7 % self, `CommonSubexpressionEliminator::run` ~38.6 %, `LiteralRematerialiser::run` ~29.9 %).

## Notes / possibly related

- #16745 — long `new C(N+1)()` constructor chain; same config fingerprint (`--via-ir` and `--via-ssa-cfg`) but different source shape (~250 contracts, ~16 KB) and a different hot path (`Object::summarizeStructure` / `AsmPrinter`). Filed separately.
- #16746 — long `((super).f)() | this.f() | …` diamond chain; `--via-ir` is fine *without* the optimizer there (1 s) but hangs *with* the optimizer; here `--via-ir` hangs even without `--optimize`, and the peak-RSS blowup (~1.87 GB) is unique to NEW-A.
- #14885 — `StackCompressor` infinite loop on assembly input; same component is hot here but the trigger is a long Solidity expression rather than handwritten assembly.

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the timings and memory use with source.sol under the listed --via-ir and --via-ssa-cfg commands. Profile CompilerStack::generateIR, then inspect StackCompressor::run and StackLayoutGenerator::reportStackTooDeep for the unoptimized case, and DataFlowAnalyzer::clearValues, CommonSubexpressionEliminator::run, and LiteralRematerialiser::run for optimized cases. Done means the long-chain reproducer no longer exhibits the reported timeout or memory blowup.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, solidity
Domain
compilers, performance
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.