3 puan yazan GN⁺ 2025-05-18 | 1 yorum | WhatsApp'ta paylaş
  • Bir fonksiyon içindeki if ifadelerini çağrı noktasına taşımak, kodun karmaşıklığını azaltmaya yardımcı olur
  • Koşul kontrolleri ve dallanma işlemleri tek bir yerde toplandığında, tekrarları ve gereksiz dal kontrollerini fark etmek kolaylaşır
  • enum çözme refaktörü kullanılarak aynı koşulun kodun farklı yerlerine yayılması önlenebilir
  • Toplu işlem temelli for döngüleri, performans artışı ve yinelemeli işlerin optimize edilmesinde etkilidir
  • if yukarı, for aşağı deseni birlikte kullanıldığında, kodun okunabilirliği ve verimliliği aynı anda artırılabilir

Birbiriyle ilişkili iki kural hakkında kısa not

  • Bir fonksiyon içinde if koşul ifadesi varsa, bunun fonksiyon çağrı noktasına taşınıp taşınamayacağını düşünmek önerilir
  • Örnekte olduğu gibi, önkoşulu fonksiyon içinde denetlemektense, bu denetimi çağrı noktasına bırakmak ya da önkoşulu türler (veya assert) ile garanti altına almak daha uygundur
  • Önkoşul denetimlerini yukarı taşıma (Push up) yaklaşımı, kodun genelini etkileyerek toplamda gereksiz koşul kontrolü sayısını azaltma etkisi yaratır

Denetim akışı ve koşul ifadelerinin merkezileştirilmesi

  • Denetim akışı ve if ifadeleri, kod karmaşıklığı ve hataların başlıca nedenlerindendir
  • Koşul ifadelerini çağrı noktası gibi üst seviyelerde toplayıp dallanma işlemlerini tek bir fonksiyonda yoğunlaştırmak, asıl işi ise doğrusal (straight-line) alt yordamların yapmasına bırakmak yararlı bir desendir
  • Dallanma ve denetim akışı tek bir yerde toplandığında, tekrarlanan dallar ve gereksiz koşullar daha kolay fark edilir

Örnek:

  • f fonksiyonu içinde iç içe geçmiş if ifadeleri olduğunda, ölü kodu (Dead Branch) fark etmek daha kolaydır
  • Dallanma birden fazla fonksiyona (g, h) dağıtıldığında bunu görmek zorlaşır

enum çözme refaktörü (Dissolving enum Refactor)

  • Kod aynı koşullu dallanmayı enum gibi yapılar içinde barındırıyorsa, koşulu üst seviyeye çekerek dallanma ile işi daha net ayırmak mümkündür
  • Bu yaklaşım uygulandığında, aynı koşulun kod içinde birden çok kez tekrar etmesi engellenebilir

Örnek:

  • Aynı dallanma koşulunun f, g fonksiyonlarında ve enum E içinde ayrı ayrı ifade edildiği bir durum,
  • tek bir üst seviye koşul dallanmasıyla kodun genelinde sadeleştirilebilir

Veri odaklı düşünme (Data Oriented Thinking) ve toplu işlemler

  • Programların çoğu birden çok nesneyle (varlıkla) çalışır. Kritik yolun (Hot Path) performansı genellikle çok sayıdaki nesnenin işlenmesiyle belirlenir
  • Toplu işleme (batch) kavramını benimseyerek, nesne kümeleri üzerindeki işlemleri temel yaklaşım haline getirmek ve tekil nesne işlemlerini özel durum olarak ele almak daha uygundur

Örnek:

  • frobnicate_batch(walruses) gibi bir toplu işleme fonksiyonunu temel almak,

  • tek tek nesneleri ise bir for döngüsüyle işlenen özel duruma dönüştürmek mümkündür

  • Bu yaklaşım, performans optimizasyonunda önemli rol oynar; büyük hacimli işlerde başlangıç maliyetini düşürür ve sıra esnekliğini artırır

  • SIMD işlemlerinden (struct-of-array vb.) da yararlanılabilir; belirli alanlar topluca işlendiikten sonra tüm işlem sürdürülebilir

