Learning
Všechny články
Engineering14. září 2026 6 min

Rychlost buildu je topologie, ne volba jazyka

Pětinásobné zrychlení Bunu měřilo granularitu překladu, ne Rust proti Zigu

Pětinásobný rozdíl v buildu neměřil jazyk, měřil tvar projektu

Bun se ze Zigu přepsal do Rustu a build na Linuxu spadl z 30m06s na 5m37s. Z toho se dá snadno udělat věta o tom, že Rust překládá rychleji než Zig. Ta věta je nepravdivá a měření, které ji vyvrací, je poučnější než samotné číslo.

Klíč je v tom, co se vlastně srovnávalo. Dva buildy se nelišily jazykem, lišily se počtem jednotek, na které šlo práci rozdělit, a tím, kolik z ní se odkládalo na linker.


Profil, který vidí celý strom procesů

Existující nástroje vidí vždycky jen část. ninjatracing umí z Ninja logu udělat timeline toho, co běželo paralelně, ale build Bunu prochází několika vrstvami a Ninja je jen jednou z nich. Co chybělo, byl profil, který zachytí každý proces, který build spustí, včetně jejich podprocesů.

Nástroj buildprof to řeší přes ptrace. Sleduje strom procesů, zaznamenává fork, exec a exit, a přes seccomp filtry navíc odchytává souborové operace, takže je vidět i závislost mezi kroky. Výsledek se vykresluje v Perfettu jako jedna souvislá timeline.

Tenhle přístup je přenositelný mimo Bun i mimo Rust. Když nerozumíš tomu, kde build tráví čas, obvykle je to proto, že se díváš na log jednoho nástroje, zatímco čas hoří o tři vrstvy níž.


Linker běžel šestnáct minut úplně sám

První pohled na timeline ukázal jedinou dlouhou čáru. Volání ld.lld běželo na konci osamoceně přes šestnáct minut, tedy zhruba dvě třetiny celého buildu. Všechna jádra kromě jednoho stála.

Příčinou byl Full LTO. Ten slepí všechny překladové jednotky do jedné obří optimalizační úlohy, kterou nelze paralelizovat. Rust build používal ThinLTO, který jednotky drží oddělené a optimalizuje je souběžně. To není vlastnost jazyka, je to jeden přepínač.

Přepnutí Zig buildu na ThinLTO srazilo link na 12m55s. Zbytek držela závislost: Bun si stahoval předkompilovaný WebKit, sestavený s Full LTO. Po jeho přestavbě s ThinLTO klesl link na 7m22s a celek na 15m11s. Strategie LTO tvé závislosti je součástí doby tvého buildu, i když ji nepřekládáš.


Zbytek rozdílu je granularita, ne kompilátor

Ani po obou opravách se Zig build nedotáhl. Zůstal skoro 3× pomalejší a důvod byl strukturální. Rust verze rozprostřela překlad přes více než devadesát crate, zatímco Zig verze hnala všechno jedním modulem.

Na timeline je to vidět doslova. Cargo se rozvětví do desítek pojmenovaných procesů rustc, vedle toho stojí jediný zig build-obj, který nespouští nic. Jeden proces znamená jedno jádro a žádnou inkrementalitu: změna jednoho řádku překládá všechno znovu.

Rozdělení na crate tedy nebylo důsledkem přechodu na Rust, byl to nezávislý refaktoring, který se k němu přibalil. Kdyby zůstal Zig a rozpadl se do modulů, většina zrychlení by přišla taky.


Co si z toho odnést

Když někde uvidíš benchmark, který srovnává dvě technologie, hledej nejdřív, co všechno se kromě té technologie změnilo. Tady se současně změnil jazyk, nastavení LTO, build závislosti a dekompozice kódu. Publikované číslo bylo reprodukovatelné a přitom mělo jinou příčinu, než jakou mu lidé připisovali.

Pro vlastní build z toho plyne pořadí kroků. Nejdřív si pořiď profil, který vidí všechny procesy, ne jen ty, které pouští tvůj build systém. Pak se podívej, jestli někde na konci nestojí jediný dlouhý proces, protože to je skoro vždycky linker s Full LTO. Teprve potom řeš granularitu překladových jednotek, protože ta je nejdražší na změnu a zároveň určuje strop paralelizace i inkrementálních buildů.

A drž se toho, že sériový úsek je důležitější než celkový čas. Šestnáct minut na jednom jádře nezachrání ani stroj se sto jádry.

Zdroje

Poslech místo čtení — 2 zdroje k načtení.