- Docker ile Rust ile yapılmış bir web sitesini tekrar tekrar derlerken derleme süresi sorunu yaşanıyor
- Varsayılan Docker ayarlarında her seferinde tüm bağımlılıklar baştan derleniyor ve bu işlem 4 dakikadan uzun sürüyor
cargo-chef ve önbellekleme araçları kullanılsa da son ikili dosyanın derlenmesi hâlâ çok zaman alıyor
- Profilleme sonucunda sürenin büyük kısmının LTO (link time optimization) ve LLVM modül optimizasyonuna harcandığı görülüyor
- Optimizasyon seçenekleri, debug bilgisi ve LTO ayarları değiştirilerek kısmi iyileştirme yapılabilse de son ikili derlemesinin yine de en az 50 saniye sürdüğü doğrulanıyor
Sorunun ortaya konması ve arka plan
- Rust ile yapılmış kişisel web sitesinde her değişiklikten sonra statik bağlı bir ikili dosya derleyip sunucuya kopyalama ve yeniden başlatma işi sürekli tekrarlanıyor
- Docker veya Kubernetes gibi konteyner tabanlı dağıtıma geçmek istense de, Rust’ın Docker içindeki derleme hızı büyük bir sorun olarak ortaya çıkıyor
- Docker içinde küçük kod değişikliklerinde bile her şeyi sıfırdan yeniden derlemek gerektiği için verimsiz bir durum oluşuyor
Docker’da Rust derleme – temel yaklaşım
- Yaygın Dockerfile yaklaşımı, tüm bağımlılıkları ve kaynak kodu kopyaladıktan sonra cargo build çalıştırmak şeklinde
- Bu durumda önbelleklemenin avantajı kayboluyor ve tam yeniden derleme sürekli tekrarlanıyor
- Yazarın web sitesi özelinde tam derleme yaklaşık 4 dakika sürüyor; buna bağımlılık indirmeleri için ek süre de ekleniyor
Docker derleme önbelleğini iyileştirme – cargo-chef
cargo-chef aracı kullanılarak yalnızca bağımlılıklar ayrı bir katmanda önceden önbelleğe alınabiliyor
- Böylece kod değiştiğinde bağımlılık derlemeleri yeniden kullanılarak derleme hızında iyileşme bekleniyor
- Gerçek kullanımda toplam sürenin yalnızca %25’inin bağımlılık derlemesine gittiği, nihai web servisi ikilisinin derlenmesinin ise hâlâ ciddi zaman aldığı görülüyor (2 dakika 50 saniye ila 3 dakika)
- Başlıca bağımlılıklar (axum, reqwest, tokio-postgres vb.) ve yaklaşık 7.000 satırlık özel koda rağmen rustc’nin tek bir çalıştırmasının 3 dakika sürmesi dikkat çekiyor
rustc derleme süresi analizi: cargo --timings
cargo --timings ile her crate’in (derleme biriminin) derleme süresi görülebiliyor
- Sonuçta toplam sürenin büyük kısmını nihai ikili derlemesinin oluşturduğu doğrulanıyor
- Bu yöntem daha ayrıntılı neden analizi için yardımcı olsa da, derleyicinin iç işleyişini somut biçimde anlamada sınırlı kalıyor
rustc’nin kendi profillemesi (-Zself-profile) kullanımı
- rustc’nin yerleşik profilleme özelliği -Zself-profile bayrağıyla etkinleştirilerek ayrıntılı çalışma süreleri ölçülüyor
- Bunun için ortam değişkenleri üzerinden profilleme açılıyor
- Sonuçlar summarize aracıyla incelendiğinde toplam sürenin %60’ından fazlasını LLVM LTO (link time optimization) ve LLVM modül kod üretiminin aldığı görülüyor
- Flamegraph görselleştirmesi de toplam sürenin %80’inin codegen_module_perform_lto aşamasında harcandığını gösteriyor
LTO (link time optimization) ve derleme optimizasyon seçenekleri
- Rust derlemesi varsayılan olarak codegen unit’lere bölünüyor, ardından LTO ile tüm program optimizasyonu nispeten geç bir aşamada uygulanıyor
- LTO için off, thin, fat gibi farklı seçenekler bulunuyor; bunların her biri performansı ve nihai çıktıyı etkiliyor
- Yazarın projesinde
Cargo.toml içinde LTO thin, debug sembolleri ise full olarak ayarlanmış durumda
- Farklı LTO/debug sembolü kombinasyonları test edildiğinde:
- full debug sembollerinin derleme süresini artırdığı ve fat LTO’nun derlemeyi yaklaşık 4 kat yavaşlattığı görülüyor
- LTO ve debug sembolleri kaldırıldığında bile derleme süresi en az 50 saniye oluyor
Ek optimizasyonlar ve notlar
- Yaklaşık 50 saniye, gerçek servis yükü neredeyse olmayan kişisel site için büyük bir sorun olmasa da, teknik merak nedeniyle ek analizler yapılıyor
- Artımlı derleme (incremental compilation) Docker ile iyi kullanılırsa daha hızlı derlemeler mümkün olabilir; ancak bunun için temiz bir derleme ortamı ile Docker önbelleğinin birlikte ele alınması gerekiyor
LLVM aşamasının ayrıntılı profillemesi
- LTO ve debug sembolleri kaldırıldıktan sonra bile sürenin yaklaşık %70’i LLVM_module_optimize aşamasında harcanıyor
- release profilinde opt-level varsayılanının (3) getirdiği optimizasyon maliyetinin yüksek olduğu fark edilerek yalnızca ikili için opt-level düşürme yöntemi test ediliyor
- Çeşitli optimizasyon kombinasyonları sonucunda optimizasyon kapalıyken (
opt-level=0) sürenin yaklaşık 15 saniye, optimizasyon açıkken (1~3) ise yaklaşık 50 saniye olduğu görülüyor
LLVM izleme olaylarının derin analizi
- rustc’nin ek bayrakları (
-Z time-llvm-passes, -Z llvm-time-trace) ile LLVM aşamalarının çalışma süreleri ayrıntılı biçimde izlenebiliyor
-Z time-llvm-passes çok büyük çıktı ürettiği için çoğu zaman Docker’ın log sınırını aşıyor; bu yüzden log ayarlarını değiştirmek gerekebiliyor
- Loglar dosyaya kaydedilip incelendiğinde her LLVM optimizasyon geçişinin çalışma süresi ayrı ayrı görülebiliyor
-Z llvm-time-trace seçeneği, chrome tracing biçiminde devasa JSON çıktısı üretiyor; dosya çok büyüdüğü için sıradan metin düzenleme/analiz araçlarıyla işlemek zorlaşıyor
- Bu çıktı satır bazında bölünerek (jsonl) CLI veya betik ortamında analiz edilebiliyor
Temel içgörüler ve sonuç
- Karmaşık Rust projeleri Docker ile derlenirken darboğaz çoğunlukla nihai ikili derlemesi ve buna bağlı LLVM optimizasyon aşamalarında ortaya çıkıyor
- LTO, debug sembolleri ve opt-level ayarlanırken derleme süresi ile ikili boyutu arasında net bir ödünleşim bulunuyor
- Optimizasyon ayarları agresif biçimde değiştirilirse derleme süresi ciddi ölçüde kısaltılabiliyor, ancak optimizasyon kapatıldığında performans düşebilir
- Büyük crate bağımlılıkları olan ve üretim ortamında derleme verimliliğinin önemli olduğu projelerde, profillemeyi aktif kullanarak ayrıntılı darboğazları somut şekilde tespit etmek iyi bir strateji
- Rust derleme hattı tasarlanırken LTO, opt-level, debug sembolleri ve önbellek stratejilerinin ince ayarlanmış bir kombinasyonla ele alınması gerekiyor
1 yorum
Hacker News görüşleri
Rust projeleri bazen dışarıdan küçük görünse de ilginçtir. Birincisi, bağımlılıklar kod tabanının gerçek boyutuyla bağlantılı değildir. C++'ta büyük proje bağımlılıkları sık sık vendoring ile içeri alınır ya da hiç kullanılmaz; bu yüzden 400 bin satırlık kodda yavaş çok şey varsa, "kod çok, yavaş olması normal" diye düşünebilirsiniz. İkincisi, çok daha sorunlu kısım makrolardır. 10 satırı, 100 satırı tekrar tekrar genişleten makrolar, 10 bin satırlık bir projeyi çok kısa sürede milyon satıra çıkarabilir. Üçüncüsü generics'tir. Her generic instance oluşturma CPU kaynağı tüketir. Yine de biraz savunmak gerekirse, bu özellikler sayesinde C'de 100 bin satır, C++'ta 25 bin satır sürecek şeyler Rust'ta birkaç bin satıra inebilir. Ama bu özelliklerin aşırı kullanılması yüzünden ekosistemin yavaş görünmesi de bir gerçek. Örneğin şirketimizde async-graphql kullanıyoruz; kütüphanenin kendisi harika ama procedural macro bağımlılığı çok yüksek. Performansla ilgili sorunlar yıllardır açık ve her yeni veri tipi eklediğimizde derleyicinin belirgin biçimde yavaşladığını hissediyoruz
Ryan Fleury, Epic RAD Debugger'ı C ile 278 bin satırlık unity build tarzında yaptı (tüm kod tek bir dosyada, tek bir derleme birimi olarak) ve Windows'ta clean compile sadece 1,5 saniye sürüyor. Sadece bu örnek bile derlemenin ne kadar hızlı olabileceğini gösteriyor; Rust veya Swift'te neden benzeri yapılamıyor merak ediyorum
Go'nun optimizasyondan çok derleme hızını öncelemiş olmasına gerçekten seviniyorum. Sunucu, ağ ve glue code işlerinde derlemenin gerçekten hızlı olması her şeyden önemli. Bir miktar tip güvenliği de istiyorum ama gevşek prototiplemeyi engellemeyecek düzeyde. GC olması da kullanışlı. Bence Google, çok büyük ölçekli geliştirme deneyiminden sonra basit tipler, GC ve aşırı hızlı derlemenin; çalışma hızı veya anlamsal mükemmellikten çok daha önemli olduğu sonucuna vardı. Go ile yapılmış büyük ağ ve altyapı yazılımlarına bakınca bu tercih tam isabet görünüyor. Elbette GC'nin kabul edilemez olduğu ortamlarda ya da kusursuz doğruluğun daha önemli olduğu yerlerde Go kullanılmayabilir ama benim çalışma ortamımda Go'nun tercihleri en uygunu
Tek bir statik ikili kurulumunun konteyner yönetiminden daha basit olduğu iddiasını anlayamıyorum
Dizüstümde (Mac M4 Pro) Deno'nun tamamını derlemek 2 dakika sürüyor; bu büyük bir Rust projesi. Komut bazında bakınca debug yaklaşık 1 dakika 54 saniye, release ise yaklaşık 8 dakika 17 saniye sürüyor. Bunlar incremental compilation olmadan ölçülmüş değerler. Gerçi dağıtım build'leri zaten CI/CD sisteminde çalıştığı için bunları doğrudan beklemek zorunda değilim
Cranelift'ten neden hiç bahsedilmiyor? Bana kalırsa Rust ile oyun geliştirirken derleme süreleri o kadar uzundu ki neredeyse vazgeçecektim. Araştırınca LLVM'in optimizasyon seviyesinden bağımsız olarak yavaş olduğunu gördüm. Jai dili geliştiricileri bunu hep söylüyordu. Cranelift ile build süresinin 16 saniyeden 4 saniyeye düştüğünü yaşadım. Cranelift ekibine hayran kaldım!
Rust'ın yavaş olduğunu düşünmüyorum. Benzer seviye dillerle kıyaslayınca yeterince hızlı; 15 dakikaya varan C++/Scala derlemelerine göre çok daha iyi
Eski bir C++ geliştiricisi olarak Rust build'lerinin yavaş olduğu iddiasını pek anlayamıyorum
Incremental compilation gerçekten çok güçlü. İlk build'den sonra incremental cache snapshot'ını sabitleyip değişiklik yoksa aynı hâliyle hızlı build/deploy için kullanabiliyorsunuz. Docker ile de uyumu iyi. Derleyici sürümü veya büyük web sitesi güncellemeleri dışında image build katmanlarına dokunulmuyor. Sadece kod değiştiğinde ilgili katmanın yeniden build edilmemesini sağlayabilirseniz çok verimli oluyor
Ana sayfamın build süresi 73 ms. Static site generator 17 ms'de yeniden derliyor. Generator'ün fiilen çalışması da sadece 56 ms sürüyor. Zig build log çıktısını ekledim