Hak ettiğimiz Rust çağrı kuralı (calling convention)
(mcyoung.xyz)Bu yazı, Rust dilinin Calling Convention mekanizmasının nasıl iyileştirilebileceğini ayrıntılı olarak açıklıyor.
Rust'ın mevcut Calling Convention yapısının sorunları
- Rust'ta şu anda çağrı kuralı (Calling Convention) açık biçimde tanımlanmış değil
- Pratikte LLVM'in varsayılan C çağrı kuralı kullanılıyor
- Rust şu anda muhafazakâr biçimde, Clang'in üretebileceği türden LLVM fonksiyon imzaları üretmeye çalışıyor
- Hata ayıklayıcılarla uyumluluk için
- LLVM hatalarından kaçınmak için
- Ancak fazla muhafazakâr olduğu için basit fonksiyonlarda bile kötü kod üretiyor
fn extract(arr: [i32; 3]) -> i32 { arr[1] }
- Yukarıdaki kodun register üzerinden aktarılması gerekirken pointer ile aktarılıyor
- Rust, C ABI'den bile daha muhafazakâr.
extern "C"olarak belirtildiğinde register ile aktarılıyor.
Yeni Calling Convention önerisi
extern "Rust"fonksiyonları için mevcut çağrı kuralı korunurextern "Rust"fonksiyonlarının çağrı kuralını ayarlamak için-Zcallconvbayrağı eklenir-Zcallconv=legacymevcut yöntemdir-Zcallconv=fastyeni tasarlanacak yöntemdir
- Neden mevcut çağrı kuralı korunmalı?
- Hata ayıklama kolaylığı için C ABI sırasına göre yerleştirme yapılmaz
- WASM gibi bazı hedefler bunu desteklemeyebilir
- Debug derlemelerinde bunun pek anlamı olmayabilir
- Fonksiyon pointer'ları ve
extern "Rust" {}bloklarıyla ilgili dikkat edilmesi gerekenler- Bu, crate düzeyinde bir bayrak olduğu için fonksiyon pointer'larına uygulanamaz
- Fonksiyon pointer çağrıları yavaştır ve nadirdir; bu yüzden
-Zcallconv=legacykullanılır - Gerekirse çağrı kuralı dönüşümü için shim üretilir
extern "Rust" { fn my_func() -> i32; }gibi doğrudan çağrı durumlarında- Yalnızca mangling yapılmamış semboller çağrılabilir
#[no_mangle]fonksiyonlar mevcut çağrı kuralını kullanır
LLVM'i kullanma yaklaşımı
- İdeal olarak LLVM'e çağrı kuralını doğrudan belirtebilmek güzel olurdu, ancak pratikte bu zor
- Bunun yerine şu adımlarla dolaylı bir çözüm mümkün
- Verilen hedef için register ile aktarılabilecek azami değer sayısını belirle
- Dönüş değerinin nasıl aktarılacağına karar ver. Register'a sığıyorsa olduğu gibi, büyükse referansla aktar
- Değer olarak geçirilen argümanlar arasından hangilerinin referansla aktarılması gerektiğini seç
- Register ile aktarılabilir alandan büyük olanlar
- x86'da bu yaklaşık 176 bayt
- Register alanını en iyi şekilde kullanmak için hangi argümanların register ile aktarılacağını belirle
- Bu NP-hard bir problem olduğu için sezgisel bir yöntem gerekir
- Kalanlar stack üzerinden aktarılır
- LLVM IR ile fonksiyon imzasını oluştur
- Register ile aktarılan argümanlar i64, ptr, double, <2 x i64> gibi non-aggregate türlerle ifade edilir
- Stack üzerinden aktarılan argümanlar "register inputs" kuralını izler
- Fonksiyon prologue'unu oluştur
- Rust düzeyindeki argümanları register girdilerinden decode edip
-Zcallconv=legacydurumundakiyle aynı %ssa değerlerini üret - Fonksiyon gövdesi, çağrı kuralından bağımsız olarak aynı kodu üretebilir
- Gereksiz decode kodu DCE ile kaldırılır
- Rust düzeyindeki argümanları register girdilerinden decode edip
- Fonksiyon return bloğunu oluştur
-Zcallconv=legacydurumundakiyle aynı dönüş tipi için phi komutlarını içerir- Gerekli çıktı biçimine encode edilip
retile döndürülür retyerine bu bloğa dallanılmalıdır
- Fonksiyon pointer olarak kullanılabilecek monomorfik ve inline olmayan fonksiyonlar varsa
- Crate dışına açılıyorsa veya fonksiyon pointer olarak geçiriliyorsa
-Zcallconv=legacykullanan bir shim üretilir ve gerçek implementasyona Tail Call yapılır- Fonksiyon pointer eşitliğini korumak için bu gereklidir
LLVM'in register ile aktarma sınırlarını nasıl belirleyebiliriz?
- LLVM'in izin verdiği azami register iletimi sayısını kontrol eden bir LLVM programı mevcut
- x86'da girişte 6 tamsayı, 8 SSE vektörü; çıkışta ise 3 tamsayı, 4 SSE vektörü mümkün
- aarch64'te giriş ve çıkış için aynı şekilde 8 tamsayı ve 8 vektör kullanılabiliyor
- Bu sınırın ötesindeki değerler stack'e aktarılıyor
Rust'ta struct ve enum işleme
- rustc'nin bunları zaten temel aggregate ve union olarak ele aldığı varsayılıyor
- Dönüş değeri işleme
- Önemli olan struct'ın boyutu değil, padding hariç gerçek veri boyutu
- [(u64, u32); 2] 32 bayt olsa da 8 bayt padding çıkarıldığında 24 bayt kalır
- Türün etkin boyutu (Effective Size) tanımlanır
- Padding hariç tanımsız bitlerin sayısı
- [(u64, u32); 2] için 192 bit
- bool için 1 bit
- Etkin boyut, çıkış register alanından küçükse değer olarak döndürülür
- x86'da 3 tamsayı + 4 SSE = 88 bayt = 704 bit
- Argüman register işleme
- Bu bir Knapsack problemi, yani NP-hard
- Basit bir sezgisel yaklaşım
- Etkin boyut toplam giriş register alanından büyükse referansla aktar
- Enum'ları discriminator-union çiftine dönüştür
- Union'lar başlatılmamış bitlere dokunabileceği için u8 dizisi ya da boş olmayan tek bir varyant olarak aktar
- Pointer, tamsayı, kayan nokta, boolean gibi en temel öğelere kadar düzleştir
- Etkin boyuta göre artan sırada sırala
- Mümkün olan en büyük öneki register'a yerleştir, kalanları stack'e koy
- Stack'e gidecek girdilerin bir kısmı pointer boyutunun küçük katlarından büyükse stack üzerindeki pointer ile aktar
- Kalanlar sıralama öncesindeki sıraya göre doğrudan stack'e aktar
- Register ile aktarılacakları boyuta göre azalan sırada yerleştir
- Boolean değerleri 64'erli bit paketleme ile taşı
GN+ görüşü
- Bana göre Rust'ın mevcut çağrı kuralı gerçekten büyük bir eksik. C++'tan çok daha iyi performans verebilecekken bunu hâlâ yapamıyor
- Go dili bunu zaten uzun zaman önce uyguladı
- Rust'ın bunu henüz uygulayamama nedenleri
- ABI kod üretimi karmaşık ve LLVM bu konuda pek yardımcı olmuyor
- Derleyici ekibinde LLVM'i çok iyi bilen kişi sayısı az
- Derleme süresiyle ilgili kaygılar var, ancak bu yalnızca optimize derlemelerde kullanılacağı için büyük bir sorun değil
- Yazarın bunu bizzat düzeltmeye zamanı yok, ancak LLVM uzmanlığıyla Rust derleyici ekibine yardımcı olmaya istekli
- Ya da doğrudan
extern "C"veyaextern "fastcall"kullanımına geçmek de bir alternatif olabilir
1 yorum
Hacker News görüşü
Özet:
Option<u8>alanı 8 adet olan bir struct Rust'ta 16 bayt, C'de ise 9 bayttır.&Option<T>veya&mut Option<T>ile eşlenemez.reprile devre dışı bırakılabilir.