Blog
Fifteen openXC7 builds with stated boundaries: for small designs more time goes to turning FASM into a bitstream than to place-and-route, a blinky beaten by a GF multiplier, and 25% spread on byte-identical work.
Everybody has an impression of how long the open Xilinx toolchain takes. I could not find a published number with a method attached — and the numbers in our own repository turned out to be estimates rather than measurements: three different designs recorded as taking identical time, which is how you can tell nobody ran a stopwatch. So we ran one.
The boundaries are the whole point, because they are where this kind of comparison usually cheats.
| Measured | Not measured |
|---|---|
| yosys synthesis | docker pull |
| nextpnr-xilinx place and route | git checkout |
| fasm2frames + xc7frames2bit | chipdb generation |
The chipdb is built once in its own job and handed to every measurement job as an artifact, so it is identical across runs and sits outside the timed window. It is a property of the part rather than of the design, and Vivado has no equivalent step — timing it would flatter neither tool honestly.
Five runs per design, seed pinned at 1. Machine: a shared GitHub ubuntu-latest runner, 4 cores. Part: xc7a200tfbg484-2.
| design | median | min | max | synth | p&r | bitstream |
|---|---|---|---|---|---|---|
| gf12_mul | 46.1s | 37.6s | 48.2s | 9.8s | 6.7s | 29.4s |
| blinky | 60.7s | 54.6s | 69.6s | 3.5s | 28.9s | 28.2s |
| gf128_mul | 533.2s | 521.5s | 662.6s | 163.1s | 253.7s | 122.3s |
A blinking LED took 60.7 s. A GF(2^12) multiplier took 46.1 s. The multiplier is the larger design by any measure, and it finished a quarter faster.
The reason is not size. blinky is placed with the simulated-annealing placer against a 100 MHz constraint; the multiplier uses the analytic placer at 5 MHz. Place-and-route for blinky is 28.9 s against the multiplier’s 6.7 s — a factor of four, in the opposite direction to the design sizes. At this scale the placer and the frequency target decide the build time and the netlist barely participates.
That is worth knowing before quoting anybody’s blinky number, including ours.
Bitstream generation — fasm2frames followed by xc7frames2bit — took 28.2 s for blinky and 29.4 s for the multiplier. That is 46% and 64% of their total build time, and it is close to constant: the two designs differ in every other respect and their bitstream stage differs by one second.
It behaves like a floor. For anything small, more time goes into turning FASM into frames and frames into a .bit than into placing and routing the design. Optimisation effort in this toolchain goes overwhelmingly to the placer and the router, which is where the interesting algorithms are; on small designs that is not where the seconds are.
The floor stops dominating once the design is big enough. At gf128_mul the bitstream stage is 122.3 s of 533.2 s — 23% — while place-and-route takes 253.7 s and synthesis 163.1 s. Both of those grow with the netlist; the bitstream stage grows much more slowly.
Every run of a given design did byte-identical work: same sources, same constraints, same seed, same container image. The spread was still this:
| design | min → max | spread vs median |
|---|---|---|
| blinky | 54.6s → 69.6s | 25% |
| gf12_mul | 37.6s → 48.2s | 23% |
| gf128_mul | 521.5s → 662.6s | 26% |
About a quarter, consistently, across three designs spanning an elevenfold range in runtime. That is the machine — shared runners with noisy neighbours — and not the toolchain, because the toolchain was handed the same input every time.
The practical consequence: a build time from one run can sit a quarter away from the median, and nothing in that run tells you it does. Anyone publishing a single number for either toolchain is publishing a sample from a distribution they did not look at. That includes the number we would have published if we had run this once.
The first attempt lost all five jobs of one design at synthesis: its wrapper instantiates a decoder core that lives in a git submodule, and the job checked out without submodules. My error, and local to one design — the other two produced their measurements.
The fix was worse than the fault. Adding submodules to the checkout made that step itself fail, so the second run lost all fifteen jobs, including the two designs that had never needed a submodule and had passed cleanly an hour earlier. That submodule is not reachable from CI at all. The decoders left the set and a self-contained GF(2^128) multiplier took the slot, which is also why the size range in the table is as wide as it is.
The third mistake never reached a run. The workflow header said the five repeats were there because nextpnr is seed-sensitive. The seed is pinned, so that is not what the repeats measure — they measure the machine. Had it stood, this post would have opened with a claim about the placer that the data does not support.
A Vivado column, run on the same three designs with the same boundaries, on a machine described as precisely as this one. We cannot produce it. The designs are ordinary Verilog and XDC and the workflow is public, so anyone who can run Vivado can add the other half — and if the two halves disagree in a way neither side can reproduce, that is the interesting outcome rather than a failure.
Every figure above is measured, and the limits are named with it.