Tree Borrows: Rust unsafe kodu için takma ad kuralları modeli
(plf.inf.ethz.ch)- Rust derleyicisinin işaretçi takma ad garantilerini optimizasyonlarda kullanabilmesi için, unsafe kodun kuralları nerede ihlal ettiğinin açıkça tanımlanması gerekir
- Mevcut Stacked Borrows bu ölçütü sundu, ancak gerçek dünyadaki unsafe Rust kodunda yaygın olan kalıpları ve borrow checker'ın güncel özelliklerini yeterince kapsayamıyor
- Tree Borrows, Stacked Borrows'un temel yapısını yığından ağaca çevirerek daha fazla geçerli kalıbı ifade etmeyi mümkün kılıyor
- En yaygın kullanılan 30.000 Rust crate'i üzerinde yapılan değerlendirmede, Stacked Borrows'a kıyasla reddedilen test vakaları %54 daha az
- Rocq ispatıyla, mevcut optimizasyonların çoğu korunurken read-read yeniden sıralama gibi yeni optimizasyonların da mümkün olduğu doğrulandı
unsafe Rust'ta gerekli takma ad kuralları
- Rust, sahiplik temelli tür sistemi ile bellek güvenliği ve veri yarışı önleme gibi güçlü garantiler sunar
- Ancak unsafe kod alanında güvenlik otomatik olarak garanti edilmez; programcının uyması gereken ayrı kurallar gerekir
- Derleyici, tür sisteminin garantilerini, özellikle de işaretçi takma ad (aliasing) ile ilgili bilgileri kullanarak fonksiyon içi optimizasyonları güçlendirmeye çalışır
- Hatalı yazılmış unsafe kod bu optimizasyonları bozabileceği için, hangi kodun “badly behaved” sayılacağını açıkça belirleyen bir ölçüt önemlidir
- Mevcut çalışma olan Stacked Borrows bu ölçütü tanımladı, ancak bazı sınırlamaları var
- Gerçek dünyadaki unsafe Rust kodunda yaygın olan çeşitli kalıpları reddeder
- Rust borrow checker'a yakın dönemde eklenen gelişmiş özellikleri yansıtamaz
Tree Borrows'un yaklaşımı ve değerlendirme sonuçları
- Tree Borrows, Stacked Borrows'un merkezî yapısı olan yığını ağaçla değiştirerek tanımlanır
- Bu yapısal değişiklik, mevcut modelin sınırlamalarını gevşetir
- En yaygın kullanılan 30.000 Rust crate'i üzerinde yapılan değerlendirmede, Stacked Borrows'a göre reddedilen test vakaları %54 azaldı
- Rocq ispatıyla optimizasyonla ilgili özellikler de doğrulandı
- Stacked Borrows'un izin verdiği optimizasyonların çoğu korunuyor
- Önemli yeni bir optimizasyon olan read-read reorderings de mümkün oluyor
- Tree Borrows, PLDI'25 Distinguished Paper Award ödülünü aldı
- İlgili kaynaklar
1 yorum
Hacker News yorumları
Bonus olarak, Ralf Jung grubunun Rust lehçesiyle Rust’ın yürütme semantiğini çalıştırılabilir biçimde hassas şekilde belirtmeyi amaçlayan yakın tarihli bir sunumu da var: https://youtube.com/watch?v=yoeuW_dSe0o
Torvalds uzun zamandır C’nin katı aliasing kurallarının faydadan çok zarar getirdiğini savunuyor ve kulağa ikna edici geliyor. Örnek burada: https://lore.kernel.org/all/CAHk-=wgq1DvgNVoodk7JKc6BuU1m9Un... Bu konuyla ilgileniyorsanız tüm thread de okunmaya değer
Rust’ın temelde farklı olup olmadığı, sınırlı deneyimime göre öyle görünmüyor. En azından işin içine unsafe girince durum daha da böyle
Derleyici için daha yararlı, programcı içinse daha az külfetli olduklarını düşünüyorum. Ayrıca dilin içinde gerçekten bir kaçış yolu da var: raw pointer kullanmak. Kodu denetleyecek araçlar da var
Sonuçta dil tasarımındaki her şey gibi bu da bir ödünleşim ve Rust’ta bu tür optimizasyonlar için yeni bir sweet spot bulunmuş olabilir. Doğru olup olmadığını zaman gösterecek
C’de
restrictdiye nükleer bomba gibi bir araç var; deneyimime göre clang ve gcc’de yalnızca fonksiyon argümanlarına eklendiğinde bir etkisi oluyordu. Tip tabanlı alias analizi genel olarak kullanması zor;int64_ttipinin sonsuz kopyasını da yapamazsınız, muhtemelen yapmak da istemezsiniz. Başka bir tipe yeniden yorumlamak içinmemcpyyi zorunlu kılması da can sıkıcıBuna karşılık Rust referansları ömür, kapsam ve değiştirilebilirlik açısından ince ayrıntılı sınırlarla çevrili ve “fiziksel” tipin kendisini pek umursamıyor. Bu yüzden aynı belleği
&mut i32/&i32ile&mut i64/&i64olarak yeniden yorumlayıp aralarında geçiş yapmak da mümkün. Unsafe soyutlama aynı anda çakışan&mutreferansları vermediği ya da tek bir&mutı çakışmayan birden fazla&muta böldüğü sürece, sıradan güvenli Rust okuma-yazmalarıyla yarım değerleri veya birden çok değeri okuyup yazabilirsinizAlias analizi bugünlerde iyi performans elde etmek için çok önemli. Ancak en büyük kazanımların en basit sezgisel kurallardan geldiğini de unutmamak gerekir. Örneğin aynı SSA değerini pointer olarak kullanan iki load’ın mutlaka birbirine alias olması gibi
LLVM açısından BasicAA bu rolü üstlenir. “Bir nesnenin allocation noktasını izleyebiliyorsan alias sorgusunu kesin olarak çöz, yoksa bilmiyorum de” yaklaşımına yakın basit sezgisel kurallar kümesidir
Asıl soru, temel ve bariz kontrollerin ötesine geçen alias analizinin değeridir. Alias sorgularının artık önemsiz biçimde çözülemediği aşamaya gelindiğinde, sonuçla yapılabilecek şeyler de genelde büyük ölçüde azalır ve çoğunlukla kod taşıma risklerini bulmak düzeyine iner. Kazanç çok daha küçüktür
Yapmak istediğim deneylerden biri, teorik olarak kusursuz bir alias analizinin sağlayacağı toplam hız artışını ölçmek. Tahminim, Linux çekirdeği gibi HPC dışı kodlarda bile bunun yaklaşık %20 olacağı yönünde
[1] Buna, yüksek kaliteli alias analizi olmadan denenmeyecek veri yerleşimi dönüşümleri gibi kahramanca optimizasyonlar dahil değil. Gerçekte böyle bir alias analizinin olmadığını zaten bildiğimiz için o tür optimizasyonlar da denenmeyecek; beklenen hız artışına katmaya değmez diye düşünüyorum
C’de aliasing yalnızca tipe dayanır; bu yüzden diğer adı da tip tabanlı alias analizi ya da TBAA’dır
noaliasın çalışma süresi açısından yaklaşık %5 performans artışına katkı sağladığını iddia eden bir şey buldum, ama kaynağın çok eski olduğu kesinhttps://github.com/rust-lang/rust/issues/54878#issuecomment-...
https://news.ycombinator.com/item?id=22281205
https://news.ycombinator.com/item?id=17715399
&muttan*mut i32oluşturupwrite(x)yerine*x = 10yaparsanız örtük iki aşamalı borrow kullanmadığınız için derleyicinin bunu reddetmesi gerekir gibi açıklanmıştı, ama gerçekte kabul ediyor*x = 10;sürümünde hata raporlanır,write(x);sürümünde ise raporlanmazHata “Undefined Behavior: attempting a write access using [...] but that tag does not exist in the borrow stack for this location” biçimindedir
rustc’nin kendisinin ikisini de reddetmesi için bir neden yok. Çünkü
ybir*mut; derleme zamanı tip sistemi açısındanxolan&mutile arasında bir ödünç alma/ömür ilişkisi yok.Mevcut ödünç alma denetçisi daha kısıtlayıcı bir analiz kullanıyor ve ham işaretçiler ile değiştirilebilir referanslar arasındaki bu belirli çatışmayı tespit edemiyor.
Kendi deneyimimde [1] [2] örneklerinde de Stacked Borrows’a göre yasa dışı olan ama makul kodlara izin veriyordu.
[1] https://github.com/Voultapher/sort-research-rs/blob/main/wri... Miri sütunu
[2] https://github.com/rust-lang/rust/blob/6b3ae3f6e45a33c2d95fa...
Kapsam tabanlı ödünç alma denetçisinden sözcüksel olmayan ömür ödünç alma denetçisine geçildi; bir sonraki deneysel Polonius uygulaması da seçenek olarak var. Ancak yeni uygulama üretime hazır olduğunda eski uygulama bırakılıyor. Çünkü seçmek için bir neden yok.
Ödünç alma denetimi hızlıdır ve yeni uygulama daha fazla doğru programı katı biçimde kabul eder.
Ayrıca çalışma zamanı denetimi maliyetini ödeyerek daha fazla esneklik sağlayan
RcveRefCelltipleri de var.Hepsinin uygulama, performans ve geliştirici deneyimi açısından farklı maliyetleri ve yetenekleri var.
Ayrıca Rust dışındaki çoğu şeyin gerçekte hedeflediği, otomatik kaynak yönetiminin üretkenliği. Yöntem ne olursa olsun otomatik kaynak yönetimi kullanıp, yalnızca performans açısından kritik yollarda yukarıdaki tip sistemlerinden birini birleştirmek şeklinde.
Bu bağlamda “Rust”, “insanların genelde istediği değişmezler” ve “bu olağan değişmezleri varsayan ama ne daha fazlasını ne de daha azını yapan optimizasyonlar kümesi”nden ibaret görülebilir.
Derleme zamanının çoğu trait çözümleme, monomorfizasyon, LLVM optimizasyon geçişleri ve bağlamaya gider.
Belki aptalca bir soru olabilir ama birden fazla uygulamayı paralel iş parçacıklarında çalıştırıp ilk olumlu sonuç verenin kazanmasını sağlayamaz mıyız diye merak ediyorum.
İşaretçi kullanarak aynı değişken için birden çok değiştirilebilir referansın aynı anda var olmasını sağlamak tanımsız davranıştır. Makalenin niyetini yanlış anlamadıysam böyle görünüyor.
Yukarıdaki kod Rust derleyicisi tarafından kabul ediliyor ama kuralları çiğniyor. Sorun hangi kuralları çiğnediği.
Özünde ödünç alma denetçisinin kabul ettiği şey yasaldır; unsafe ise yasa dışı veya tanımsız davranış olan şeyleri de ifade edebilir. Ayrıca ödünç alma denetçisinin doğrulayabildiğinden daha geniş, ama yine de yasal ve tanımlı davranış olan bir kurallar kümesi vardır.
Bu araştırmanın amacı o kurallar kümesini hassas biçimde belirtmek. Büyük çerçeve “yazılabilir işaretçiler alias olmamalı” fikrine yakın; ama iç işaretçiler, yineleyici geçersizleştirme, kötü işaretçi üretmenin mi yoksa onu kullanmanın mı sorun olduğu gibi ayrıntılar çok zor.
Önceki Stacked Borrows makalesi daha basitti ama daha kısıtlayıcıydı; bu yüzden gerçek dünyadaki unsafe kodlar kurallardan sık sık geçemiyordu. Tree Borrows daha geniştir, daha çok koda izin verir ve yine de kanıtlanabilir biçimde güvenlidir.
Tree Borrows tam da böyle bir tanım öneriyor.
Burada “kod bunu yapabilir” demek, “bu kodu yazabilir, derleyebilir ve çalıştırabilirsiniz; Tree Borrows gibi bir şey yoksa bu kodda sorun olduğunu iddia etmek için dayanağınız yoktur” anlamına geliyor.
Böyle bir kodun tanımsız davranış olduğunu söylememiz gerektiğini, yani Tree Borrows gibi bir şeye ihtiyaç olduğunu zaten kabul etmiş oluyorsunuz. Makalenin bu bölümü, neden böyle bir şeye ihtiyaç duyulduğunu savunan kısım.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
Rust derleyicisi geliştiricilerinin alias optimizasyonlarını desteklemek istediği açık olduğundan, yukarıdaki gibi karşı örnekleri değerlendirme dışı “bırakmanın” bir yoluna ihtiyaç olduğundan bahsediyor.
unsafe, Rust’ın ömür analiziyle kodun geçerliliğini kanıtlamanın zor olduğu durumlar içindir; ancak bundan çok daha fazlasını yapmak için kötüye kullanılabilir