1 puan yazan GN⁺ 2025-02-10 | 1 yorum | WhatsApp'ta paylaş
  • GPU shader’larında üçlü operatör veya basit if ile 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.0 maskesi 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ünde x = abs(v.x) hesapladıktan sonra iki üçlü operatörle üç vec2 sonucundan birini döndürür
  • Aynı mantık normal if ifadeleriyle yazıldığında da korunur
  • Sorunlu “optimizasyon”, üçlü operatörleri step() ve ağırlıklı birleştirme ile değiştiren yöntemdir
    • w0, w1, w2 değerlerini step() ile oluşturur
    • res0, res1, res2 değerlerini ayrı ayrı hesaplar
    • Nihai sonucu w0*res0 + w1*res1 + w2*res2 ile 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
  • Microsoft derleyici çıktısı da aynı yapıyı gösterir
    • Karşılaştırma: lt
    • Koşullu taşıma: movc
  • 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 ile 0.0 veya 1.0 maskesi 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 durumlarda abs() çağrısı neredeyse ücretsiz sayılabilir
  • float a = mix(b, c, step(y, x)); ifadesini float a = x < y ? b : c; için bir optimizasyon olarak önermek yanlış bir yaklaşımdır

1 yorum

 
GN⁺ 2025-02-10
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

    • Esas nokta, koşul ifadesinin gerçek bir dallanma oluşturmamış olması
      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ı
    • RDNA 1 için üretilen kod burada: https://shader-playground.timjones.io/5d3ece620f45091678dcee...
  • 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/lerp kullanması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ı if sözdiziminin kimi zaman dallanma, kimi zaman da dallanma olmaması insanı tedirgin ediyor
    Gerçekten dallanma olmaması gereken bağlamlarda branch-if ile 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ı vermeli

    • Bunun arka planında NVIDIA ile cg/CUDA derleyicisinin kafa karıştırıcı dokümantasyonu var
      Baş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ır
      Yalnı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
    • Bu, skaler CPU’larda da aynı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
    • GPU’larda ise duruma tersinden bakmak gerekir
      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
    • Şöyle bir örnek düşünülebilir: a = f(z); b = g(z); v = x > y ? a : b;
      f() ve g() ç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 meselesidir
      Basit bir seçim değildir; kararı derleyici verir
    • Shader dillerinde böyle bir özellik olsa ilginç olurdu
      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 IFEH komutu 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

    • Biri sadece “dallanma” diyorsa bunun incoherent dallanma anlamına geldiğini varsaymak gerekir
      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
    • Coherent dallanma “bedava”ya yakındır, ancak ek komutlar register baskısını artırır
      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

    • “Artık tüm GPU’larda hızlı tamsayı işlemleri var” ifadesinin ne kadar doğru olduğunu merak ediyorum
      Ö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...
    • En azından “büyük” GPU’larda bu eskisi kadar büyük bir sorun değildi, ama bu yazı aslında dallanmadan kaçınmanın kendisiyle ilgili değil
      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

    • Bir shader’ın en hızlı sürümünü çalışma zamanında bulmanın çok zor olduğunu düşünüyorum
      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
    • İlginç şekilde NVIDIA sürücüsü bunu bir ölçüde yapıyor
      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
    • Bir shader daha eklemek gibi bir şeyse sorun olmayabilir, ama “modern” grafik API’lerinde aynı shader için binlerce permütasyon gerekebiliyor ve her varyant eklediğinizde bu sayı 2 katına çıkı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ıp step() = 0.0, step() == 1.0 durumları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

    • Gerçekten de öyle olabilir
      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
    • Optimizasyonun bir başka sorunu, tüm olasılıkları denemenin çok uzun sürmemesi gerektiği
      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?

    • Tek yol, asıl yazıdaki gibi shader’ı derlemek, disassemble etmek ve ardından assembly’yi okumaktır
      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 sincos komutu varken, ters trigonometrik fonksiyonların derleme sırasında emüle edilmesi ilginçtir
    • Bunu neden bilmeniz gerektiği amaca bağlıdır
      Performans ö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 yok
      GPU mimarisi merak ediliyorsa disassembly’ye, açık kaynak sürücü kodlarına, LLVM’e ve ISA belgelerine bakılabilir
    • PC tarzı assembly’de görebileceğiniz fonksiyonlar dışında GPU’nun özel primitiflere sahip olduğu bir duruma rastlamadım
      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
    • Bu, programlamanın genelinde sık görülen iyi bir soru ve optimizasyon yaparken önce ölçmek gerektiğinin de temel nedenlerinden biridir
      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 kafamı karıştıran nokta, “branch”in benim büyürken öğrendiğim anlamdan daha donanıma özgü, iyi tanımlanmış bir anlamı var gibi görünmesi
      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üm
      step()in yerleşik fonksiyon olması ya da matematik makalelerinde geçen bir fonksiyon olması bunu değiştirmez
      Matematikte de step()in tanımı kelimenin tam anlamıyla bir koşul ifadesidir
      Koş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 edersiniz
      Yazarı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ınabilmesindendir
      Bu yüzden abs() en yüksek biti koşulsuz 0 yapmakla ya da okuyan komutta maskelemekle sınırlı kalır
      Ama step() veya rastgele bir üçlü operatör ve bildiğim kadarıyla koşullu taşıma komutları böyle özel durumlar değildir
      Temel abs(), sqrt(), trigonometrik fonksiyonlar gibi şeyler neredeyse standart bilgi sayılır; geri kalanların zaten ne kadar önemli olduğu tartışılır
      step() 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ştirmez
  • Bu 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ü

    • Bu garip değil
      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
    • LLM internetteki insanların söylediklerini tekrarlar; insanlar da sık sık yanılır