1 puan yazan GN⁺ 2025-06-28 | Henüz yorum yok. | WhatsApp'ta paylaş
  • 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 rustc içi ve LLVM optimizasyon aşamalarında olduğu anlaşılıyor
  • Sırasıyla cargo-chef, cargo --timings, -Zself-profile, measureme uygulanı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.toml ayarlarındaki lto = "thin" ve debug = "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-generics ve 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.22 kullanılıyor ve x86_64-unknown-linux-musl hedefi 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

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-server son crate’i için yapılan tek bir rustc ç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 ile cargo 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 --timings tek başına ayrıntılı nedeni bulmaya yetmedi
  • rustc self-profile özelliğini kullanmak için -Zself-profile kullanıldı
    • Kararlı derleyicide kararsız -Z bayraklarını kullanmak için RUSTC_BOOTSTRAP=1 kullanıldı
    • cargo-chef önbelleğinin bozulmaması için cargo rustc -- -Z self-profile yerine RUSTFLAGS='-Zself-profile' tercih edildi
  • measureme içindeki summarize, flamegraph, crox araçlarıyla self-profile verisi analiz edildi
  • summarize içindeki üst sıralar LLVM ile ilgili işlerde toplanıyordu
    • LLVM_lto_optimize: 851.95 saniye, toplamın %33.389’u
    • LLVM_module_codegen_emit_obj: 674.94 saniye, %26.452
    • LLVM_thin_lto_import: 317.75 saniye, %12.453
    • LLVM_module_optimize: 189.00 saniye, %7.407
  • Flamegraph’ta codegen_module_perform_lto toplam 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 rustc iç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.toml iç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ı lto ve debug kombinasyonları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
  • 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 /target dizinini 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çin cargo-chef kullanı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_optimize içinde geçtiği görüldü; bu, LLVM’nin kodu optimize ettiği aşama
  • Release profilinin varsayılan opt-level = 3 değeri düşürülerek yalnızca son ikilinin daha az optimize edilmesi denendi
    • Bağımlılıklar önbellekte olduğundan profile.release.package."*" içinde opt-level = 3 korundu
    • Yalnızca son crate için opt-level düşürüldü
  • Ö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=2 veya 3: yaklaşık 50~55 saniye
    • Son crate opt-level="z": yaklaşık 42 saniye
  • 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

  • rustc iç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_SIZE
    • BUILDKIT_STEP_LOG_MAX_SPEED
  • Bu ortam değişkenleri docker build çağrısında değil, Docker daemon tarafında ayarlanmalı; Linux’ta systemd drop-in ile docker.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.json dosyası ü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://tracing aracı 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 traceEvents dizisi, olay başına bir satıra bölündü
    • Dönüşüm sonrası olay sayısı 7,301,865 satır oldu

LLVM olaylarında görülen darboğazlar

  • LLVM trace olayları çoğunlukla "ph":"X" olan complete event’lerdi; dur alanı 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 saniye
    • Total ModuleToPostOrderCGSCCPassAdaptor: 656.47 saniye
    • Total DevirtSCCRepeatedPass: 632.44 saniye
    • Total OptFunction: 189.62 saniye
    • Total 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 OptFunction ve inlining işlemi olan InlinerPass idi

Inlining eşiklerini ayarlamak

  • LLVM inlining seçenekleri, rustc içindeki -C llvm-args ile 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

  • OptFunction olaylarının args.detail alanında optimize edilen fonksiyonun mangled sembolü yer alıyor
  • rustfilt ile demangle edildiğinde asıl Rust sembolü görülebiliyor
    • __rustc::__rust_alloc
    • serde_json::value::to_value
  • Aynı serde_json::value::to_value fonksiyonunun 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::new içindeki closure
    • web_http_server::run içindeki closure
    • tokio_postgres::connect_raw
    • pulldown_cmark içindeki 500 satırlık jenerik fonksiyon
    • core::ptr::drop_in_place iç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_place parametreleş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=v0 eklendiğinde closure numaraları ve jenerik tip bilgileri daha görünür hale geldi
  • Örneğin serde_json::value::to_value’nun hangi web_http_server tipiyle 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 saniye
    • web_http_server::run::{closure#0}: 1.56 saniye
    • core::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::new ile 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 .await noktalarını birleştirip .await sayı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> tipini Pin<Box<dyn Send + Future<Output = T>>> içine sarmalıyordu
  • Her .await noktasında erase(get_img_candidates()).await? gibi uygulandığında:
    • PhotosState::new ile 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, hem cargo chef cook hem de cargo build iç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-cmark içindeki jenerik fonksiyonu jenerik olmayan hale getiren PR
    • lol_html ve deadpool_postgres tarafı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-generics etkinleş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-generics açılınca toplam derleme süresi 32.3 saniyeden 29.1 saniyeye indi
  • drop_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-musl kaldı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-args ile 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-generics yararlı 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 = 0 ayarlamak da yeterince makul bir tercih olabilir

Henüz yorum yok.

Henüz yorum yok.