- GPU shader’larında üçlü operatör veya basit
ifile değer seçen kodlar genellikle koşullu dallanma olarak değil, koşullu taşıma (select) olarak işlenir step()ve aritmetik maskeleme ile değiştirmek, ortadan kaldırılacak bir dal olmadığı için, sözde dal kaldırma optimizasyonu varsayımının kendisi doğru değildir- AMD ve Microsoft derleyici çıktılarında karşılaştırma ve koşullu maske/taşıma komutları görünür; jump veya branch komutları görünmez
step()sürümü,0.0/1.0maskesi oluşturduktan sonra çarpma ve toplama ile sonucu birleştirir; bu da doğrudan koşullu taşıma yapan koda göre gereksiz işlemleri artırır- Koşula bağlı olarak büyük hesaplama bloklarını atlayan GPU dallanmaları hâlâ yararlıdır, ancak basit değer seçimi için üretilen makine kodunu kontrol etmek daha güvenlidir
Basit değer seçimi GPU dallanması değildir
- Örnek
snap45()fonksiyonu, giriş vektöründex = abs(v.x)hesapladıktan sonra iki üçlü operatörle üçvec2sonucundan birini döndürür - Aynı mantık normal
ififadeleriyle yazıldığında da korunur - Sorunlu “optimizasyon”, üçlü operatörleri
step()ve ağırlıklı birleştirme ile değiştiren yöntemdirw0,w1,w2değerlerinistep()ile oluştururres0,res1,res2değerlerini ayrı ayrı hesaplar- Nihai sonucu
w0*res0 + w1*res1 + w2*res2ile birleştirir
- Bu dönüşüm, özgün kodun koşullu branch oluşturduğu yanılgısından yola çıkar
- Basit register değeri seçimi komut işaretçisini değiştirmez; tahmin hatası, pipeline flush veya komut önbelleği geçersizleştirmesi de yaratmaz
- GPU’daki gerçek dallanma, koşula göre büyük hesaplama bloklarını atladığında hızlı ve yararlı olabilir
- Ancak örnekteki gibi basit bir değeri veya hesaplama sonucunu seçme durumlarında, üretilen makine kodunda branch oluşmadığı varsayılabilir
Derleyici çıktısının gösterdiği fark
- Özgün GLSL üçlü operatör kodu, AMD derleyicisinde karşılaştırma ve koşullu maske komutlarına dönüştürülür
- Karşılaştırma:
v_cmp_gt_f32,v_cmp_ngt_f32 - Koşullu maske:
v_cndmask_b32
- Karşılaştırma:
- Microsoft derleyici çıktısı da aynı yapıyı gösterir
- Karşılaştırma:
lt - Koşullu taşıma:
movc
- Karşılaştırma:
- Her iki derleyici çıktısında da jump/branch komutu yoktur
step() yöntemi neden daha pahalı hale gelir?
step()tabanlı yöntem önce koşullu taşıma ile0.0veya1.0maskesi oluşturur, ardından birden fazla aday sonucu çarpma ve toplama ile maskeler- Özgün kod gerekli değeri doğrudan koşullu taşır; bu yüzden maske oluşturma ve aritmetik birleştirme ekleyen
step()yöntemine göre daha az israf yapar - Çeşitli donanımlarda
step()tabanlı sürüm, özgün sürümden çok daha yavaş ölçülebilir - Örnek koddaki bazı
abs()GLSL çağrıları ayrı bir GPU komutu değil, komut modifier’ı olarak girer; bu durumlardaabs()çağrısı neredeyse ücretsiz sayılabilir float a = mix(b, c, step(y, x));ifadesinifloat a = x < y ? b : c;için bir optimizasyon olarak önermek yanlış bir yaklaşımdır
1 yorum
Hacker News yorumları
TFA’nın vardığı sonuç doğru görünüyor, ama yalnızca daha iyi sürümün üretilen kodunu göstermek yerine iki sürümün üretilen kodunu da gösterseydi argüman daha güçlü olurdu
Alıntıda “optimize edildiği iddia edilen sürüm, özgün sürümden çok daha yavaş… iki çarpma ve bir-iki toplama boşa harcıyor… üretilen makine koduna bakalım” diyor, ama gerçekte yalnızca çarpma ya da toplama içermeyen iyi sürümü gösteriyor
Bu, iyi sürümün fena olmadığını kanıtlar; kötü sürümün daha kötü olduğunu ise henüz kanıtlamaz
Diğer sürümün üretilen kodu gösterilseydi de büyük olasılıkla sadece daha uzun olduğu görülürdü; onda da dallanma oluşması beklenmediğinden pek değerli olmazdı
if’in ne zaman gerçek dallanmayı zorladığını, ne zaman zorlamadığını anlamanın iyi bir yolu olsa keşkeİnsanların daha pahalı olabilecek
mix/lerpkullanmasının nedeni, küçük bir ek yükü göze alsalar da dallanma oluşmasından korkmalarıv = x > y ? a : b;gibi en açık kodun gerçekte iyi çalışması güzel, ama aynıifsözdiziminin kimi zaman dallanma, kimi zaman da dallanma olmaması insanı tedirgin ediyorGerçekten dallanma olmaması gereken bağlamlarda
branch-ifile dallanmayan if farklı anahtar sözcükler olsa isterdim; dallanmayan anahtar sözcükte derleyici bunu dallanma olmadan üretemiyorsa derleme başarısız olmalı, dallanan anahtar sözcükte de dallanma olmadan üretebiliyorsa uyarı vermeliBaşta programcıları korkutmak istemedikleri için yürütme modelini gizleyip “thread” soyutlamasıyla anlattıklarını, daha sonra GPU tanıtımlarında da “çok fazla CUDA thread’i var” tarzında kullanmayı sürdürdüklerini düşünüyorum
Sonuçta GPU kodlama etrafında tuhaf batıl inançlar oluştu
Aslında kodda dallanma olması çoğu zaman iyidir ve dallanmanın kendisi hızlıdır
Sorun, SIMD lane’lerinin her birinin farklı dallara sapamamasıdır; bu yüzden derleyici dallanma yerine iki tarafın kodunu da üretir ve sonucu koşula göre maskeler
Dolayısıyla shader giriş değerleri, verteksler, compute shader indeksleri vb. temelindeki hesaplamalar gerçek dallanma yapmaz; maskeleme ile sıralı yürütülür
TFA örneğinde de
?operatörünün iki tarafındaki değerlerin ikisi de hesaplanır; SIMD değerleri üzerindeki koşul ifadelerinde de genel olarak durum aynıdırYalnızca tüm lane’ler aynı değere sahip olduğunda hesaplamayı hızlıca atlayan kısa devre bir dallanma ortaya çıkabilir; ama genel olarak doğru/yanlış taraflarının ikisi de hesaplanır
Yalnızca skaler register, yani shader sabitleri veya uniform değerler temelindeki koşul ifadeleri gerçek dallanma oluşturur ve bu dallanmalar çok hızlıdır
Örneğin CMOV komutu 1995’te P6 çekirdeğiyle kullanıma girdi
Dallanma skaler mimarilerde de pahalıdır ve derleyici ne zaman alternatif strateji kullanması gerektiğini elinden geldiğince belirler
Bazen yanılır, ama çok sık yanılmaz
Koşullu taşıma varsayılandır; gerçek dallanma, ancak workgroup’un tamamı aynı yöne giden uniform bir dallanma olduğunda mümkün olan bir performans optimizasyonudur
a = f(z); b = g(z); v = x > y ? a : b;f()veg()çağrılarının maliyeti görece yüksekse, koşullu kod üretmek mi yoksa ikisini de hesaplayıp sonra seçmek mi gerektiği bir ödünleşim meselesidirBasit bir seçim değildir; kararı derleyici verir
Koddaki tüm fonksiyonları dallanabilir/dallanamaz diye renklendirir gibi ayırıp, dallanamaz olarak işaretlenen fonksiyonlarda
if’in koşullu taşımaya derlenmesini ve yalnızca dallanamaz fonksiyonların çağrılabilmesini sağlamak mümkün olabilir“GPU’da dallanma yavaştır” efsanesinin önemli bir kısmı, eski PlayStation 3 döneminde bunun gerçekten epey yavaş olmasından kaynaklanıyor
PS3’te NVIDIA RSX GPU vardı; dokümantasyonda dallanmanın 6 cycle olduğu yazıyordu diye hatırlıyorum, ama gerçek ölçümler her zaman bundan daha yavaş çıkıyordu
Warp’taki tüm thread’lerin aynı yolu izlediği tamamen coherent dallanmalarda bile böyleydi; incoherent dallanma ise
IFEHkomutu 6 cycle olduğu ve GPU iki dalı da yürütmek zorunda kaldığı için daha da yavaştıBugüne kadar gelen “GPU dallanması yavaştır” efsanesinin buradan başladığını düşünüyorum
Günümüz GPU dallanmaları, özellikle coherent dallanmalar, oldukça ucuz
Günümüzde dallanma mekanizmasının ek yükü düşmüş olabilir; ama dallanmanın iki tarafındaki throughput’un etkin thread oranı kadar azalması şeklindeki fiziksel kısıt değişmedi
İki dal da yürütülüyorsa ve komut uzunlukları da aynıysa, iki tarafın ortalama performansı en az yarıya iner
Bu yüzden GPU’da dallanmanın yavaş olduğu inancı uzun ömürlüdür ve aslında doğrudur
Mümkünse problemi dallanma olmadan yeniden kurgulamak için daha fazla çaba harcamaya değer
Dinamik dallanmadan kaçınmanın başlıca nedeni, dallanmanın özünde yavaş olması değil, daha çok budur
Bu tür dallanmayı önleme optimizasyonları bir zamanlar işe yarıyordu
Xbox 360 ve eski Intel tümleşik GPU’larında profilini çıkarmıştım, ama artık bunu yapmamak daha doğru
Bit çıkarma ve diğer tamsayı işlemleri de benzer
Eskiden bunları kayan nokta matematiğiyle emüle etmek daha hızlıydı, ama artık tüm GPU’larda hızlı tamsayı işlemleri var
Örneğin PS5 ve Xbox Series S|X’in mimarisi olan RDNA2 ISA’ya bakınca, tamsayılar için yalnızca 32 bit skaler komutlar görünüyor gibi
[0] https://www.amd.com/content/dam/amd/en/documents/radeon-tech...
Verilen kod zaten dallanmasız kod
Tavsiye verenler, kaynakta koşul ifadesi gibi görünen bir sözdizimi olup olmadığına bakarak dallanma koduna hükmediyor ve bunu optimizasyonla önlediklerini sanıyor gibi
Şu yazı da ilgili: https://medium.com/@jasonbooth_86226/branching-on-a-gpu-18bf...
“İnternete GPU’da dallanma nasıl yazılır diye sorarsanız, bunu cehennemin kapılarını açıp içeri iblisleri almak gibi anlatabilir. Ne pahasına olursa olsun kaçınmanız gerektiğini ve üçlü operatör ya da
step()gibi tuhaf matematik hileleriyle bundan kaçınabileceğinizi söyleyecektir. Bu tavsiyelerin çoğu en iyi ihtimalle eskimiş, çoğu durumda da düpedüz yanlıştır. Hadi bunu düzeltelim.”İşlemciler de değişiyor, derleyiciler de
Bu tür ayrıntılar önemliyse, en iyisi birkaç farklı varyant dağıtıp çalışma zamanında en hızlı sürümü seçmek
Daha önce de birkaç kez söyledim; elle yazılmış assembly’yi kaldırıp sıradan C ya da benzeri kodla değiştirerek çok daha hızlı hale getirdiğim oldu
O assembly 10-20 yıl önce daha hızlı olmuş olabilir, ama bugün durum değişti
Bunu gerçekten yapan oyun ya da motor pek bilmiyorum
İlke olarak mümkün olabilir
D3D, GL, Vulkan gibi çoğu API performans sayaçlarını açığa çıkarıyor; güvenilirlikleri üreticiye göre değişse de, temsili bir test sahnesi hazırlayıp birkaç kez oynatarak optimizasyonu ölçmek mümkün
Ancak birçok oyun dinamik olarak oluşturulan sahneler ve dinamik olarak oluşturulan shader’lar kullandığı için, test edilmesi gereken kombinasyon sayısı engel olabilir
Kullanıcıdan benchmark bitene kadar beklemesini istemek gerekebilir
Donanım varsa, her üreticinin çeşitli GPU nesillerinde önceden ölçüm yapıp yalnızca önemli kararları hardcode etmek mümkün olabilir; ama böyle bir mevcut altyapı pek bilmiyorum
Oyun shader’larını yakalayıp NVIDIA’nın optimize ettiği özel shader’larla değiştiriyor
Bu yüzden NVIDIA sürücü değişiklik günlüklerinde “X oyunu için optimizasyon, %40 daha hızlı çalışıyor” gibi ifadeler görülüyor
Her shader’a sonsuz zaman ayıramazsınız
Önemsediğiniz donanımda profil çıkarır, seçtiğiniz yöntem varsayımsal bir gelecekteki işlemcide daha yavaşsa da yapacak bir şey olmadığını kabul edersiniz
Umarız o işlemci bunun sorun olmayacağı kadar hızlıdır
Bu yazının düzeltmeye çalıştığı hata ve kafa karışıklığı burada da tekrarlanıyor gibi
Yazı koşullu dallanmanın bedava olduğunu iddia etmiyor
Bana göre dallanma kodunun performans maliyeti hakkında konuşan bir yazı da değil
Yazının asıl noktası, sunulan biçimdeki koşul mantığının koşullu dallanma koduna derlenmediği
Ve gözle görülen her koşul ifadesini zorla gizlemeye yönelik zararlı tavsiyeyi yaymaya devam etmemek gerektiği
Gerçek dallanma koduna gelirsek, dallanma kodu çalıştırmanın daha karmaşık olduğu zaten açık
Bedava dallanma yoktur; makul sınırlar içinde dallanmadan kaçınmak herhangi bir kodun daha hızlı olma ihtimalini artırır
Neyse ki özgün kod zaten dallanmasız koddu
Her zaman olduğu gibi, bir optimizasyonun değip değmediğini söyleyen evrensel bir ölçüt yok
[0] Burada “gözle görülen” önemli. Üretilen kodla ilgilenmeyip yalnızca kaynak kodun koşul ifadesi gibi görünmemesine takılan durumları kastediyorum
[1] Elbette bu şans eseri değildi. IQ’ya birinin shader kodu için bariz görünen ama yanlış bir iyileştirme önerisi göndermiş olduğunu tahmin ediyorum
Peki derleyici neden “optimize edilmiş” sürümün aynı kod olduğunu anlayacak kadar akıllı değil?
step()’i anlayıpstep() = 0.0,step() == 1.0durumlarını ayrı optimize edebilmesi gerekmez mi?En azından bir çarpma işlemi kaldırılabilir; bu yüzden genelde koşullu load/store’a ya da başka bir şeye dönüşse bile her zaman kazançlı olacak gibi
Bazı derleyicilerin bazı durumlarda böyle bir optimizasyon yapma ihtimali gayet var, ama derleyicinin anlayamadığı bir sürüm yazmak da kesinlikle mümkün
Optimizasyonların çoğu sürücü tarafında gerçekleşir ve fazla uzun süren işler shader derleme takılması olarak kendini gösterir
Bu optimizasyonun şu anda gerçekten yapılıp yapılmadığını söyleyemem, ama her zaman hesaba katılması gereken bir faktör
Söz konusu “optimize edilmiş” sürümün daha yavaş olmasının nedeni,
step()fonksiyonunun gerçekte şu şekilde uygulanmasıdır:float step( float x, float y ) { return x < y ? 1.0 : 0.0; }Bir OpenGL fonksiyonunun GPU primitifini mi çağırdığını, yoksa emüle mi edildiğini nasıl bilebiliriz?
Bunu HLSL shader’larıyla sık sık yaptım ve sanal komut kümeleri hakkında çok şey öğrendim
Örneğin GPU’da
sincoskomutu varken, ters trigonometrik fonksiyonların derleme sırasında emüle edilmesi ilginçtirPerformans önemliyse bilmeniz gerekebilir
Ancak
step’in özel bir komut değil de koşul ifadesi üzerine kurulmuş bir kütüphane fonksiyonu olarak uygulanmış olması, tek başına özel bir komuta kıyasla performansı söylemez; bu yüzden uygulamanın kendisine fazla takılmaya gerek yokGPU mimarisi merak ediliyorsa disassembly’ye, açık kaynak sürücü kodlarına, LLVM’e ve ISA belgelerine bakılabilir
Decompile edilmiş shader’lara her baktığımda, genel olarak C’de düşündüğümüz şeye benziyordu
OpenGL gibi spesifikasyonlar birçok yerleşik fonksiyonun davranışını tanımlar; uygulama da bu spesifikasyonu standart assembly komutlarıyla karşılayacak şekilde yapılır
Birden fazla mimariye decompile eden çevrimiçi siteler bulunabilir
Genellikle yerleşik fonksiyonların nasıl uygulandığını bilmenize de, bunu umursamanıza da gerek yoktur
Bunu umursuyorsanız muhtemelen optimizasyon düşünüyorsunuzdur; o noktada cevap “ölç ve hangisinin daha iyi olduğunu doğrula”dır
Benim öğrendiğim anlamda koşul ifadesi bir branch’tir
Makine kodu seviyesinde kontrol akışı çalışma zamanında seçildiği için, koşullu atlama tanımı gereği branch’tir
step()kullanmayı mantığı aritmetiğe dönüştürmek olarak değil, mantığı bir kütüphane fonksiyonu çağrısının içine gizlemek olarak gördümstep()in yerleşik fonksiyon olması ya da matematik makalelerinde geçen bir fonksiyon olması bunu değiştirmezMatematikte de
step()in tanımı kelimenin tam anlamıyla bir koşul ifadesidirKoşul ifadesi olmadan gerçekten optimize etmek istiyorsanız, istenen sonuca benzeyen sürekli bir fonksiyon seçip parametreleri hedefe mümkün olduğunca yaklaştıracak şekilde ayarlamak gerekir
Genellikle bir polinom seçer, standart yinelemeli yaklaşım yöntemini çalıştırır ve sonunda branch olmadan yalnızca toplama, çarpma ve “garip derecede spesifik” sabitler içeren bir
f(x)elde edersinizYazarın koşullu taşımanın “branch” olmadığını güçlü biçimde söylediği kısmı pek anlayamıyorum
abs()in GPU komutu değil de komut değiştiricisine dönüşüp bedavaya gelmesi, tamsayıların ikinin tümleyeni gösterimi ve IEEE-754 kayan nokta gösterimi sayesinde işaret bitinin en yüksek bit olarak ele alınabilmesindendirBu yüzden
abs()en yüksek biti koşulsuz 0 yapmakla ya da okuyan komutta maskelemekle sınırlı kalırAma
step()veya rastgele bir üçlü operatör ve bildiğim kadarıyla koşullu taşıma komutları böyle özel durumlar değildirTemel
abs(),sqrt(), trigonometrik fonksiyonlar gibi şeyler neredeyse standart bilgi sayılır; geri kalanların zaten ne kadar önemli olduğu tartışılırstep()bir yerde koşul içermek zorundadır; bunu doğrudan yapmanız, kütüphaneye bırakmanız ya da donanıma bırakmanız temel niteliğini değiştirmezBu tuzağa ben de düşmüştüm
Claude veya ChatGPT de bunu optimizasyon olarak önerebiliyor
Ama her ölçtüğümde performans düştü; bazen de oldukça belirgin şekilde düştü
LLM’ler yalnızca eğitim derlemindeki içerikleri tekrar eder
İnternetin büyük bölümü bu koşullu taşıma “optimizasyonu” gibi yanlış şeyler önerirse, LLM de aynı şeyi önerir