
Performans optimizasyonu yazılım mühendisliğinin önemli parçalarından biridir. Ancak yanlış zamanda yapılan optimizasyonlar performans kazandırmak yerine teknik borç üretir. Birçok proje henüz ihtiyaç oluşmadan cache mekanizmaları, karmaşık state yapıları veya gereksiz abstraction katmanları ile doldurulur. Bu yazıda erken optimizasyonun neden tehlikeli olduğunu, ne zaman gerekli hale geldiğini ve sürdürülebilir sistemler kurarken performansa nasıl yaklaşılması gerektiğini inceliyoruz.
Yazılım geliştirmeye başlayan çoğu kişi performans kelimesini duyduğunda heyecanlanır.
Kodun daha hızlı çalışması, daha az kaynak tüketmesi ve daha ölçeklenebilir hale gelmesi kulağa oldukça iyi gelir. Bu yüzden birçok geliştirici daha ortada gerçek bir problem yokken optimizasyon düşünmeye başlar. Daha hızlı çalışabilecek fonksiyonlar, daha karmaşık cache mekanizmaları, henüz ihtiyaç duyulmayan abstraction katmanları ve daha kullanılmadan ölçeklenmeye çalışılan mimariler...
İlginç olan şu ki çoğu zaman bu çalışmalar performansı değil, karmaşıklığı artırır.
Bilgisayar bilimlerinin en çok alıntılanan cümlelerinden biri şudur:
"Premature optimization is the root of all evil."
Bu cümle yıllardır tekrar edilse de çoğu zaman yanlış yorumlanır. Buradaki mesaj performansın önemsiz olduğu değildir. Asıl anlatılmak istenen, ölçülmeden yapılan optimizasyonun riskli olduğudur.
Örneğin daha ilk sprintlerde şöyle kodlar görmeye başlayabiliyoruz:
Buradaki soru şu:
Bu filtreleme işlemi gerçekten pahalı mı?
Yoksa birkaç satırlık basit bir operasyonu optimize etmeye mi çalışıyoruz?
Çoğu zaman ikinci senaryo gerçekleşiyor.
Henüz yüzlerce satırlık veri bile yokken memoization kullanmak, performans kazancı sağlamaktan çok kodun karmaşıklığını artırabiliyor.
Premature optimization'ın temel problemi budur. Ortada henüz kanıtlanmış bir performans problemi yoktur ama geliştirici gelecekte oluşabilecek olası bir problemi çözmeye çalışıyordur.
Örneğin bir React uygulamasında daha ilk sprintlerde useMemo, useCallback ve React.memo kullanımının yaygınlaştığını düşünelim. Bunun sebebi genellikle aynıdır:
"İleride performans problemi olabilir."
Fakat uygulamanın kaç kullanıcıya ulaşacağı bile belli değilken yapılan bu optimizasyonlar çoğu zaman gereksiz karmaşıklık oluşturur. Kod okunabilirliği azalır, onboarding süreci zorlaşır ve debug maliyeti artar.
Karşılığında elde edilen performans kazancı ise çoğu zaman ölçülemeyecek kadar küçüktür.
İlk versiyon:
Zamanla:
Buradaki soru artık sadece performans değildir.
Bu yapıyı ekipteki yeni biri ne kadar sürede anlayabilir?
Bir hata oluştuğunda debug etmek ne kadar kolay olacak?
Sisteme yeni bir özellik eklenirken bu cache davranışı ne kadar öngörülebilir olacak?
Performans kazancını konuşurken bu maliyetleri de hesaba katmak gerekir.
Performans konuşulurken genellikle CPU, RAM veya ağ trafiği konuşulur. Ancak yazılım projelerinde başka bir maliyet daha vardır:
Mühendislik maliyeti.
Bir optimizasyonun sisteme kattığı karmaşıklık da ödenmesi gereken bir bedeldir.
Basit bir veri çekme işlemi zamanla cache katmanları, invalidation kuralları, fallback senaryoları ve ekstra abstraction katmanları ile çevrelenebilir.
Bu yapı birkaç milisaniye kazandırabilir.
Ama aynı zamanda sistemin anlaşılmasını zorlaştırabilir.
Bu noktada sorulması gereken soru şudur:
Kazandığımız performans, ödediğimiz karmaşıklığa gerçekten değiyor mu?
Frontend tarafında bunun en yaygın örneği memoization araçlarının gereğinden fazla kullanılmasıdır.
useMemo, useCallback ve React.memo performans araçlarıdır. Ancak her performans aracının bir maliyeti vardır.
Bu araçlar:
Ek karşılaştırmalar yapar
Ek bellek kullanır
Kod okunabilirliğini azaltır
Debug süreçlerini zorlaştırabilir
Özellikle küçük component'lerde ve ucuz hesaplamalarda memoization çoğu zaman fayda sağlamaz.
Hatta bazı durumlarda performansı düşürebilir.
Sorun araçlarda değil, ölçmeden kullanımlarındadır.
Şöyle kullanımlar oldukça yaygın:
Ancak bu fonksiyonun yeniden oluşturulmasının maliyeti neredeyse yoksa, buradaki optimizasyon gerçek bir kazanç sağlamayabilir.
Bazı durumlarda sadece kodun okunmasını zorlaştırır.
Birçok projede henüz yüz kullanıcı bile yokken milyonlarca kullanıcı varmış gibi mimariler kurulmaya başlanır.
Mikroservisler oluşturulur.
Mesaj kuyrukları eklenir.
Dağıtık sistemler tasarlanır.
Karmaşık event-driven yapılar hazırlanır.
Fakat ürün henüz problem-market fit aşamasındadır.
Gerçek kullanıcı davranışları bilinmeden yapılan bu yatırımlar çoğu zaman boşa gider.
Çünkü sistem büyüdüğünde ihtiyaç duyulan mimari, başlangıçta tahmin edilen mimari olmayabilir.
Teknik borç sadece kötü kod değildir.
Gereksiz optimizasyonlar da teknik borç oluşturur.
Çünkü ekip artık şu sorularla yaşamak zorundadır:
Buradaki cache neden var?
Bu invalidation mantığı neyi çözüyor?
Bu abstraction neden oluşturulmuş?
Bunu kaldırırsak ne bozulur?
Kimsenin dokunmak istemediği alanlar oluşmaya başlar.
Ve zamanla sistem daha kırılgan hale gelir.
Bu noktada amaç optimizasyondan kaçınmak değildir.
Amaç doğru zamanda optimizasyon yapmaktır.
İyi bir yaklaşım genellikle şu sırayı takip eder:
Önce çalışan sistemi kur.
Sonra ölç.
Darboğazları tespit et.
Ve yalnızca gerekli olan yeri optimize et.
Ölçmeden yapılan optimizasyon tahmindir.
Ölçüm sonrasında yapılan optimizasyon ise mühendisliktir.
Premature optimization çoğu zaman iyi niyetle başlar.
Geliştirici sistemi geleceğe hazırlamak ister.
Ancak birçok durumda performans kazancı yerine karmaşıklık üretir.
İyi mühendislik her şeyi optimize etmek değildir.
Neyi optimize etmemek gerektiğini bilmektir.
Çünkü yazılım projelerinde en değerli kod her zaman en hızlı çalışan kod değildir.
En değerli kod; anlaşılabilen, bakım yapılabilen, geliştirilebilen ve gerçekten ihtiyaç duyulduğunda optimize edilen koddur.