Pratik örnekler ve önerilen desen

  • FFT tabanlı polinom çarpımı gibi durumlarda, birden çok noktada eşzamanlı hesaplamayı mümkün kılarak performans en üst düzeye çıkarılabilir
  • Koşul ifadelerini yukarı, döngüleri aşağı taşıma kuralı birlikte uygulanabilir

Örnek:

  • Aynı koşul ifadesini döngü içinde sürekli denetlemek yerine, koşulu döngünün dışına çıkarmak döngü içindeki dallanmayı azaltır ve optimizasyon ile vektörleştirmeyi kolaylaştırır
  • Bu yaklaşım, TigerBeetle tasarımı gibi büyük ölçekli sistemlerin veri düzleminde de yüksek verimlilik sağlar

Sonuç

  • if ifadelerini (üst seviye: çağrı noktası, denetim katmanı) yukarıya, for döngülerini (alt seviye: işlem katmanı, veri işleme katmanı) aşağıya taşıma desenini birleştirmek; kodun okunabilirliğini, verimliliğini ve performansını birlikte iyileştirebilir
  • Soyut vektör uzayı bakış açısından düşünmek (küme düzeyinde işlemler), tekrarlayan dallanma işleminden daha iyi bir problem çözme aracıdır
  • Kısacası: if yukarı, for aşağı!

