Düşük Seviyeli Optimizasyon ve Zig
(alloc.dev)- Düşük seviyeli optimizasyon, derleyicinin kodun niyetini ve kısıtlarını daha iyi anlamasını sağlamaya yönelik bir iştir; Zig de tür, hizalama, alias ve derleme zamanı bilgilerini açıkça ifade etmeyi kolaylaştırdığı için bu amaca iyi uyar
- LLVM gibi optimize edici derleyiciler bile her zaman en iyi kodu üretmez; bu yüzden darboğaz bölgelerde üretilen kodu kontrol etmek ve kodu ayarlamak hâlâ gerekir
- Zig,
noalias,align, sabit dizi boyutu ve eleman türünü derleme zamanında geçirerek JavaScript örneğine göre daha küçük vektörleştirilmiş kod üretebilir comptime, normal Zig kodunu derleme zamanında çalıştırarak sabit üretimi, generic uygulama, tür yansıtma ve string karşılaştırma optimizasyonu gibi metaprogramlama olanakları sağlar- Zig’in gücü, AST’yi doğrudan değiştiren makrolardan ziyade dile entegre derleme zamanı yürütmesindedir; bazı çalışma zamanı değerleri de derleme zamanı özelleştirme fonksiyonlarına dispatch edilebilir
Derleyiciye güvenip yine de doğrulamak neden gerekir
- Optimizasyon yalnızca hızlı programlar üretme tekniği olmanın ötesinde maliyet azaltma, ölçeklenebilirliği artırma ve sistem sadeliğini korumayla bağlantılıdır
- Modern derleyiciler LLVM gibi backend’lerle etkileyici sonuçlar üretir, ancak bazı durumlarda hâlâ alt düzeyde optimize edilmiş kod oluşturur
- Düşük seviyeli dillerin hızlı olmasının nedeni yalnızca garbage collection veya interpreter overhead’inin az olması değil, derleyicinin anlayabileceği niyet bilgisini daha fazla ifade edebilmeleridir
- Derleyici algoritmanın ya da programlama paradigmasının kendisini değiştiremez; çoğunlukla döngüler gibi sınırlı kapsamlar içinde optimizasyon yapar
JavaScript ve Zig’de dizi maksimumu örneği
- JavaScript örneği, iki dizinin eleman bazında maksimumunu
x[i] = y[i] > x[i] ? y[i] : x[i]biçimindexiçine yazar - İnsan için açık bir kod olsa da V8’in ürettiği bytecode şişkindir
- Zig örneği, fonksiyon argümanlarında optimizasyon için gereken bilgileri daha somut biçimde belirtir
noalias x:xbaşka pointer’larla alias değildir*align(64): 64 bayt hizalama[65536]f64: dizi boyutu ve eleman türüconst: salt okunur argüman
- Bu bilgiler sayesinde derleyici daha iyi kod üretebilir; örnekte vektörleştirilmiş assembly oluşturulur
- Eşdeğer Rust kodu da neredeyse aynı assembly’yi üretir
Zig’in optimizasyon açısından avantajlı noktaları ve sınırları
- Zig, ayrıntılı ifadeye izin vererek LLVM’e kod hakkında çok fazla bilgi aktarabilir
- Optimizasyonla ilgili olarak Zig’in sunduğu başlıca unsurlar şunlardır
- Rust’ın bellek modeli, derleyicinin fonksiyon argümanlarının alias oluşturmadığını her zaman varsayabilmesini sağlar; Zig’de ise bunu doğrudan belirtmek gerekir
- Derleyici Zig fonksiyon argümanlarının alias olmadığını bilemezse, açıklama eklenmemiş bir Zig fonksiyonu Rust fonksiyonundan daha yavaş olabilir
- Yalnızca iyi açıklama eklenmiş LLVM IR temel alınsa bile Zig iyi sonuçlar üretir; ancak daha büyük gücü derleme zamanı yürütmesindedir
comptimeın rolü
- Zig’in
comptimeı, derleme zamanında kod üretimi için bir özelliktir - Derleme zamanında yapılabilecek işler şunlardır
- Sabitler üretip binary’ye dahil etmek
- Aynı hashmap yapısını birden çok veri türü için tekrar tekrar yazmamak
- Derleme zamanında bilinen verilere dayanarak gereksiz kodu kaldıracak optimizasyonu teşvik etmek
- Türleri inceleyip yansıtarak ve üreterek generic uygulamak
comptimekodu, derleme zamanında çalıştırılan normal Zig kodudur; ağ IO’su gibi yan etkileri olamaz- Derleme zamanı emülasyon makinesi, derleme hedefiyle eşleşir
- Neredeyse tüm Zig kodları
comptimeile derleme zamanında çalıştırılabilir ve tüm türler derleme zamanında incelenebilir, yansıtılabilir ve üretilebilir
Makrolardan farkı
comptimeın amacı makrolara benzer, ancak çalışma biçimi farklıdır- Bazı makrolar ham metni değiştirir, bazıları programın AST’sini doğrudan değiştirir
- Zig’in
comptimeı AST’yi doğrudan değiştirmez; token birleştirme makroları gibi özellikleri de yoktur - Zig okunması kolay bir dil olmayı hedeflediğinden, ilgisiz scope’larda değişken oluşturan veya değiştiren makro tarzıyla uyumlu değildir
- Makroların yapabildiği ancak Zig
comptimeın doğrudan yapamadığı işler şunlardır- Başka makrolar tanımlamak
- AST değiştirmek
- Mini dil veya DSL’i doğrudan uygulamak
- Bununla birlikte Zig’de de DSL oluşturulabilir; Zig’in
printfonksiyonu, veriyi serileştirecek fonksiyon grafiğini oluşturmak içincomptimeile format string’ini parse eder - Örnekler arasında TigerBeetle account testing DSL, comath, zilliam vardır
comptime string karşılaştırma optimizasyonu
- Tipik string karşılaştırması, uzunluklar farklıysa
falsedöndürür; aynıysa her baytı sırayla karşılaştırır - Bu yöntem iki string’in her birinden bayt okuyup karşılaştırmayı gerektirir
- Bir string’in derleme zamanında zaten bilindiği sık görülür; bu yüzden Zig’de bir argümanın
comptimeolmasını isteyebilirsinizfn staticEql(comptime a: []const u8, b: []const u8) bool
"Hello!\n"gibi statik bir string ile karşılaştırırken derleyici, uzunluk karşılaştırması ve her baytın sabit karşılaştırmasından oluşan kod üretir- Bu bölümün amacı yalnızca derleyicinin otomatik yapabileceği optimizasyonları göstermek değil,
comptimeile dönüşümü zorlayarak derleyicinin göremediği fırsatları açabileceğini göstermektir
Daha büyük birimlerle karşılaştırma ve SIMD kullanımı
- Basit
comptimestring karşılaştırması hâlâ bayt bazında karşılaştırma yapar - İyileştirilmiş sürüm, karşılaştırma blok boyutunu belirlemek için
std.simd.suggestVectorLength(u8)veya@sizeOf(usize)kullanır - Önce string uzunluğu kontrol edilir, ardından karşılaştırılabilecek büyük blok sayısı ve kalan bayt sayısı hesaplanır
- Her blok,
std.meta.Int(.unsigned, block_len * 8)ile oluşturulan tamsayı türüne@bitCastedilerek karşılaştırılır - Kalan baytlar da ayrı bir tamsayı türüyle karşılaştırılır
"Hello, World!\n"örneğinin üretilen assembly’si daha büyük register’lar kullanır ve koşullu dallanma sayısını azaltır- Daha büyük string karşılaştırmalarında daha büyük SIMD register’ları kullanan assembly üretilir
Çalışma zamanı değerleriyle derleme zamanı özelleştirmesini birlikte kullanmak
- Zig’in
comptimeı yalnızca derleme zamanında bilinen verilerle sınırlı değildir - Basit durumlarda birden çok prosedür derleme zamanında üretilip çalışma zamanı değerine göre uygun prosedüre dinamik dispatch yapılabilir
- Örnek kod,
switch (runtime_val)içindeinline 0...100aralığındaki değerleristaticFn(comptime_val)e gönderir, kalanlarıruntimeFn(runtime_val)ile işler - Binary boyutunun artmasını istemiyorsanız tamamen çalışma zamanı uygulamasına fallback yapılabilir
Sonuç
- Zig’in
comptimeı template, makro, generic ve elle kod üretiminin yerini alan bir rol üstlenir - Başka dillerle de benzer işler yapılabilir, ancak Zig’de
comptimedile daha doğal biçimde entegre edilmiştir - Zig, gerçekten faydalı durumlarda performanslı kod yazmayı kolaylaştırır; her şeyin mümkün olduğu ama ilginç işlerin zor olduğu Turing tar-pit ile tezat oluşturur
- Dil savaşları konusunda, Turing tamlığının tek başına yeterli olduğu yönündeki geniş bakış ile insanların aynı zamanda tercih ettikleri dillere sahip olabileceği görüşü birlikte kalır
- “C, Python’dan hızlıdır” gibi dili kendi başına benchmark hedefi gören ifadeler yanıltıcı olabilir; gerçek benchmark hedefi dil değil, belirli kod ve uygulamadır
1 yorum
Hacker News yorumları
Zig’de beni en çok çeken yan, derleme sisteminin kolaylığı, çapraz derleme ve hızlı yineleme hızını hedeflemesi.
Oyun geliştiricisiyim, bu yüzden performans gereksinimlerim var; ama çoğu dilin gereken performansı yeterince sağladığını düşündüğümden dil seçiminde birinci önceliğim bu değil.
Her dille güçlü kod yazılabilir; asıl mesele, onlarca yıl modüler kodu sürdürebilecek geleceğe dayanıklı bir framework seçmek.
C/C++ her yerde desteklendiği için varsayılan yanıttı; Zig’in de o seviyeye ulaşabileceği hissi var.
Zig kapsüllemeye karşı ve struct üyelerini özel yapamıyorsunuz: https://github.com/ziglang/zig/issues/9909#issuecomment-9426...
Temel alıntıdaki yaklaşım şu: “özel alanlar ve getter/setter’lar Java’nın popülerleştirdiği bir anti-pattern’dir; alanlar var olan veridir, bu yüzden dikkatle adlandırın ve genel API’nin parçası olarak belgeleyin.”
İç temsili gizleyemiyorsanız, yazılım modülerliğinin temeli olan API sözleşmesini düzgün kurmak zorlaşır; kullanıcı kodunu bozmadan iç temsili değiştirebilmek gerekir.
Zig’in tutumu, ayrı bir iç temsil diye bir şey olmaması; temsilin kendisinin açık, belgelenmiş ve garanti edilmiş olması gerektiği yönünde görünüyor. Umarım bir gün bu kararı geri alıp özel alanları destekler.
Pek çok şey doğrudan çalıştı; eski bir GDB ile garip bir hatayı bile debug edebildim.
Ben de Zig’e ikna oldum; bununla ilgili burada yazdım: https://news.ycombinator.com/item?id=44211041
Neden bu kadar nefret edildiğini pek anlamıyorum.
Göze çirkin gelen generic’ler C#’ta da TypeScript’te de var; borrow checker ise düşük seviye iş yaptıysanız anlaşılır bir kavram.
Zig üzerine yapılmış araçlar arasında gerçekten hayran kaldığım şey bun; bun’ı kullanmaya başladıktan sonra hayatım çok daha basitleşti.
Rust ile yapılmış uv için de benzer şeyler söylenebilir.
Konsollar genelde C/C++ olmayan şeyleri sevmez; ama Zig C’ye dönüştürülebildiği için belki tamamen dışlanmayabilir.
“Modern derleyiciler bile dil spesifikasyonunu ihlal ediyor (Clang, yan etkisi olmayan tüm döngülerin sonlandığını varsayıyor)” iddiasına gelince; derleyicilerin zaman zaman spesifikasyonu ihlal ettiğinden şüphem yok, ama bu durumda Clang en azından C11 ve sonrası açısından haklı.
C11’de, denetim ifadesi sabit ifade olmayan ve giriş/çıkış, volatile erişim, senkronizasyon ya da atomik işlem yapmayan döngüler için implementasyonun bunların sonlandığını varsayabileceği söyleniyor.
Bu yüzden C’deki basit sonsuz döngü
for (;;);gerçekten sonsuz döngü olarak derlenmeli; Rust’ın daha az opakloop {}ifadesi de öyle olmalı.Ama LLVM, her zaman C++ derleyicisi yapmadıklarını unutan insanlar tarafından geliştirildiği için, Rust “bana sonsuz döngü ver” dediğinde LLVM’in “C++’a göre böyle bir şey yok, optimize edeceğim” dediği bir nokta vardı; bu da başka bir dile yanlış uygulanmış bir durumdu.
String karşılaştırmasını inline etmek ve açmak için illa comptime gerekmez.
C’de de mümkün: https://godbolt.org/z/6edWbqnfT
Yazım hatasını düzelttim.
Daha iyi bir örnek şu: https://github.com/RetroDev256/comptime_suffix_automaton
Yine de bağlantısını verdiğin godbolt kodu aslında iki kötü örnekten birini de gösteriyor
JavaScript örneğiyle Zig/Rust örneklerini karşılaştırmanın iyi bir karşılaştırma olmadığını düşünüyorum
Zig ve Rust derleyicilerine çok modern bir hedef CPU seçmeleri söylenmiş, ama V8 aynı koşullarda değil gibi görünüyor
Optimizasyon yapan JIT de koşullar uygunsa vektörleştirme yapabilir
Bu arada modern dillerin çoğu string’ler için aynı optimizasyonu uygular; C++ örneği burada: https://godbolt.org/z/TM5qdbTqh
Zig örneği sabit boyutlu, tipi bilinen bir dizi kullanıyor; JS kodu ise çalışma zamanında “generic”, yani
xveyherhangi bir nesne olabilirJS’de bu bedeli ödemek gerektiği doğru, ama ironik biçimde bu özel örnekte tip bilgisini JIT’e daha iyi aktarabilirsiniz
Bu fonksiyonu her zaman aynı boyutta Float64Array ile çağıracak şekilde düzenlerseniz JIT bunu bilir ve daha hızlı bir döngü üretebilir. Vektörleştirme olmasa bile çok daha iyi olur
Ancak typed array’lerin başlatma maliyeti yüksek olduğundan, büyük bir typed array’i bir kez ayırıp çok kez yeniden kullanmadıkça pratikte pek kullanılmaz
Ayrıca yazıda JS bytecode’unun epey şiştiği söylenmiş; muhtemelen JIT’in
65536değerinin iki dizinin uzunluğuna eşit olduğunu garanti edemeyip guard eklediği kısım bunun büyük nedeniYine de gerçekte kimse
fordöngüsünü öyle yazmaz,i < x.lengthdiye yazar; bu durumda JIT en azından dizi kontrollerinden birini kaldırırtargetdaha eski bir CPU’ya değiştirilebilirJS hedefinin sınırlamalarını düşünemediğim için üzgünüm
Bağladığın C++ örneği Clang’in C++’ta neler yapabildiğini gösteren iyi bir örnek, ama Zig’in belirli bir CPU’yu hedefleyerek derlediği düşünülse bile üretilen assembly biraz hayal kırıklığı yaratıyor
https://github.com/RetroDev256/comptime_suffix_automaton için bir C++ portu görmek çok ilginç olurdu
Bu, C++ derleyicisinin temiz biçimde çıkarım yapamayacağı bir comptime kullanımı
“Yüksek seviyeli dillerde, düşük seviyeli dillerde bolca bulunan niyet eksik” cümlesinin gerçekten doğru olup olmadığından emin değilim
Niyeti ifade etme yüksek seviye/düşük seviye spektrumunun bir unsuru değil gibi; aksine niyeti daha ayrıntılı ifade etmenin ne kadar çok yolu varsa, o kadar yüksek seviyeye yakın olmalı
Neyin başarılmak istendiğinden ziyade makineye ne yaptırmak istediğiniz konusundaki niyet
purchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?;gibi bir kod niyetle dolu, ama sonunda hangi makine kodunun çıkacağını bilemezsinizŞu
fordöngüsü sözdizimi korkunçYan yana iki liste var ve bir listedeki öğenin konumu diğer listedeki öğenin konumuna karşılık geliyor, öyle mi?
Sadece bakmak bile gözümü yoruyor
Modern diller parser’a türlü “sihirler” eklemeye ve kodun her yanına küçük semboller serpiştirmeye başladığından beri yanlış yola girmiş gibi
Saatlerce bakmak isteyeceğim bir biçim değil
Bu yüzden Zig’in tam da bu işi kolaylaştıran, ama ne olduğunu da açıkça gösteren bir sözdizimi sunması doğal
Kişisel olarak oldukça iyi yapıldığını düşünüyorum; neden gözünü yorduğunu merak ediyorum
Zig’in allocator modeli gerçekten hoşuma gidiyor
Go’da da garbage collection yerine istek başına allocator gibi bir şey kullanabilsek iyi olurdu
Dilin kendisinde sahiplik kurallarını ifade edip zorlayacak bir yol olmadığından, sonunda yalnızca sözdizimi biraz farklı olan C yazıp işlerin yolunda gitmesini ummak gibi oluyor
Garbage collection olmadığı durumda C++ bile Go’dan çok daha güvenli
Rust’ın bellek modeli, derleyicinin fonksiyon argümanlarının asla alias referansı olmadığını her zaman varsayabilmesini sağlar; Zig’de ise bunu elle belirtmek gerekir
Bu tür alias belirtimlerinden kaçınılıyor
Çünkü anlayan kişi az ve yanlış kullanılırsa kodda anlaşılması zor bug’lar oluşabilir
Zig’in comptime esnekliğinin başka dillere iyi iyileştirmeler getirdiği iddiasına gelirsek, derleme zamanında fonksiyon çalıştırma ve sabit argüman alan fonksiyonları D 2007’de tanıttı ve birçok dilin benzer şeyler benimsemesine yol açtı
https://dlang.org/spec/function.html#interpretation
Zig’i seviyorum ama “Zig’in laf kalabalığını seviyorum” demek kulağa garip geliyor
C’nin birçok köşede fazla gevşek olduğu doğru, ancak Zig mevcut hâliyle biraz fazla ters yöne savrulmuş durumda ve bazen yorum gibi duran tip belirtimi gürültüsü çok artabiliyor
Özellikle ifadelerde açık tamsayı cast’leri böyle; bununla ilgili burada biraz yazmıştım: https://floooh.github.io/2024/08/24/zig-and-emulators.html
Performans açısından Zig kodu benzer C kodundan daha hızlıysa, bu genellikle Zig’in daha agresif LLVM optimizasyon ayarlarından kaynaklanır
Örneğin Zig varsayılan olarak
-march=nativeve tüm program optimizasyonu kullanır, ayrıca projedeki tüm Zig kodunu tek bir derleme birimi olarak derlerunreachable’ı optimizasyon ipucu olarak kullanmak gibi neredeyse tüm “numaralar” C’de de mümkündür; yalnızca zaman zaman standart dışı dil uzantıları gerekirC derleyicileri, özellikle Clang, sabit katlamayı da çok agresif yapar ve derin çağrı yığınları olsa bile sabite katlanabilen büyük kod bölgelerini küçültebilir
Bu yüzden yalnızca kod üretimine bakıldığında çoğu zaman Zig’in comptime’ı ile büyük bir fark yoktur
comptime’ın avantajı, sessizce çalışma zamanı koduna geri düşmemesidir; comptime olmayan kod da C’deki gibi aynı sabit katlama optimizasyonlarına tabi olur
Örneğin “saf” bir non-comptime fonksiyon sabit argümanlarla çağrılırsa derleyici yine de fonksiyon çağrısını sonucuyla değiştirir
Özetle C kodu Zig kodundan yavaşsa C derleyici ayarlarını kontrol etmek gerekir. Sonuçta optimizasyonun ağır işi LLVM’in altında gerçekleşir
fn signExtendCast(comptime T: type, x: anytype) T { const ST = std.meta.Int(.signed, @bitSizeOf(T)); const SX = std.meta.Int(.signed, @bitSizeOf(@TypeOf(x))); return @bitCast(@as(ST, @as(SX, @bitCast(x)))); }export fn addi8(addr: u16, offset: u8) u16 { return addr +% signExtendCast(u16, offset); }Aynı assembly’ye derlenir, yeniden kullanılabilir ve niyeti de daha net hâle getirir
Buna katılıyorum
Virgil 2006’dan beri tüm dili derleme zamanında kullanabiliyor ve tüm program derlemesini de destekliyordu
Ancak Virgil LLVM’i hedeflemediği için hız karşılaştırması eninde sonunda iki derleyici arka ucunun karşılaştırmasına dönüşüyor
Virgil, bu derleme modelinin mümkün kıldığı erişilebilirlik analizi ve özelleştirme optimizasyonlarına büyük ölçüde yaslanıyor
Örneğin metot çağrılarını agresif biçimde devirtualize eder, erişilemeyen alanları ve nesneleri kaldırır, alanlar ve heap nesneleri üzerinden sabit yükseltmesi yapar ve polimorfik kodu tamamen monomorfik hâle getirir
Bunun tek nedeni, yapay zekanın bunları daha kolay işlemesi
Kodlamada yapay zeka kullanmanın iyi bir fikir olup olmadığı ya da ne ölçüde iyi olduğu ayrı bir mesele; ancak birçok geliştirici buna inanıyor ve diller de onları kucaklamaya çalışacak