Rust derleyicisi neden bu kadar yavaş?
(sharnoff.io)- Docker dağıtımı için yapılan Rust web sitesi derlemesinde, bağımlılıklar önbelleklense bile yalnızca son crate yaklaşık 175 saniye sürüyor; darboğazın
rustciçi ve LLVM optimizasyon aşamalarında olduğu anlaşılıyor - Sırasıyla
cargo-chef,cargo --timings,-Zself-profile,measuremeuygulanınca, sorunun basit bir bağımlılık meselesi değil, derleme süresine LTO ve LLVM kod üretimi maliyetinin hakim olduğu görüldü - Eski
Cargo.tomlayarlarındakilto = "thin"vedebug = "full"büyük etki yaptı; ikisi kapatılınca son ikili derleme süresi 172.2 saniyeden yaklaşık 50 saniyeye düştü - LLVM izlemesinde
OptFunction,InlinerPass,core::ptr::drop_in_place, büyük async fonksiyonlar ve jenerik monomorfizasyon başlıca maliyetler olarak öne çıktı; inlining azaltma, fonksiyonları bölme,Pin<Box<dyn Future>>ve jenerikleri kaldırma ek iyileştirmeler sağladı - Son olarak
-Zshare-genericsve Debian tabanlı derlemeye geçiş uygulanınca derleme süresi 29.1 saniyeden 9.1 saniyeye kadar düştü; yani yalnızca kod yapısı değil, allocator ve musl hedefi kullanılıp kullanılmaması da derleme süresini ciddi biçimde etkiliyor
Docker derlemesinde ortaya çıkan darboğaz
- Web sitesi ağırlıklı olarak tek bir Rust ikilisi olarak sunuluyordu; daha önce statik bağlantılı bir ikili derlenip sunucuya kopyalanıyor, ardından servis yeniden başlatılıyordu
- Konteyner tabanlı dağıtıma geçince, Docker içinde hızlı Rust derlemesi kurmanın beklenenden daha zor olduğu görüldü
- Temel Dockerfile, kaynak değiştiğinde her şeyi baştan derliyordu
- Builder olarak
rust:1.87-alpine3.22kullanılıyor vex86_64-unknown-linux-muslhedefi için derleme yapılıyordu - Son imajda yalnızca ikili Alpine içine kopyalanıyordu
- Bu yöntemde temiz derleme, crate indirmeleri için geçen 10 saniye dahil 3 dakika 51 saniye sürüyordu
- Builder olarak
cargo-chef ile bağımlılık önbelleği ayrıldı ama yetmedi
- cargo-chef, workspace içinde sadeleştirilmiş bir recipe dosyası üretip buna dayanarak bağımlılıkları ayrı bir Docker önbellek katmanında önceden derliyor
- Web sitesi yüzlerce bağımlılık kullandığı için önbellek etkisinin büyük olması bekleniyordu
- Gerçek ölçümlerde bağımlılık derlemesi 1 dakika 7 saniye, önbelleğe alınmış bağımlılıklarla son ikili derlemesi ise 2 dakika 50 saniye sürdü
- Toplam sürenin yalnızca yaklaşık %25’i bağımlılıklara gidiyordu; geri kalan büyük kısmı
web-http-serverson crate’i için yapılan tek birrustcçağrısında harcanıyordu
cargo --timings ve rustc self-profile
cargo build --release --timings, crate bazında derleme sürelerini gösteriyor; son crate süresi 174.1 saniye ilecargo buildçıktısındaki 2 dakika 54 saniyeye yaklaşık olarak uyuyordu- Darboğaz tek bir son crate üzerinde yoğunlaştığı için
cargo --timingstek başına ayrıntılı nedeni bulmaya yetmedi rustcself-profile özelliğini kullanmak için-Zself-profilekullanıldı- Kararlı derleyicide kararsız
-Zbayraklarını kullanmak içinRUSTC_BOOTSTRAP=1kullanıldı cargo-chefönbelleğinin bozulmaması içincargo rustc -- -Z self-profileyerineRUSTFLAGS='-Zself-profile'tercih edildi
- Kararlı derleyicide kararsız
- measureme içindeki
summarize,flamegraph,croxaraçlarıyla self-profile verisi analiz edildi summarizeiçindeki üst sıralar LLVM ile ilgili işlerde toplanıyorduLLVM_lto_optimize: 851.95 saniye, toplamın %33.389’uLLVM_module_codegen_emit_obj: 674.94 saniye, %26.452LLVM_thin_lto_import: 317.75 saniye, %12.453LLVM_module_optimize: 189.00 saniye, %7.407
- Flamegraph’ta
codegen_module_perform_ltotoplam sürenin yaklaşık %80’ini kaplıyordu
LTO ve debug sembol ayarlarının etkisi
- Rust derleyicisi bir crate’i codegen unit’lere bölüp LLVM’ye ayrı modüller olarak gönderiyor
- LTO, bağlama aşamasında codegen unit’ler veya crate’ler arasında inlining ve optimizasyon yapan bir seçenek
- Cargo ve
rustciçin LTO seçenekleri şöyle- LTO kapalı
"thin"LTO"fat"LTO- Belirtilmezse yalnızca tek crate içinde sınırlı olan “thin local LTO”
- Mevcut
Cargo.tomliçinde yıllar önce ayarlanmış değerler kalmıştılto = "thin"debug = "full"
debug = "full", release profilinde varsayılan olarak dışarıda bırakılan tam debug sembollerini etkinleştiriyor- Farklı
ltovedebugkombinasyonlarının ölçüm sonuçlarında büyük fark vardı- LTO kapalı,
debug=none: 50.0 saniye / 21.0MiB - Thin local LTO,
debug=full: 88.2 saniye / 256.8MiB "thin"LTO,debug=full: 172.2 saniye / 197.5MiB"fat"LTO,debug=full: 287.1 saniye / 155.9MiB
- LTO kapalı,
- Tam debug sembolleri derleme süresini %30~50 artırdı; fat LTO ise LTO’nun tamamen kapalı olduğu duruma göre yaklaşık 4 kat daha uzun sürdü
- LTO ve debug sembolleri kapatılsa bile tek bir son ikiliyi derlemek için hâlâ yaklaşık 50 saniye gerekiyordu
Neden artımlı derleme yerine Docker önbelleği korundu?
- Yerel geliştirmede
/targetdizinini Dockerfile içindeki cache mount ile bağlayıp derlemeler arasında koruyarak artımlı derleme kullanılabiliyor - Ancak
docker build’in her seferinde temiz bir ortam sağlayabilmesi ve Docker’ın kendi önbellek sisteminden yararlanmak istendiği içincargo-chefkullanılmaya devam edildi
LTO sonrasında da kalan LLVM optimizasyon maliyeti
- LTO ve debug sembolleri kapatıldıktan sonra bile son ikili derlemesi yaklaşık 50 saniye sürüyordu
- Self-profile’a yeniden bakıldığında sürenin yaklaşık %70’inin
LLVM_module_optimizeiçinde geçtiği görüldü; bu, LLVM’nin kodu optimize ettiği aşama - Release profilinin varsayılan
opt-level = 3değeri düşürülerek yalnızca son ikilinin daha az optimize edilmesi denendi- Bağımlılıklar önbellekte olduğundan
profile.release.package."*"içindeopt-level = 3korundu - Yalnızca son crate için
opt-leveldüşürüldü
- Bağımlılıklar önbellekte olduğundan
- Ölçümlerde optimizasyon düzeyine göre büyük fark oluştu
- Son crate
opt-level=0: yaklaşık 15 saniye - Son crate
opt-level=1: yaklaşık 48 saniye - Son crate
opt-level=2veya3: yaklaşık 50~55 saniye - Son crate
opt-level="z": yaklaşık 42 saniye
- Son crate
- Son ikili için herhangi bir optimizasyon açıldığında yaklaşık 50 saniyelik bir taban oluşuyor; optimizasyon tamamen kapatılırsa süre yaklaşık 15 saniyeye iniyor
LLVM izleme verisi toplamanın zorluğu
rustciçinde LLVM bilgilerini görmek için bayraklar var-Z time-llvm-passes: LLVM profil bilgisini düz metin olarak yazdırır-Z llvm-time-trace: LLVM profilini Chrome tracing formatında üretir
-Z time-llvm-passes, Docker BuildKit’in varsayılan log sınırına takıldıBUILDKIT_STEP_LOG_MAX_SIZEBUILDKIT_STEP_LOG_MAX_SPEED
- Bu ortam değişkenleri
docker buildçağrısında değil, Docker daemon tarafında ayarlanmalı; Linux’tasystemddrop-in iledocker.serviceüzerinde yapılandırılabiliyor - Sınır kaldırılınca yaklaşık 200 bin satır metin çıktı ve bunu doğrudan işlemek zorlaştı
-Z llvm-time-trace,*.llvm_timings.jsondosyası üretti ama son ikiliye ait iz dosyası 1.4GiB boyutunda tek satırlık bir JSON’du- Firefox Profiler, Perfetto UI ve Chromium’un
chrome://tracingaracı bu dosyayı işlemek konusunda sorun yaşadı - JSON, genel araçlarla işlenebilmesi için JSONL’ye dönüştürüldü
- Tek JSON nesnesindeki
traceEventsdizisi, olay başına bir satıra bölündü - Dönüşüm sonrası olay sayısı 7,301,865 satır oldu
- Tek JSON nesnesindeki
LLVM olaylarında görülen darboğazlar
- LLVM trace olayları çoğunlukla
"ph":"X"olan complete event’lerdi;duralanı mikro saniye cinsinden süreyi gösteriyordu "ph":"M", metadata event’ti ve bu analizde çok faydalı bilgi taşımıyordu- Aggregate olaylarda en çok süre harcanan kalemler şunlardı
Total ModuleInlinerWrapperPass: 665.37 saniyeTotal ModuleToPostOrderCGSCCPassAdaptor: 656.47 saniyeTotal DevirtSCCRepeatedPass: 632.44 saniyeTotal OptFunction: 189.62 saniyeTotal InlinerPass: 182.25 saniye
- Bu çalıştırma 16 çekirdekli bir makinede yaklaşık 110 saniye sürdüğü için bazı pass süreleri çakışmalı biçimde sayılmıştı
- Büyük eksenler, fonksiyon optimizasyonu olan
OptFunctionve inlining işlemi olanInlinerPassidi
Inlining eşiklerini ayarlamak
- LLVM inlining seçenekleri,
rustciçindeki-C llvm-argsile iletilebiliyor - 2025 Haziran itibarıyla
rustc -C llvm-args='--help-list-hidden'çıktısında inlining ile ilgili yaklaşık 100 seçenek var - Deneylerde kullanılan üç seçenek şunlardı
--inlinedefault-threshold=225--inline-threshold=225--inlinehint-threshold=325
- Threshold, yaklaşık olarak maliyeti bu değerden düşük olan fonksiyonların inline edilmesine izin veriyor; değeri düşürmek daha az inlining anlamına geliyor
- Üç eşik de 50’ye düşürüldüğünde süre 48.8 saniyeden 42.2 saniyeye indi
- Neredeyse hiç yük almayan kişisel bir web sitesi kullanım senaryosunda threshold 10 da umut verici bir seçenek olarak değerlendirildi
OptFunction ve jenerik monomorfizasyon
OptFunctionolaylarınınargs.detailalanında optimize edilen fonksiyonun mangled sembolü yer alıyor- rustfilt ile demangle edildiğinde asıl Rust sembolü görülebiliyor
__rustc::__rust_allocserde_json::value::to_value
- Aynı
serde_json::value::to_valuefonksiyonunun farklı hash’lerle görünmesinin nedeni, jenerik fonksiyonların farklı tip parametreleriyle monomorfize edilmesi - Başka crate’lerdeki fonksiyonlar da son crate içinde optimize ediliyor; çünkü bir fonksiyonun belirli tiplerle monomorfize edildiği yer, onu çağıran crate bağlamı oluyor
- Optimizasyon süresi yüksek olan fonksiyon örnekleri şunlardı
web_http_server::photos::PhotosState::newiçindeki closureweb_http_server::runiçindeki closuretokio_postgres::connect_rawpulldown_cmarkiçindeki 500 satırlık jenerik fonksiyoncore::ptr::drop_in_placeiçin çeşitli somut tipler
- Dış crate adına göre kabaca toplandığında en büyük pay 61.53 saniye ile
core’daydı ve bunun %84’ücore::ptr::drop_in_placeparametreleştirmelerinden geliyordu
v0 sembol mangling ile async fonksiyonların yerini daha net görmek
- Varsayılan legacy symbol mangling, closure’ları ayırt etmeyi zorlaştırıyordu
-C symbol-mangling-version=v0eklendiğinde closure numaraları ve jenerik tip bilgileri daha görünür hale geldi- Örneğin
serde_json::value::to_value’nun hangiweb_http_servertipiyle monomorfize edildiği, tam jenerik argümanlarla görülebiliyordu - v0 çıktısında pahalı kalemler şunlardı
<web_http_server::photos::PhotosState>::new::{closure#0}: 1.99 saniyeweb_http_server::run::{closure#0}: 1.56 saniyecore::ptr::drop_in_place::<axum::routing::Endpoint<web_http_server::AppState>>: 1.22 saniye
- Dışarıdan küçük görünen bu closure’ların, LLVM IR dökümünde aslında async fonksiyonlar ve async block’ların içte içe closure’lar olarak temsil edildiği görüldü
- Rust tarafında async function/block mangling ile ilgili zaten açık bir issue bulunuyordu
Büyük async fonksiyonlar ve Pin<Box<dyn Future>>
- Pahalı kalem, closure’ın kendisinden çok büyük async fonksiyon gövdesiydi
PhotosState::newile ilgili optimizasyon süresi başlangıçta toplam 5.3 saniye idi- Fonksiyonu yalnızca bölmeye yönelik ilk deneme, süreyi ancak 4.66 saniyeye indirebildi
- Yakın
.awaitnoktalarını birleştirip.awaitsayısını 10’dan 3’e düşüren deneme ise tersine 6.24 saniyeye çıktı - Async fonksiyonlar içerde karmaşık bir state machine’e indirgendikleri için, çağıran tarafta gerçekleştirim ayrıntılarını gizlemek amacıyla
Future’ı trait object olarak silme yaklaşımı denendi - Kullanılan yapı,
impl Future<Output = T>tipiniPin<Box<dyn Send + Future<Output = T>>>içine sarmalıyordu - Her
.awaitnoktasındaerase(get_img_candidates()).await?gibi uygulandığında:PhotosState::newile ilgili süre 2.14 saniyeye düştü- Toplam derleme süresi, profil çıkarmadan, 48.8 saniyeden 46.8 saniyeye indi
#[inline(never)]ve poll fonksiyonu inlining’ini kapatma da denendi ama boxing kadar iyi sonuç vermedi
Birden çok değişiklik birleştirildiğinde
- Uygulanan yaklaşım üç parçadan oluşuyordu
- LLVM argümanlarıyla daha az inlining
- Ana crate içindeki pahalı fonksiyonların ayrılması ve async Future boxing
- Bağımlılık API’lerindeki jeneriklerin azaltılmasıyla son crate’te yeniden derlenen kısımların azaltılması
- Son Dockerfile’da üç inlining threshold’unu 10’a düşüren
RUSTFLAGS, hemcargo chef cookhem decargo buildiçin uygulandı - Ana crate içinde 10 dosyaya yayılan 898 satır ekleme, 657 satır silme yapıldı
- Bağımlılık tarafındaki değişiklikler de buna dahildi
pulldown-cmarkiçindeki jenerik fonksiyonu jenerik olmayan hale getiren PRlol_htmlvedeadpool_postgrestarafından kullanılan API’lerin jenerik olmayan sürümlerini sunan yerel crate
- Bu birleşimle son derleme süresi 32.3 saniye oldu
2025-06-27 güncellemesi: -Zshare-generics ve Alpine’den çıkış
- Bluesky ve Lobsters üzerinden gelen önerilerle iki ek deney yapıldı
-Zshare-genericsetkinleştirmek- Alpine’den çıkmak
-Zshare-generics, crate bağımlılıklarının jenerik örneklerini yeniden kullanan bir bayrak- Release derlemelerde varsayılan olarak etkin değil
- Kararlı toolchain’in dev derlemelerinde etkin
- Bu bayrak yalnızca nightly’de kullanılabiliyor
-Zshare-genericsaçılınca toplam derleme süresi 32.3 saniyeden 29.1 saniyeye indidrop_in_placeörnekleri hâlâ çok sayıda derleniyordu ama bunların optimizasyon süresi 21.7 saniyeden 17.4 saniyeye düştü- Alpine yerine Debian’a geçilip
--target=x86_64-unknown-linux-muslkaldırılınca toplam derleme süresi 29.1 saniyeden 9.1 saniyeye dramatik biçimde düştü - Bu önerinin arkasındaki düşünce, varsayılan allocator’ın derleme süresinde büyük etkiye sahip olabilmesiydi
Son sayılar ve kalan işler
- Nihai değişimler şöyleydi
- Başlangıç noktası: yaklaşık 175 saniye
- LTO ve debug sembollerini kapatma: 51 saniye, -%71
- Son crate için
opt-level = 1: 48.8 saniye, -%4 -C llvm-argsile inlining azaltma: 40.7 saniye, -%16- Yerel kod değişiklikleri: 37.7 saniye, -%7
- Bağımlılık değişiklikleri: 32.3 saniye, -%14
-Zshare-generics: 29.1 saniye, -%10- Alpine’den çıkış: 9.1 saniye, -%69
- Analiz sürecinde kullanılan araçlar ve dokümantasyon gerçekten iyileştirme üretecek kadar işe yaradı
- Yine de birkaç karmaşık sorun duruyor
- Derin async fonksiyon çağrı grafiklerinin derleme süresi daha da iyileştirilmeli
core::ptr::drop_in_place<T>için,T’yi tanımlayan crate’te derleme yapacak özel bir yaklaşım bazı durumlarda faydalı olabilir; ancak bunu jenerik tiplere uygulamak zor ve kullanılmayan drop glue’nun da derlenmesi riski var-Zshare-genericsyararlı olsa da tam çözüm değil- Kod tabanının hangi bölümlerinin çok derleme süresi harcadığını izole edip çözüm önerecek daha fazla araca ihtiyaç olabilir
- Pratikte son crate için
opt-level = 0ayarlamak da yeterince makul bir tercih olabilir
Henüz yorum yok.