1 yorum

 
GN⁺ 2025-05-18
Hacker News görüşü
  • Benim kendime özgü zihinsel modelim, çeşitli durumların ya da program akışlarının ağaç yapısı oluşturduğu yönünde. Koşul ifadeleri bu ağacın dallarını buduyor. Mümkün olduğunca erken budama yapıp sonrasında işlenecek dal sayısını azaltmak istiyorum. Tüm dalları tek tek değerlendirip temizledikten sonra en sonunda bütün ağacı bir seferde kesmek zorunda kalacağım bir durumdan kaçınmak istiyorum. Biraz farklı bir bakışla, koşul ifadeleri "gereksiz işi tespit etme süreci", döngüler ise "asıl iş"tir. Nihayetinde istediğim, bir fonksiyonun ya program ağacını dolaşmaya ya da gerçek işi yapmaya odaklanmasıdır
    • Kendi yan modelimi önermek istiyorum. Sınıflar isimdir, fonksiyonlar fiildir diye düşünüyorum
    • Benim zihinsel modelim, yazdığım somut kodun var olduğu dünyaya uyum sağlıyor. Alanın özelliklerine, mevcut kod kalıplarına, veri hattının aşamalarına, performans profiline vb. göre değişiyor. Başta böyle kurallar ya da sezgisel yaklaşımlar üretmeye çalışıyordum ama çok kod yazınca bu tür soyut kuralların pratikte pek anlamlı olmadığını fark ettim. Çoğu zaman rastgele bir fonksiyon adı ya da tek bir harf belirleniyor ve kurallar yalnızca o “ada gibi kodun” içinde geçerli oluyor; oysa gerçek kod tabanında bu fonksiyonların birleştirilmemesinin genellikle bir nedeni var. Örnek olarak “tekrar ve ölü koşullar” anlatılıyor ama bu, ilgili fonksiyonun yalnızca tek bir yerden çağrıldığı gibi rahat bir varsayıma dayanıyor. Gerçekteyse başka nedenlerle ayrı tutuldukları çok oluyor
    • Bence oldukça iyi bir model
  • Daha genel bir kural, koşul ifadelerini giriş kaynağına mümkün olduğunca yakın tutmaktır. Dışarıdan programa giren giriş noktalarını (başka servislerden gelen veriler dahil) olabildiğince erken belirlemek ve çekirdek mantığa ulaşmadan önce, özellikle de kaynak tüketimi yüksek bölümlere gelmeden önce, mümkün olduğunca çok garanti üretmek kritik. Bunu tiplerde açıkça ifade etmek de çok iyi olur
    • Bu durumda çekirdek mantığı anlamaya çalışırken hangi varsayımların geçerli olduğunu görmek zorlaşmaz mı? Tüm çağrı zincirini incelemek gerekmez mi?
  • “Bir if koşulu fonksiyonun içindeyse, bunu çağırana taşıyıp taşıyamayacağını düşün” tavsiyesine çok fazla karşı örnek var. Fonksiyon 37 yerde çağrılıyorsa ne olacak, her çağrı noktasında aynı if ifadesini mi tekrar edeceğiz? Mesela getaddrinfo ya da EnterCriticalSection gibi fonksiyonlara da böyle if’i dışarı taşı demek mümkün mü? Bence bu dönüşüm ancak en fazla birkaç yerden çağrılan ve bu kararın fonksiyonun ilgi alanı dışında olduğu durumlarda düşünülebilir. Bir yöntem, yalnızca koşulu değerlendiren bir fonksiyonun yardımcı bir fonksiyona işi devretmesi olabilir. Ve koşulu döngünün dışına taşımak gerektiğinde, çağıran taraf daha alt seviyedeki koşul yardımcısını doğrudan kullanabilir. Ama bu tartışmanın özü “optimizasyon”. Optimizasyon çoğu zaman daha iyi program tasarımıyla çatışıyor. Çağıranın koşulu bilmesine gerek olmaması daha iyi tasarım olabilir. Bu ikilem OOP’de de sık görülür. “if” ile temsil edilen kararın gerçekte metot dispatch’i ile yapıldığı durumlar vardır. Bu dispatch’i döngünün dışına çıkarmak da tasarım ilkeleriyle sürtüşebilir. Örneğin bir canvas’a görüntü çizerken her seferinde putpixel çağırmak yerine blit gibi bir yöntem kullanmak buna örnektir
    • Eğer bir fonksiyon 37 yerde çağrılıyorsa kodu refactor etmek gerekebilir. Soruna cevap olarak: duruma bağlı. DRY doğru cevap gibi hissettirse de karar vermek için gerçek örnek kodu görmek gerekir. Kütüphane ise sahiplik sınırında olduğu için herkes kendi verisini ve sorumluluğunu yönetmelidir. EnterCriticalSection gibi bir fonksiyon için giriş noktasında güçlü doğrulama yapmak, buna koşullar da dahil, doğrudur. Ama uygulama kodunda if’i çağıran tarafa taşımak sorun olmayabilir. Kütüphane ya da çekirdek kodda denetim akışını kenarlara taşımak uygundur. Kendi çalıştığınız alan içinde denetim akışını kenarlarda tutmak iyidir. Ama bu tür kurallar her zaman yalnızca deyimsel yaklaşımlardır; bağlama uygun biçimde mantıklı karar verebilen biri tarafından duruma göre uygulanmalıdır
  • “dissolving enum refactor” örneği aslında bir polymorphism kalıbı. match ifadesi, çok biçimli bir metot çağrısıyla değiştirilebilir. Bu yaklaşımın amacı, ilk koşul ayrımının belirlendiği an ile gerçek davranışın çalıştırıldığı anı ayırmaktır. Durum ayrımı nesnenin içinde (burada enum değeri) ya da closure’da taşındığı için, bunu her çağrıda tekrar etmeye gerek kalmaz. Durum ayrımı değişirse yalnızca ayrım noktasını değiştirirsiniz; gerçek davranışın gerçekleştiği yerleri düzenlemek gerekmez. Bunun bedeli ise her duruma ait davranış dallarını doğrudan görebilmenin rahatlığı ile kod düzeyinde durum listesine bağımlılık oluşması arasındaki ödünleşimdir
  • Bazen koşul ifadelerini fonksiyonun içinde tutmayı seviyorum. Çünkü çağıranın fonksiyon çağrı sırasını yanlış yapmasını bilinçli olarak engelleyebiliyorsunuz. Örneğin idempotency garantisi gerekiyorsa, önce zaten işlenmiş olup olmadığını kontrol edip değilse işlemi yapmak gibi. Bu koşulu çağrı tarafına çıkarırsanız, bütün çağıranların bu prosedürü doğru izlemesi gerekir ki idempotency korunsun; dolayısıyla soyutlama bu garantiyi sunamaz. Böyle bir durumda bu felsefenin nasıl uygulanması gerektiğini merak ediyorum. Bir başka örnek de, veritabanı transaction’ı içinde bir dizi kontrolü tamamladıktan sonra işi yapmak istediğiniz durumlar; o kontrolleri nereye koymak gerekir?
    • Aslında sorunu kendin cevaplamış gibisin. Koşulu çağıran tarafa taşırsan fonksiyon artık idempotent olmaz ve doğal olarak bu garantiyi veremez. Eğer idempotency garantisi için her fonksiyona durum yönetimi mantığı koyuyorsan, muhtemelen epey tuhaf bir kod yazıyorsundur ve çok fazla iş mantığını tek bir fonksiyona yüklüyorsundur. İdempotent kod kabaca ikiye ayrılır. Birincisi, veri modeli ya da işlemin kendisi idempotenttir; bu durumda işlem sırasına özel önem vermek gerekmez. İkincisi ise daha karmaşık iş operasyonlarında idempotent bir soyutlama kurmaktır. Bunun için rollback ya da atomic apply üzerine soyutlama gibi daha karmaşık mantık gerekir ve bu tür şeyler tek bir fonksiyona kolayca sığacak konular değildir
    • Kontrolsüz bir iç fonksiyon oluşturup, dışta bir wrapper fonksiyonun kontrolleri yaptıktan sonra iç fonksiyonu çağırmasını sağlamak da bir yöntemdir
  • Kod karmaşıklığı tarayıcıları sonuçta if ifadelerini aşağı itmeyi seven araçlar. Ama bu yazı tam tersine, if ifadelerini yukarıya, yani daha üst seviye fonksiyonlara taşımayı öneriyor. Böylece karmaşık dallanma mantığını tek bir fonksiyonda merkezileştirebilir, gerçek somut işi ise alt yordamlarına devredebilirsiniz
    • Çözüm, “karar” ile “yürütme”yi ayırmaktır. Bu fikri Bertrand Meyer’den öğrenmiştim. Mesela if (weShouldDoThis()) { doThis(); } gibi; her kontrolü ayrı bir fonksiyona çıkarırsanız test etmek ve karmaşıklığı yönetmek kolaylaşır
    • Kod tarayıcılarının raporlarına ciddi şüpheyle yaklaşmak gerekir. SonarQube vb. araçlar gerçek hatalar olmayan “code smell”leri de rastgele raporluyor. Bu şekilde “aslında sorun olmayan kodu” düzeltmeye çalışmak yeni hatalar üretme riskini artırıyor ve gerçekten önemli sorunlarla ilgilenme süresini boşa harcıyor
    • Bu tür optimizasyonlar genellikle “yerel optimum” oluyor. Yani yeni bir gereksinim ya da istisna durum ortaya çıktığında, dallanma mantığının döngünün dışında da gerekmesi başlıyor. O noktada dallanmalar hem döngü içinde hem dışında karışınca anlamak zorlaşıyor. Koşulun yalnızca döngü içinde gerekeceğinden eminseniz öyle bırakın; değilseniz bence biraz daha uzun bir tasarımı ve daha ayrıntılı kodu tercih etmek, anlaşılması daha kolay olduğu sürece, daha iyidir. Haskell kullanırken bunu yaşadım. Mantığı en kısa ve en optimize yerel optimum biçime zorladığınızda, gereksinimler çok az değişse bile tasarımın niyetini ifade etmek yerine ortada sadece ham mantık kalıyor ve küçük değişikliklerde bile kod ciddi biçimde açılıp saçılıyor
    • Kod karmaşıklığı tarayıcıları hep rahatsız edici gelmiştir. Okuması kolay büyük fonksiyonlardan bile şikâyet ederler. Mantığı tek yerde tutmak genel bağlamı anlamayı kolaylaştırır ama fonksiyonları bölüyorsanız gerçek bağlamı kaybetmemeye dikkat etmek gerekir
    • Dün LLM’lerle ilgili bir başlıkta “geliştiricilerin hep birlikte benimsediği güvenilmez araçlar” konuşuluyordu. Sanırım artık cevabı biliyorum…
  • Bazı durumlarda tam tersine yaklaşmak ve SIMD kullanmak gerekir. Örneğin AVX-512 gibi ortamlarda dallanmalı kodu, vektör maske register’ları kullanarak dallanmasız koda çevirebilirsiniz. Mesela for döngüsünün içindeki if, for döngüsünün dışındaki if’ten daha kolay yönetilebilir ve bellek erişimi açısından daha verimli olabilir. Somut bir örnek olarak, tek sayıysa +1, çift sayıysa -2 yapan bir işlem düşünün; normalde her döngü adımında dallanma gerekir ama SIMD ile vektörleştirirseniz 16 adet int değerini aynı anda işleyebilir ve hiç branch kullanmazsınız. Derleyici doğru şekilde vektörleştirebilirse, özgün kodu dallanmasız optimize bir sürüme dönüştürür
    • Verilen before kodu bence yazının ana fikrine tam uymuyor; tersine, optimize SIMD sürümü asıl fikre daha çok uyuyor. Örnekte for içindeki if, veriye bağlı bir dallanma olduğu için yukarı taşımak kolay değil. Eğer algoritma if (length % 2 == 1) { ... } else { ... } gibi döngü dışı bir koşul kullanıyor olsaydı, böyle bir koşulu elbette for’un üstüne taşımak doğru olurdu. SIMD sürümünde ise if tamamen ortadan kalkıyor; bence bu, yazarın da seveceği ideal kod kalıbı
    • Benim de aklıma hemen for döngüsünde öğe değerine göre dallanan kod geldi. Derleyicilerin böyle kodu otomatik vektörleştirmesi ne kadar zor, bilen var mı? Sınırın nerede olduğunu merak ediyorum
  • Şahsen bunun “iyi” bir kural olduğunu düşünmüyorum. Uygulanabileceği durumlar var ama bağlama göre o kadar değişiyor ki kesin bir sonuca varmak zor. İngilizce yazım kuralları gibi, istisnası o kadar çok ki fiilen kural saymak güç geliyor
  • (2023) O dönemdeki tartışma bağlantısı (662 puan, 295 yorum) https://news.ycombinator.com/item?id=38282950
  • Sandi Metz’in 99 Bottles of OOP kitabında buna benzer bir fikir görmüştüm. Benim tarzım değil ama dallanma mantığını çağrı yığınının en üstüne taşımak bazı durumlarda gerçekten faydalı. Özellikle bayrakların birkaç katman boyunca taşındığı kod tabanlarında bunu çok hissettim. https://sandimetz.com/99bottles
    • Aynı yazarın “The Wrong Abstraction” yazısı hemen aklıma geldi. for içi dallanma, “for kuraldır, dallanma ise davranış” gibi bir soyutlama yaratıyor. Ama yeni gereksinimler geldiğinde bu soyutlama bozuluyor ve zorla parametre ekleyerek ya da istisna işleme artırarak kodu anlaması güç bir hale getiriyorsunuz. Başta soyutlama kurmadan yazılmış olsaydı sonuç daha açık ve bakımı daha kolay olurdu. https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction