Hak Ettiğimiz Rust çağrı kuralı (calling convention)
(mcyoung.xyz)- Rust’ın mevcut
extern "Rust"çağrı kuralı LLVM’in C çağrı kuralı yoluna dayanıyor; karmaşık değer aktarımında register kullanımı konusunda muhafazakâr kaldığı için daha iyi kod üretimi fırsatlarını kaçırıyor - Temel fikir, crate düzeyindeki
-Zcallconvbayrağıyla mevcut yöntem olanlegacyile yeni register odaklı yöntem olanfastı ayırmak ve optimizasyon derlemelerinde daha agresif bir ABI kullanmak - LLVM’e doğrudan yeni bir çağrı kuralı eklemeden de sabit LLVM fonksiyon imzaları ve
poisondeğerleri ile kullanılmayan register argümanlarını maliyetsiz biçimde boş bırakarak argüman yerleşimini kontrol etmek mümkün - Struct, enum, union,
bool,Resultgibi Rust tipleri; padding hariç geçerli boyut, düzleştirme, bit paketleme ve stack/register bölme sezgiselleriyle daha sıkı biçimde aktarılabilir - Fonksiyon gövdesi, borrow checker bilgisi ve profil bilgisi ABI kararlarına yansıtılırsa daha güçlü optimizasyonlar mümkün olur; ancak rustc ABI kod üretiminin karmaşıklığı ve LLVM uzmanlığı eksikliği pratik engeller olarak kalıyor
Rust’ın şu anda kaçırdığı çağrı kuralı optimizasyonu
- Çağrı kuralı (calling convention), fonksiyon argümanları ve dönüş değerlerinin nasıl aktarılacağını, hangi register’ların kullanılacağını, prologue/epilogue ve unwinding işlemlerinin nasıl ele alınacağını belirleyen ABI’nin bir parçasıdır
- Rust kendi unspecified çağrı kuralını tanımlar; ancak pratikte LLVM’in yerleşik C çağrı kuralına indirilir ve LLVM’in prologue/epilogue kod üretimine dayanır
- rustc, Clang’in üreteceğine benzer LLVM fonksiyon imzaları üretmek için muhafazakâr davranır
- Debugger’ın bozulma olasılığını azaltabilir
- Clang’in pek kullanmadığı ABI kod üretim yollarıyla LLVM hatalarını tetikleme olasılığını düşürebilir
- ELF tabanlı sistemlerde DWARF, Linux C ABI’sini sabitlemediğinden, yazının kapsamı içinde hata ayıklanabilirlik temel sorun olarak görülmez
- Basit bir örnek olarak
fn extract(arr: [i32; 3]) -> i32içinde 12 baytlık dizi register yerine pointer ile aktarılırextern "C"eklendiğinde aynı[i32; 3],rdiversiiçine packed edilerek aktarılır- Bu, Rust’ın varsayılan yolunun Linux C ABI’sinden bile daha muhafazakâr olduğu bir örnektir
-Zcallconv: legacy ve fast’i ayırma yöntemi
extern "Rust"ın mevcut çağrı kuralı korunur; ancak crate derleme bayrağı-Zcallconvile kullanılacak çağrı kuralı seçilir-Zcallconv=legacy: mevcut yöntem-Zcallconv=fast: yeni tasarlanacak register odaklı yöntem-O, otomatik olarak-Zcallconv=fastayarlayabilir
fastçağrı kuralı argümanları C ABI sırasına göre yerleştirmediğinden, x86’nın alışılagelmiş register sırasını bekleyen biri için kafa karıştırıcı olabilir- WASM gibi register ve spilling kavramı olmayan hedeflerde
-Zcallconv=fastdesteklenmeyebilir - Optimizasyonun kapalı olduğu debug derlemelerinde
fastdaha kötü kod üretebilir; bu nedenle etkinleştirilmesi uygun olmayabilir - Fonksiyon pointer’ları ve
extern "Rust" {}blokları için ayrı kısıtlar gerekir- Bayrak crate düzeyindedir; fakat fonksiyon pointer’ının hangi
extern "Rust"sürümünü kullandığını ifade etmek zordur - Fonksiyon pointer çağrıları yavaş ve nadir bir yol olarak görülüp
-Zcallconv=legacyzorunlu kılınabilir - Gerekirse çağrı kuralını dönüştüren bir shim üretilebilir
- Unmangled sembollerin çağrılabildiği yollar nedeniyle
#[no_mangle]sembollerinin de legacy çağrı kuralını kullanması sağlanabilir
- Bayrak crate düzeyindedir; fakat fonksiyon pointer’ının hangi
LLVM’i dolaylı olarak yönlendirme yöntemi
- İdeal olarak LLVM’e “bu argüman şu register’da, bu dönüş değeri şu register’da” gibi doğrudan çağrı kuralı belirtmek istenir; ancak LLVM’e çağrı kuralı eklemek çok miktarda C++ kodu yazmayı gerektirir
- Bunun yerine aşağıdaki süreçle özel bir çağrı kuralına yakın etki elde edilebilir
- Hedef triple’a göre register üzerinden aktarılabilecek maksimum değer sayısı belirlenir
- Dönüş değerinin çıktı register’larına sığıp sığmadığı ya da
sretniteliğine sahip ek birptrargümanıyla by-reference döndürülmesi gerekip gerekmediği belirlenir - Çok büyük by-value argümanlar by-reference’a indirilir
- Hangi argümanların register’dan gönderileceği seçilerek register alanı kullanım oranı maksimize edilir
- Kalan argümanlar stack’te tutulur
- LLVM IR fonksiyon imzası
i64,ptr,double,<2 x i64>gibi aggregate olmayan argümanlardan oluşturulur - Fonksiyon prologue’unda register girdileri Rust düzeyindeki argümanlara decode edilir
- Fonksiyon bitiş bloğunda dönüş değeri gerekli çıktı biçimine encode edilip
retyapılır - Adresi alınabilen non-polymorphic, non-inline fonksiyonlar için fonksiyon pointer kimliğini korumak üzere legacy shim oluşturulur
- Hangi değerlerin register’a konacağına karar verme problemi sırt çantası problemi (knapsack problem) gibidir ve NP-hard’dır; gerçek uygulamada sezgiseller gerekir
- Bu bilgi çok geç hesaplanmayıp yeniden hesaplamayı önlemek için
rmetaiçine konabilir - Rust’ın ABI’si her sürümde kırıldığı için, farklı Rust derleyicilerinin ürettiği kodların link edilmemesi gerektiği koşulu zaten mevcut durumla uyumludur
LLVM’in izin verdiği register aktarım sınırı
- LLVM, aggregate by-value argümanları fonksiyona aktarırken mümkün olduğunca çok register’a “explode” etmeye çalışır
- x86’da LLVM’in register üzerinden aktarabildiği girdiler kabaca şöyledir
- 6 tamsayı
- 8 SSE vektörü
- Dönüş için bunun yarısı: 3 tamsayı ve 4 vektör
aarch64-unknown-linuxüzerinde hem girdi hem çıktı için 8 tamsayı ve 8 vektör mümkündür- x86’daki tüm
-Zcallconv=fastfonksiyonlarının aynı sayıda by-register argümana sahip olması sağlanabilir- Tamsayı register’ları için 6 argüman
xmm0danxmm7ye kadar 8 vektör argümanı- Gerçek pointer aktarımında ilgili
i64,ptrile değiştirilir doubleaktarımında<2 x i64>yuvasının yerini alır
- Çoğu fonksiyon 176 bayt aktarmasa bile kullanılmayan argümanlara LLVM
poisongeçirilerek ek maliyetten kaçınılabilir- LLVM,
poisonı o anda en uygun değer olarak görebilir - Register argümanı olarak
poisonaktarıldığında “zaten o register’da bulunan değer” gibi ele alınabilir; bu yüzden register’a dokunmak gerekmez - Örnekte
load_rcx()pointer’ırcxile alır; kalan 13 register’apoisonyükleyen kod optimizasyon sırasında hiç kod üretmez
- LLVM,
- Bu yöntem argüman aktarımını neredeyse tamamen kontrol etmeyi sağlar; ancak girdi ve çıktı için aynı register’ı kullanan ideal durum mimariden mimariye değişir
- ARM ve RISC-V, girdi ve çıktıda aynı register’ı kullanan yapıya daha yakındır
- x86 öyle değildir; ancak register ayırma sırasını farklı varsayarak gereksiz register taşımaları azaltılabilir
Rust tiplerini register’lara daha iyi uydurma
- Rust struct’ları ve union’ları ele alınırken rustc’nin kullanıcı tiplerini zaten temel aggregate ve union olarak işlediği varsayılır; hangi parçaların register’da tutulacağına karar verilir
- Dönüş değerlerinde struct’ın toplam boyutundan çok padding hariç geçerli boyutu önemlidir
[(u64, u32); 2]toplamda 32 bayttır, ancak 8 baytı padding’dir(u64, u32, u64, u32)olarak düzleştirilip boyuta göre(u64, u64, u32, u32)sırasına dizilirse 24 bayt olur- x86’nın 3 tamsayı dönüş register’ına sığabilir
- Geçerli boyut, non-
undefbit sayısı olarak tanımlanır[(u64, u32); 2]192 bittirbool1 bittircharteknik olarak 21 bittir; ancak basitleştirme içinu32alias’ı gibi ele alınır
- Çok sayıda
booliçeren struct’lar, birden fazlaboolu tek register’a bit paketleme ile döndürebilir - Argüman tarafı daha zordur ve aşağıdaki sezgiseller uygulanabilir
- Geçerli boyutu toplam by-register girdi alanından büyük olan argümanlar by-reference’a indirilir
- x86 için toplam girdi alanı 176 bayt, yani 1408 bittir
- enum, discriminant ve union çifti hâline dönüştürülür
Option<i32>içsel olarak(union { i32, () }, i1)gibi görülebilirOption<Option<i32>>ise(union { i32, (), () }, i2)gibi görülebilir
- union, başlatılmamış bitlere keyfî olarak dokunulmasına izin verebildiğinden genellikle
u8dizisi gibi aktarılır - Yalnızca bir non-empty variant’a sahip union, o variant ile değiştirilir
- Dönüştürülen argüman pointer, tamsayı, float, bool gibi primitive’lere düzleştirilir
u128,f64gibi küçük argüman register’ından büyük alanlar bölünebilir- Primitive listesi geçerli boyuta göre sıralanır ve register’a sığan en büyük prefix seçilir
- Kalanlar stack’te tutulur
- Stack’e giden kısım pointer boyutunun küçük bir katından büyükse bellek trafiğini azaltmak için pointer-on-the-stack’e indirilir
- Register ile aktarılan değerler büyükten küçüğe yerleştirilir;
boolise register başına 64 adede kadar bit paketlenir
Karmaşık Rust fonksiyonu örneği ve mevcut rustc’nin sınırı
Option<usize>,&dyn Context,&str,[char; 6],Optionsstruct’ını alando_thingörneğinde, düzleştirme ve sıralama sonrasında ham LLVM argümanlarının tamamı register’lara sığabilir- Örneğin raw argument LLVM tipleri şu biçimde olur
gprs: i64, ptr, ptr, ptr, i64, i32, i32xmm0: i32, i32, i32, i32xmm1: i32, i1, i1, i1, i1
- Fonksiyon prologue’u primitive’leri çıkarır ve Rust düzeyindeki değerlere yeniden birleştirir
Option<usize>:{ i64, i1 }- trait object:
{ ptr, ptr } &str:{ ptr, i64 }[char; 6]:[6 x i32]Options:{ i32, i1, i1, i1 }
- Argüman değerlerini gerçekten materialize eden komutlara
!dbgmetadata eklenirse gdb argüman değerlerini yazdırırken daha iyi sonuç verebilir - Mevcut rustc aynı fonksiyon için LLVM’e pointer boyutunda 8 parametre geçirir; sonuçta 6 tamsayı register’ının tamamı kullanılır ve 2 değer stack üzerinden geçirilir
Dönüş değeri ve Result optimizasyon olanağı
- Bu tasarım mümkün olan tüm çağrı kuralı optimizasyonlarını kapsamaz
- Bazı durumlarda x86’daki AVX register’ları gibi ek register’lar kullanılabilir
- Struct’ı register ve stack arasında bölerek aktarma yöntemi de değerlendirilebilir
Resultdönüşü için ayrı optimizasyon fırsatları vardır?ile birden çok fonksiyon katmanından geçildiğinde yinelenen register taşımaları artabilirResultregister’a sığmayacak kadar büyükse, her?çağrı stack’inde ok bit’ini bellekten yükleyip kontrol etmek gerekir- Alternatif olarak error bir out-parameter pointer olarak tutulabilir; ok variant payload ve is-ok bit ise
Option<T>olarak döndürülebilir ?işlecininIntoçağrısını beraberinde getirdiği ayrıntıları ele almak zor olsa da uygulanabilir
Optimizasyona bağımlı ABI
- Rust, C’den farklı olarak
-Zcallconv=fastiçinde çağıranın göreceği ABI’yi oluştururken fonksiyon gövdesini görebilir - Crate, fonksiyon bazında register aktarımı açısından kesin ABI’yi ilan edebilir
- En basit optimizasyon, kullanılmayan argümanları ABI’den atmaktır
- Fonksiyon hiçbir parametreyi kullanmıyorsa o argüman için register harcanmaz
&Targümanı saklanmıyor, raw pointer’a dönüştürülmüyor veTküçük olupT: Freezeise referans yerine pointee’nin kendisi by-value aktarılabilirHashMap::get()gibi API’ler adaydır- key
i32gibi bir tipse şu anda tamsayıyı stack’e spill edip onun pointer’ını geçirmek gerekir - Bu bellek trafiği önlenebilir
- key
- Profil tabanlı ABI daha agresif bir biçimdir
- Daha hot argümanlara register ayırma sırasında öncelik verilebilir
- Büyük bir struct referansla alınsa bile, hot olan 3
i64alanı çağıran tarafından önceden load edilip hem pointer hem register üzerinden geçirilebilir - Callee zaten o load’u yapacağı için ek maliyet görmez
- Instrumentation profile, yalnızca ABI’si farklı olan fonksiyon kopyalamayı da gerekçelendirebilir
Neden hâlâ yapılmadı?
- Rust, C++’a göre daha az ABI kısıtına sahip olduğundan daha iyi kod üretebilir ve bu fikir Go register ABI’nin pratikte kullandığı yöntemle de örtüşür
- İlk engel ABI kod üretimi karmaşıklığıdır
- LLVM yararlı kontrol düğmelerini neredeyse hiç sunmaz
- rustc içinde de dostça bir alan değildir
- Yanlış uygulanırsa kullanılabilirlik açısından kötü sonuçlar doğurabilir
- Bir diğer engel uzmanlık eksikliğidir
- rustc katkıcıları arasında LLVM semantiğini ve kod üretimi özelliklerini yeterince anlayıp iyi kod çıkarabilecek ve LLVM’i çökertmeyecek kişi sayısı azdır
- Derleme süresi de yük olabilir
- Fonksiyon imzaları karmaşıklaştıkça LLVM’in işlemesi gereken prologue/epilogue kodu artar
- Ancak
-Zcallconvyalnızca optimizasyon açıkken kullanılmak üzere tasarlandığından, bu kesin bir dezavantaj olarak görülmez
- Rust’ın ABI kodu bus factor’ı düşük bir alandır ve LLVM bilgisi, Rust derleyici ekibinin daha optimize kod üretmesine yardımcı olmak için doğrudan kullanılabilir
1 yorum
Hacker News yorumları
Çağrı kuralını optimize ederken kilit nokta, kafanızda iyi göründüğünü düşündüğünüz biçimleri tartmak değil, performansı ölçmektir.
Kod hızlıysa iyidir; hızlı görünüyorsa değil.
Yazarın kötü kod dediği şeyin, hiç de sezgisel olmayan nedenlerle en hızlı olduğu durumlar da olabilir; bunu ancak büyük benchmark’larda ölçerek anlayabilirsiniz.
Kötü görünen bir çağrı kuralının iyi çalışmasının nedenlerinden biri, argüman register’larını idareli kullanarak register ayırıcıyı biraz daha rahatlatmasıdır.
Ayrıca günümüz CPU’ları C derleyicilerinin ürettiği komut akışına göre optimize edilmiştir; özellikle MSVC gibi stack üzerinden aktarımı beklenenden sık yapan C derleyicisi tarzı kod üretirseniz CPU’nun en verimli noktasına denk gelebilirsiniz.
Inlining o kadar iyi çalışıyor ki sıcak yollarda çağrılar nadir bir sınır hâline geliyor; bu sınır biraz dağınık olsa bile başka şeyleri basitleştiriyorsa sorun olmayabilir.
Buradaki değişikliğin kötü olduğu anlamına gelmiyor; ama yalnızca tuhaf görünen koda bakıp ölçüm yapmadan tartışmak tuhaf.
JavaScriptCore’da işim çağrı kuralı optimizasyonuydu ve gerçek, büyük kod tabanlarında kötü görünen stack üzerinden aktarım kodunun kazandığı durumlar şaşırtıcı derecede sık yaşanıyordu.
Ancak performans ölçüm sonuçlarının tek ölçüt olmaması gerektiğini düşünüyorum.
“Günümüz” CPU’larının optimize edildiği ifadesindeki önemli kelime günümüz; CPU’lar sürekli değiştiği için çağrı kuralı uzun vadeli bir tasarım olmalı.
Bu yüzden, ne yazık ki C++’ın yaptığı biçimden çok uzaklaşmamak avantajlıdır. Çünkü gelecekteki işlemci optimizasyonlarının da büyük olasılıkla o tarafa hedeflenmesi muhtemel.
Aynı zamanda, argüman register’larını idareli kullanmak gibi kolay kolay değişmeyecek genel ilkeleri dikkate alarak çağrı kuralını sağlam ve geleceğe dönük yapmak iyi olur.
Rust’ın son birkaç yılda tuhaflık toleransı (https://steveklabnik.com/writing/the-language-strangeness-bu...) açısından fazla muhafazakârlaştığını düşündüğüm için bunu söylemek biraz garip geliyor. Sonuçta farklı olmadan daha iyi olmak mümkün değil.
Fonksiyon başlar başlamaz parametrenin adresini alıp bilinmeyen bir fonksiyona geçiriyorsa, zaten stack’e dökmek zorunda kalırsınız.
Fonksiyon gövdesine dayalı çağrı kuralı optimizasyonunu görmek ilginç olurdu. C’deki statik fonksiyonlar için, adresleri alınmadığı sürece güvenli gibi görünüyor.
JIT, tek bir assembly satırı üretmeden önce bile gerçekten çalışmakta olan CPU hakkında çok bilgi toplamış durumdadır; bu yüzden bu problemde avantajlıdır.
Tamamen statik derlenmiş kodda ise çalışma zamanındaki mimari özellik kümesi bilinemez; bu nedenle en çok optimize etmek istediğiniz kodda aksine sık sık inlining bariyerleriyle karşılaşırsınız.
Mevcut Rust küçük platformlarda bu konuda zayıf görünüyor ve çağrı kuralı
Resultdönüşleriyle ilgili olarak yardımcı olabilir.Yine de stack üzerinden aktarımın ampirik avantajının, çok sayıda register’a sahip ARMV8 CPU’lara veya RISC-V’ye geçildiğinde de geçerli olup olmayacağını merak ediyorum.
Makul bir taslak, ancak çağıranın sakladığı/çağrılanın sakladığı ayrımı eksik ve giriş register’larının bir kısmını çıktıya atama gibi yaygın bir hata var
Debugger’ın C’den farklı bir çağrı kuralını anlayacağını beklemek de iyimser. DWARF neyi encode edebiliyor olursa olsun, pratikte feci biçimde başarısız olma olasılığı yüksek
Optimizasyon ayarlarına göre ABI’yi değiştirmek ayrı derleme ile çok kötü etkileşir
Argümanları bin packing yapar gibi yeniden yerleştirme yöntemi çalışır, ama derleyici karmaşıklığını ciddi artırır; soldan sağa ilk uyan yerleştirmeye kıyasla buna değip değmediğini bilmiyorum. Geliştiricinin argümanların nereye gideceğini tahmin etmesi de zorlaşır
Adresi dışarı kaçan fonksiyonlarla kaçmayan fonksiyonlar için farklı çağrı kuralları belirleme yönündeki genel fikir mantıklı. Empedans eşlemesi yapan prologue’u ayırma yöntemi de iyi çalışır
Rust’ın C’den farklı bir çağrı kuralına sahip olmaya istekli olması gerekir, ama bunun tüm fonksiyonların kullandığı, hard-code edilmiş tek bir kural olması gerekip gerekmediğinden emin değilim. Bunu tip sistemine koymak doğal görünüyor; geliştiricinin çağrı kuralını kontrol edebilmesini sağlamak da assembly’nin performans avantajlarından birini ortadan kaldırır
Çağıran açısından zaten iki fonksiyon çağrısı arasında çıkış register’larını boşaltmak gerekir ve sistem çağrısı kurallarında da oldukça yaygın kullanılır
Amaç çağrılan tarafın giriş değerlerini olduğu gibi tutarken çıkış değerini hazırlamasını kolaylaştırmak mı, diye düşünüyorum. Öyleyse çıkış register’larını giriş sırasının sonuna koyup çakışmayı önlemeye çalışmayı anlayabilirim, ama herhangi bir çakışmayı tamamen yasaklamak için iyi bir neden pek göremiyorum
Function A’nınFunction B,Function C,Function D’yi çağırdığı türden bir zincirde ara fonksiyonların argümanlarını başka kurallara çevirerek overhead’i azaltan optimizasyon da aynı anda engellenirBöyle bir optimizasyonu korurken kontrol yetkisine de izin veren semantiğin ne olacağı, hatta bunun fiilen bir hayalden ibaret olup olmadığı kuşkulu
Gerçekte assembly çoğu derleyici optimizasyonunun hedefi olmadığı için performans açısından dezavantajlıdır. “Davranışa bakıp tamamen gereksiz olduğunu saptama ve tümüyle kaldırma” optimizasyonundan da çoğu zaman yararlanamaz; artık 1990’larda değiliz
Yine de bu tür optimizasyonların dikkate bile alınamadığı durumlarda, inline assembly’nin kesin geri kalacağı yerin en fazla profil tabanlı optimizasyon olduğunu düşünüyorum. Çünkü uygulama geliştiricisi kodun davranışını kusursuz bilir, derleyici geliştiricisi ise bilmez
Çağrı overhead’i, ilgili sıcak sınırları kapsayana kadar daha fazla assembly yazarak ortadan kaldırılabilir
boolsöz konusu olduğunda bağımlılık zinciri oluşturabilirx64’te
bool’ları önce register’a koyup, shift edip, sonucu OR’lamaktan daha iyi bir yöntem pek görünmüyorBasit yöntem uzunluğu 64 olan bir bağımlılık zinciri oluşturur ve 64 cycle ceza doğurabilir; ama iyi yapılırsa 6 cycle’a, gerçekçi olarak da yaklaşık 12 cycle’a indirilebilir gibi
Fakat 64 adet
bool’un nereden geldiği de sorun. O kadar çok register yok, dolayısıyla sonunda stack’ten yeniden okumak gerekirRust ABI’si struct içindeki
bool’u zaten böyle sıkı paketliyorsa bu iş zaten yapılacaktır, ama tam bilmiyorumÜstelik çağıran tarafın da hepsini yeniden açması gerekir
Derleyiciye değerleri stack’teki sonuç alanına akıtmayı öğretmek daha kolay ve muhtemelen daha performanslı olur
Bu durumda değerleri register’a koymanın pratikte ne kadar fayda sağladığı şüpheli
C çağrı kuralı pek iyi değil
C çağrı kuralını değiştiremeyeceğimiz doğru, ama bu durumu daha az üzücü yapmıyor
Kullanılabilir tüm caller-saved register’lar argümanlar ve dönüş değerleri için kullanılmalı; oysa geleneksel SysV ABI’de dönüş değeri için yalnızca bir, bazen iki register kullanılır
struct Point3D { long x, y, z }döndürürkenPoint3Drax,rdi,rsiiçine konabilecekken stack’e dökülüyorBaşka sistemlerde başka numaralar da var. Doğru hatırlıyorsam SBCL’de bir fonksiyon birden fazla değer döndürdüğünde çıkışta carry flag ayarlanıyor. Örneğin
Result’ın hata içerip içermediğini belirtmek için carry flag kullanılsa iyi olmaz mı diye düşünüyorumC çağrı kuralı fiilen C’nin desteklediği şeyi, yani tek argüman dönüşünü destekler. Struct döndürme bile doğru düzgün değil
C’de bu daha çok “bunu bilmiyor muydun” durumuna yakın; C++ tarafında ise “zaten inline etsen yeter” tarzına dönüşüyor
Öte yandan memory spill gerçekten olur. Örneğin SPARC’ın geniş register alanı ve window’ları basit fonksiyonlarda çok sayıda kullanılmayan register bırakıyordu; register ring’i spill etmek de cache’i bozan büyük stack kullanımına yol açıyordu
x86’da veriyi “gereken yere” yeniden yerleştiren çok sayıda
movolsa bile sonuçta çoğu zaman daha hızlıydıYalnızca çağrılan koduna bakınca “bu argüman burada, şu dönüş değeri şurada olursa kesin daha hızlı olur” demek geliyor insanın içinden, ama çağıranı bilemezsiniz
Argüman hazırlığının aynen geçip geçmeyeceği, dönüş değerinin sıcak biçimde tüketilip tüketilmeyeceği garanti edilemez. Örneğin
struct Point { x: i32, y: i32, z: i32 }argüman/dönüş olarak kullanılıyorsa ve çağıran bir döngüdemystruct.deepinside.point[i] = func(mystruct.deepinside.point[i])gibi bir şey yapıyorsa, register’a koyup çıkarmak overhead’e dönüşebilir veya vektörleştirmeyi engelleyebilirÇağrılan bunu bilemez; derleyicinin iki tarafı da görüp inline edebildiği durumlar bunun istisnasıdır
Çağrı ile ilgili en kolay kazanım, neredeyse tüm C ABI’lerine işlemiş fonksiyon ilkel tek bir değer döndürür varsayımını ortadan kaldırmak gibi görünüyor. Geri kalanı için çok sayıda benchmark ve kod üretimi istatistiği gerekir
Rust’ta yapıların istenenden daha büyük olmasına yol açan can sıkıcı bir başka ayrıntı daha var
NoneveyaSome(u8)olan 8 adetOptionalanı içeren birFooyapısını düşünürsek, C’de 1 bitlik 8boolve 8 adetuint8_tile toplam 9 baytta ifade etmek mümkünRust’ta ise 1 baytlık ayırt edici ve
uint8_t8 kez tekrarlandığı için 16 bayt oluyorBunun nedeni, yapının alanların ödünç alınmasını sağlayabilmesi gerekmesi. Elinizde
&Foovarsa derleyicinin&Foo::some_field, yani bir&Optionoluşturabilmesi gerekir; bu&Optionda programdaki diğer tüm&Option’larla aynı biçimde olmalıdırBu yüzden içerideki
Option, programdaki diğerOption’larla aynı yerleşime sahip olmalı: yani kendi ayırt edici bitlerinin bayta yuvarlanmış hâli veu8. Gerçekte&Foo::some_fieldhiç oluşturulmasa bile yapı bu maliyeti öderDaha büyük tiplerin
Option’larını düşünürsek durum daha da kötüleşir. 8Optionalanı olan bir yapıda her ayırt edici 2 bayta yuvarlanır, toplam 32 bayt eder; dörtte biri, hatta ayırt edicilerin kullanılmayan bitleri de dahil edilirse neredeyse yarısı ara padding olarak boşa gider. C’deki eşdeğer yapı için 18 bayt yeterlidirOptionkullanıldığında Rust yapısı 128 bayt, C yapısı ise 72 bayt olabilirElbette paketlenmiş ayırt ediciler için bir
u8ve 8 adetMaybeUninittutup,&FooiçinOption<&T>’ye,&mut FooiçinOption<&mut T>’ye eşleyen fonksiyonları kendiniz yazarak C’dekiyle aynı temsili uygulayabilirsiniz. Ancak bunu&Optionveya&mut Optionolarak yapamazsınızhttps://play.rust-lang.org/?version=stable&mode=debug&editio...
Esasen 8 adet
Optiontutan kullanıcı tanımlı bir tip tarif ediliyor; performansı önemsemeye başladığınızda içOptionişlemesini kendiniz yönetmeniz gerekirRust’ın hedefe uygun olduğunda seçip kullanabileceğiniz kullanışlı bir özellik sunması, bunu bir dezavantaj yapmaz
Anlatılan kullanım durumu görece nadir; gerçek bir performans darboğazıysa Rust’ta biraz daha zaman harcayıp uygulamak büyük bir sorun değil
Genel kullanımda
Option<_>tipinin sağladığı faydalar çok büyük olduğundan, bunu Rust’ın “can sıkıcı ayrıntısı” olarak görmek zorPolimorfik olmayan, inline edilmeyen bir fonksiyonun adresi fonksiyon işaretçisi olarak alınabiliyorsa
-Zcallconv=legacykullanan bir shim oluşturup gerçek uygulamayı hemen tail call ile çağırmaktan bahsediliyor; fonksiyon işaretçisi eşitliğini koruma niyetini anlıyorumAma legacy shim bir Rust çağrı kuralı fonksiyonunu tail call ile çağırırsa, çağrı kuralının dönüş değeri farkını düzeltemez, değil mi?
Biraz farklı bir konu ama şu anda Go ile Rust birlikte çalışabilirliği mümkün mü merak ediyorum
Eskiden araya Zig koyarak bunu başaran bir örnek gördüğümü hatırlıyorum ama bulamıyorum. Eski Rust kodum var ve onu yavaş yavaş Go’ya taşımak istiyorum
extern "C"FFI kullanarak Rust fonksiyonlarını çağırabilirsinizGitHub kod aramasında bunun nasıl kullanıldığını RustConf 2023’te sunmuştum (https://www.youtube.com/watch?v=KYdlqhb267c); sonrasında 1Password gibi yerlerde de benzer şeyler yaptıklarını duydum
C birlikte çalışabilirlik sınırının üzerinden tip taşımak zahmetli olduğu için pek eğlenceli değil, ama mümkün ve kod yeniden kullanımı da mümkün
extern "C"olarak bildirip, Go’dan C çağırır gibi çağırmanız yeterliTers yönü pek bilmiyorum
Yönetilen kod, serbest bırakacağı veya taşıyacağı belleğe sahip olabilmeli; yönetilmeyen kod ise belleğin ne zaman serbest bırakılacağını veya taşınacağını çıkarımlayabilmelidir
cgogibi şeyler Go’nun yönetilen kodundan yönetilmeyen belleğe FFI çağrılarını karıştırmanıza izin verir, ama bunun bir bedeli vardırBirbirini çağıran dillerin çöp toplayıcıyı paylaşmadığı uygulamalarda bu sorun her zaman ortaya çıkar
Yönetilen/yönetilmeyen kodu karıştırmak eski bir fikir olmakla birlikte hâlâ aktif olarak araştırılan bir konudur
Yerleşik runtime özellikle bunun için tasarlanmış değilse, yönetilmeyen koddan yönetilen koda çağrı yapmak neredeyse her zaman kötü bir fikirdir ve genellikle araya bir serileştirme katmanı girer
Bu ana işinizse kötü olduğunu düşünebilirsiniz, ama birkaç haftada bir koda döndüğümde neyi nasıl yaptığımı hatırlayamamaktan bıktım
Durum tutan bir Rust closure’ını Go koduna callback olarak geçirip bir Go standart kütüphane fonksiyonuna verdim; Rust closure’ının içindeki panic unwind’ı da buna dahildi
https://github.com/Voultapher/sort-research-rs/commit/df6c91...
Bölüm başlığının nasıl eğik ayarlandığını anlamak için Öğeyi İnceleye uzun süre baktım ama Safari araçlarında takıldım. Bunu nasıl yapmışlar?
.post-titleöğesinde:transform: skewY(-2deg) translate(-1rem, -0.4rem);element()fonksiyonunu (https://developer.mozilla.org/en-US/docs/Web/CSS/element) kullandığını sanmıştım; aslında yazı gövdesinin çok küçültülmüş bir kopyasıymışh1, h2, h3, h4, h5, h6içintransform:skewY(-2deg) translate(-1rem,0rem);,transform-origin:top;,font-style:italic;,text-decoration-line:underline;,text-decoration-color:goldenrod;,text-underline-offset:4%;,text-decoration-thickness:.25exuygulanmışBuna karşılık 2019 tarihli “How Swift Achieved Dynamic Linking Where Rust Couldn't” yazısı var
https://faultlore.com/blah/swift-abi/
Rust’ın hâlâ Rust düzeyi semantik için bir çağrı kuralına sahip olmaması üzücü; ama aynı zamanda o yazı, oraya varmak için gereken iş miktarının muazzam olduğunu gösteriyor
Apple, Swift’i uygulamaların güvenebileceği pratik bir sistem dili yapmak için güçlü biçimde motive olmuştu; Rust’ın ise böyle bir sponsoru yok
HN tartışması: https://news.ycombinator.com/item?id=21488415
Rust’ta da bu ödünleşme için daha fazla destek seçeneği olsa iyi olurdu; bunun https://github.com/rust-lang/rfcs/pull/3470 gibi şeylerle sınırlı kalması gerekmiyor
Mevcut Rust derleyicisi agresif biçimde inline edip sonra optimize ediyorsa, bunun zahmete değip değmeyeceği konusunda şüpheliyim
Çağrılan fonksiyon küçükse inline edilecektir; büyükse fonksiyonun içinde epey zaman harcanacağından çağrı ek yükü küçük olacaktır
dyn Trait, inline edilemez; dolayısıyla böyle bir değişiklik fayda sağlarÇağrıları ucuzlatabilirsek bu kadar agresif inline etmeye gerek kalmayabilir; bu da kod boyutuna ve derleme süresine yardımcı olabilir
Inline etmeye uygun olmayan karmaşık bir fonksiyonun belleğe birkaç kez erişme olasılığı yüksektir ve bu erişimin darboğaz olma ihtimali büyüktür
Yığın üzerinden aktarma, önbellek baskısını ve yükleme/saklamaları artırdığı için bu darboğazı daha da daraltır
Rust, fonksiyon çağrılarının kayda değer bir oranında argümanları en iyi şekilde aktarabilirse yalnızca birkaç L1 erişim döngüsünden kaçınmakla kalmaz, CPU’nun asıl bellek darboğazına daha hızlı ulaşmasını da sağlayabilir
Belki birkaç yüzde puanlık bir kazanç olabilir; ama şu an şarap içiyorum ve hesap yapmıyorum
x86 referans materyalindeki “Diana’s silk dress cost $89” ezber cümlesinin ne olduğunu açıklayabilir misiniz?