Ancak bu OOP, performansa dayalı yazılım için bir dezavantaj olabilir mi, yani program ne kadar hızlı çalışır?
Genellikle evet !!! FAKAT...
Başka bir deyişle, birçok farklı nesne arasında birçok referans veya birçok sınıftan birçok yöntem kullanılması "ağır" bir uygulama ile sonuçlanabilir mi?
Şart değil. Bu dile / derleyiciye bağlıdır. Örneğin, sanal işlevleri kullanmamanız koşuluyla, optimize edilen bir C ++ derleyicisi, nesnenizi genel olarak sıfıra indirir. int
Oraya bir sarmalayıcı veya düz bir eski işaretçinin üzerine, bu düz eski veri türlerini doğrudan kullanmak kadar hızlı performans gösteren kapsamlı bir akıllı işaretçi yazmak gibi şeyler yapabilirsiniz .
Java gibi diğer dillerde, bir nesneye biraz genel bir yük vardır (çoğu durumda genellikle küçüktür, ancak gerçekten ufacık nesnelerle bazı nadir durumlarda astronomiktir). Örneğin Integer
, çok daha az verimlidir int
(64 bit'te 4 yerine 16 bayt alır). Ancak bu sadece bariz bir atık ya da bu tür bir şey değildir. Buna karşılık, Java her kullanıcı tanımlı tür üzerinde tekdüze yansıma gibi şeyler ve aynı zamanda işaretlenmemiş herhangi bir işlevi geçersiz kılma yeteneği sunar final
.
Yine de en iyi senaryoyu ele alalım: Nesne arabirimlerini sıfır ek yüke kadar optimize edebilen optimize eden C ++ derleyicisi . O zaman bile, OOP genellikle performansı düşürecek ve zirveye ulaşmasını engelleyecektir. Bu tam bir paradoks gibi gelebilir: nasıl olabilir? Sorun şurada yatmaktadır:
Arayüz Tasarımı ve Kapsülleme
Sorun, bir derleyici bir nesnenin yapısını sıfır ek yüküne kadar ezebilse bile (C ++ derleyicilerini optimize etmek için en azından genellikle doğrudur), ince taneli nesnelerin kapsülleme ve arayüz tasarımı (ve biriken bağımlılıklar) genellikle kitleler tarafından toplanması amaçlanan nesneler için en uygun veri gösterimleri (genellikle performans açısından kritik yazılımlar için geçerlidir).
Bu örneği ele alalım:
class Particle
{
public:
...
private:
double birth; // 8 bytes
float x; // 4 bytes
float y; // 4 bytes
float z; // 4 bytes
/*padding*/ // 4 bytes of padding
};
Particle particles[1000000]; // 1mil particles (~24 megs)
Diyelim ki bellek erişim düzenimiz, bu parçacıklar boyunca sırayla döngü yapmak ve bunları tekrar tekrar her çerçevenin etrafında hareket ettirmek, ekranın köşelerinden sekmek ve ardından sonucu oluşturmaktır.
Zaten birth
parçacıklar bitişik bir şekilde toplandığında elemanı düzgün bir şekilde hizalamak için göze çarpan 4 baytlık bir dolgu yükü görebiliriz . Zaten belleğin ~% 16.7'si hizalama için kullanılan ölü alanla boşa harcanmaktadır.
Bu çok tartışmalı görünebilir çünkü bugünlerde gigabayt DRAM var. Yine de bugün sahip olduğumuz en çirkin makineler bile, CPU önbelleğinin (L3) en yavaş ve en büyük bölgesi söz konusu olduğunda yalnızca 8 megabayta sahiptir . Oraya ne kadar az sığabilirsek, tekrarlanan DRAM erişimi açısından daha fazla ödeme yaparız ve daha yavaş şeyler olur. Aniden, hafızanın% 16,7'sini boşa harcamak artık önemsiz bir anlaşma gibi görünmüyor.
Alan hizalaması üzerinde herhangi bir etkisi olmadan bu ek yükü kolayca ortadan kaldırabiliriz:
class Particle
{
public:
...
private:
float x; // 4 bytes
float y; // 4 bytes
float z; // 4 bytes
};
Particle particles[1000000]; // 1mil particles (~12 megs)
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
Şimdi hafızayı 24 megavattan 20 megavat'a düşürdük. Sıralı erişim düzeniyle, makine şimdi bu verileri biraz daha hızlı tüketecektir.
Ama bu birth
alana biraz daha yakından bakalım . Bir parçacığın doğduğu (yaratıldığı) başlangıç zamanını kaydettiğini varsayalım. Alana yalnızca bir parçacık ilk oluşturulduğunda ve bir parçacığın ölüp ölmeyeceğini ve ekranda rastgele bir yerde yeniden doğup doğmayacağını görmek için her 10 saniyede bir erişildiğini düşünün. Bu durumda, birth
soğuk bir alandır. Kritik performans döngülerimizde erişilmez.
Sonuç olarak, gerçek performans açısından kritik veriler 20 megabayt değil, aslında 12 megabayt bitişik bir bloktur. Sık eriştiğimiz gerçek sıcak bellek , boyutunun yarısına küçüldü ! Orijinal, 24 megabaytlık çözümümüz üzerinde önemli hızlanmalar bekleyin (ölçülmesine gerek yok - bu tür şeyleri zaten binlerce kez yaptık, ancak şüpheniz varsa çekinmeyin).
Yine de burada ne yaptığımıza dikkat edin. Bu parçacık nesnesinin kapsüllenmesini tamamen kırdık. Durumu artık bir Particle
türün özel alanları ile ayrı, paralel bir dizi arasında bölünür . İşte bu noktada ayrıntılı nesne yönelimli tasarım ön plana çıkıyor.
Tek bir parçacık, tek bir piksel, hatta tek bir 4 bileşenli vektör, hatta bir oyundaki tek bir "yaratık" nesnesi gibi tek, çok parçalı bir nesnenin arayüz tasarımı ile sınırlı olduğunda optimum veri sunumunu ifade edemeyiz. Bir çita hızı, 2 metrekarelik ufacık bir adada duruyorsa ve nesne yönelimli tasarımın performans açısından genellikle yaptığı şey budur. Veri sunumunu en uygun olmayan bir nitelikle sınırlar.
Bunu daha da ileri götürmek için, sadece parçacıkları hareket ettirdiğimizden, x / y / z alanlarına üç ayrı döngüden erişebildiğimizi varsayalım. Bu durumda, 8 SPFP işlemini paralel olarak vektörleştirebilen AVX kayıtlarıyla SoA tarzı SIMD içsellerinden yararlanabiliriz. Ancak bunu yapmak için şimdi bu temsili kullanmalıyız:
float particle_x[1000000]; // 1mil particle X positions (~4 megs)
float particle_y[1000000]; // 1mil particle Y positions (~4 megs)
float particle_z[1000000]; // 1mil particle Z positions (~4 megs)
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
Şimdi parçacık simülasyonuyla uçuyoruz, ama parçacık tasarımımıza ne olduğuna bakın. Tamamen yıkıldı ve şimdi 4 paralel diziye bakıyoruz ve onları bir araya getirecek bir nesne yok. Nesneye yönelik Particle
tasarımımız sayonara gitti.
Bu, kullanıcıların hız talep ettikleri ve sadece doğruluklarının daha fazla talep ettikleri tek şey olduğu, performans açısından kritik alanlarda çalıştığım için birçok kez oldu. Bu ufacık nesneye yönelik tasarımların yıkılması gerekiyordu ve basamaklı kırılmalar genellikle daha hızlı tasarıma doğru yavaş bir kullanımdan çıkarma stratejisi kullanmamızı gerektiriyordu.
Çözüm
Yukarıdaki senaryo yalnızca ayrıntılı nesne yönelimli tasarımlarla ilgili bir sorun sunmaktadır . Bu durumlarda, genellikle SoA temsilcileri, sıcak / soğuk alan bölme, sıralı erişim kalıpları için dolgu azaltma sonucu daha verimli temsiller ifade etmek için yapıyı yıkmak zorunda kalıyoruz (dolgu bazen rastgele erişimli performans için yararlıdır AoS vakalarında kalıplar, ancak neredeyse her zaman sıralı erişim kalıpları için bir engel) vb.
Yine de, yerleştiğimiz son temsili alabilir ve yine de nesne yönelimli bir arayüzü modelleyebiliriz:
// Represents a collection of particles.
class ParticleSystem
{
public:
...
private:
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
float particle_x[1000000]; // 1mil particle X positions (~4 megs)
float particle_y[1000000]; // 1mil particle Y positions (~4 megs)
float particle_z[1000000]; // 1mil particle Z positions (~4 megs)
};
Şimdi iyiyiz. Sevdiğimiz tüm nesne yönelimli güzellikleri alabiliriz. Çita mümkün olduğunca hızlı koşmak için bir ülkeye sahiptir. Arayüz tasarımlarımız artık bizi darboğaz köşesine sıkıştırmıyor.
ParticleSystem
potansiyel olarak soyut bile olabilir ve sanal işlevleri kullanabilir. Şimdi tartışmalı , parçacık başına değil, parçacık toplama seviyesinde ek yükü ödüyoruz . Tepegöz, tek tek parçacık düzeyinde nesneleri modellersek, bunun ne olacağının 1 / 1.000.000'i kadardır.
Bu, ağır bir yükü işleyen gerçek performans açısından kritik alanlarda ve her türlü programlama dili için çözümdür (bu teknik C, C ++, Python, Java, JavaScript, Lua, Swift, vb. Arayüz tasarımı ve mimarisi ile ilgili olduğu için kolayca "erken optimizasyon" olarak etiketlenemez . Tek bir parçacığı, bir istemci yükü bağımlılığına sahip bir nesne olarak modelleyen bir kod tabanı yazamıyoruzParticle's
ve daha sonra zihnimizi değiştirelim. Eski kod tabanlarını optimize etmek için çağrıldığımda çok şey yaptım ve bu, hantal tasarımını kullanmak için on binlerce kod satırını dikkatlice yeniden yazmanız gerekebilir. Bu ideal olarak, ağır bir yük öngörebilmemiz koşuluyla, ön plana çıkma şeklimizi etkiler.
Bu yanıtı birçok performans sorusunda, özellikle de nesne yönelimli tasarımla ilgili bir biçimde ya da başka bir şekilde yankılamaya devam ediyorum. Nesne yönelimli tasarım yine de en yüksek talep performans performansıyla uyumlu olabilir, ancak bununla ilgili düşünme şeklimizi biraz değiştirmeliyiz. Bu çitaya olabildiğince hızlı çalışması için bir yer vermeliyiz ve herhangi bir durumu zar zor saklayan ufacık küçük nesneler tasarlarsak bu genellikle imkansızdır.