Zaten mükemmel olan bu cevaplar arasına atlamak istedim ve polimorf kodun değiştirilen anti-paternine switches
veya if/else
ölçülen kazançlara sahip dallara karşı aslında çirkin bir yaklaşım benimsediğimi itiraf ettim . Ama bu toptancıyı yapmadım, sadece en kritik yollar için. O kadar siyah ve beyaz olmak zorunda değil.
Bir feragatname olarak, hız genellikle aranan en rekabetçi niteliklerden biri iken, doğruluğun elde edilmesinin çok zor olmadığı (ve genellikle de bulanık ve yaklaşık olarak tahmin edilen) ışın izleme gibi alanlarda çalışıyorum. Oluşturma sürelerinde bir azalma genellikle en yaygın kullanıcı taleplerinden biridir, sürekli olarak kafalarımızı kaşıyor ve en kritik ölçülen yollar için bunu nasıl başaracağımızı buluyoruz.
Şartların Polimorfik Yeniden Düzenlenmesi
İlk olarak, polimorfizmin koşullu dallanmadan ( switch
veya bir grup if/else
ifadeden) bir sürdürülebilirlik açısından neden tercih edilebileceğini anlamaya değer . Buradaki ana fayda genişletilebilirliktir .
Polimorfik kod ile, kod tabanımıza yeni bir alt tip ekleyebilir, bazı polimorfik veri yapısına örnekler ekleyebilir ve mevcut tüm polimorfik kodun başka bir değişiklik yapmadan otomatik olarak çalışmasını sağlayabiliriz. "Bu tür 'foo' ise, bunu yapın" biçimine benzeyen büyük bir kod tabanına dağılmış bir sürü kodunuz varsa , kendinizi tanıtmak için 50 farklı kod bölümünü güncellemenin korkunç bir yüküyle karşılaşabilirsiniz. yeni bir tür şey, ve yine de birkaç eksik.
Kod tabanınızın bu tür denetimleri yapması gereken bir çift veya hatta bir bölümünüz varsa, polimorfizmin sürdürülebilirlik faydaları doğal olarak azalır.
Optimizasyon Bariyeri
Buna dallanma ve boru hattı açısından çok bakmamanızı ve daha çok optimizasyon bariyerlerinin derleyici tasarım zihniyetinden bakmayı öneririm. Verileri alt türe göre sıralama (sıraya uyuyorsa) gibi her iki durum için de geçerli olan şube tahminini iyileştirmenin yolları vardır.
Bu iki strateji arasında daha farklı olan şey, optimize edicinin önceden sahip olduğu bilgi miktarıdır. Bilinen bir fonksiyon çağrısı çok daha fazla bilgi sağlar, derleme zamanında bilinmeyen bir fonksiyonu çağıran dolaylı bir fonksiyon çağrısı bir optimizasyon bariyerine yol açar.
Çağrılan işlev bilindiğinde, derleyiciler yapıyı yok edebilir ve smithereens'e ezebilir, çağrıları yönlendirebilir, potansiyel örtüşme yükünü ortadan kaldırabilir, talimat / kayıt tahsisinde daha iyi bir iş yapabilir, muhtemelen döngüler ve diğer dal formlarını yeniden düzenleyebilir, hatta sert üretebilir uygun olduğunda kodlanmış minyatür LUT'lar (bir şey GCC 5.3 switch
, sonuçlar için bir atlama tablosu yerine sabit kodlu bir LUT kullanarak bir deyimle beni şaşırttı ).
Dolaylı bir işlev çağrısında olduğu gibi, karışıma derleme zamanı bilinmeyenlerini tanıtmaya başladığımızda bu avantajlardan bazıları kaybolur ve koşullu dallanma büyük olasılıkla bir avantaj sağlayabilir.
Bellek Optimizasyonu
Sıkı bir döngüde art arda gelen bir yaratık dizisinden oluşan bir video oyunu örneği alın. Böyle bir durumda, bunun gibi bazı polimorfik bir kabımız olabilir:
vector<Creature*> creatures;
Not: basitlik için unique_ptr
burada kaçındım .
... Creature
polimorfik baz tipi nerede . Bu durumda, polimorfik kaplarla ilgili zorluklardan biri, genellikle her alt tip için ayrı ayrı / ayrı ayrı bellek ayırmak istemeleridir (örn: operator new
her bir canlı için varsayılan fırlatma kullanarak ).
Bu genellikle, dallanma yerine bellek tabanlı optimizasyon için ilk önceliklendirmeyi yapar (buna ihtiyacımız olursa). Buradaki bir strateji, her bir alt tip için sabit bir ayırıcı kullanmak, büyük parçalar halinde ayrılarak ve tahsis edilen her alt tip için belleği bir araya getirerek bitişik bir temsili teşvik etmektir. Böyle bir stratejiyle, bu creatures
kapsayıcıyı alt türe (adresin yanı sıra) göre sıralamaya kesinlikle yardımcı olabilir , çünkü bu sadece şube tahminini iyileştirmekle kalmaz, aynı zamanda referans yerini de geliştirir (aynı alt türün birden fazla yaratığına erişilmesine izin verir) tahliye öncesi tek bir önbellek satırından).
Veri Yapılarının ve Döngülerinin Kısmi Devrileştirilmesi
Diyelim ki tüm bu hareketlerden geçtiniz ve hala daha fazla hız istiyorsunuz. Burada girişimde bulunduğumuz her adımın sürdürülebilirliği bozduğu ve performansın geri dönüşünün azalmasıyla biraz metal taşlama aşamasında olacağımızı belirtmek gerekir. Bu nedenle, daha küçük ve daha küçük performans kazançları için sürdürülebilirliği daha da feda etmeye istekli olduğumuz bu bölgeye girersek oldukça önemli bir performans talebi olması gerekiyor.
Yine de denemek için bir sonraki adım (ve her zaman yardımcı olmazsa değişikliklerimizi geri çekme istekliliğiyle ) manuel olarak sanallaştırma olabilir .
Sürüm kontrolü ipucu: benden çok daha fazla optimizasyon meraklısı değilseniz, optimizasyon çabalarımız çok iyi olabiliyorsa, atmak için istekli olan bu noktada yeni bir şube oluşturmaya değer olabilir. Benim için her şey, bu tür noktalardan sonra bile bir profiler olsa bile deneme yanılma.
Bununla birlikte, bu zihniyet toptancılığını uygulamak zorunda değiliz. Örneğimize devam edelim, bu video oyununun çoğunlukla insan yaratıklarından oluştuğunu varsayalım. Böyle bir durumda, yalnızca insan yaratıklarını kaldırarak ve sadece onlar için ayrı bir veri yapısı oluşturarak devredebiliriz.
vector<Human> humans; // common case
vector<Creature*> other_creatures; // additional rare-case creatures
Bu, kod tabanımızda yaratıkları işlemesi gereken tüm alanların, insan yaratıkları için ayrı bir özel durum döngüsüne ihtiyaç duyduğu anlamına gelir. Yine de bu, en yaygın yaratık türü olan insanlar için dinamik dağıtım yükünü (veya belki de daha uygun bir şekilde optimizasyon bariyerini) ortadan kaldırır. Bu alanların sayısı çok fazlaysa ve bunu karşılayabilirsek, bunu yapabiliriz:
vector<Human> humans; // common case
vector<Creature*> other_creatures; // additional rare-case creatures
vector<Creature*> creatures; // contains humans and other creatures
... bunu karşılayabilirsek, daha az kritik yollar oldukları gibi kalabilir ve tüm yaratık türlerini soyut olarak işleyebilir. Kritik yollar humans
bir döngüde ve other_creatures
ikinci döngüde işlenebilir .
Bu stratejiyi gerektiği şekilde genişletebilir ve potansiyel olarak bu şekilde bazı kazançları sıkabiliriz, ancak süreçteki sürdürülebilirliği ne kadar aşağı çektiğimizi belirtmek gerekir. İşlev şablonlarını burada kullanmak, mantığı manuel olarak çoğaltmadan hem insanlar hem de yaratıklar için kod oluşturmaya yardımcı olabilir.
Sınıfların Kısmi Devrileştirilmesi
Yıllar önce yaptığım bir şey gerçekten iğrençti ve artık yararlı olduğuna emin değilim (bu C ++ 03 döneminde), bir sınıfın kısmi olarak yeniden yapılandırılmasıydı. Bu durumda, zaten başka bir amaçla (temel sınıfta sanal olmayan bir erişimci aracılığıyla erişilir) bir sınıf kimliği saklıyorduk. Orada buna benzer bir şey yaptık (hafızam biraz puslu):
switch (obj->type())
{
case id_common_type:
static_cast<CommonType*>(obj)->non_virtual_do_something();
break;
...
default:
obj->virtual_do_something();
break;
}
... virtual_do_something
alt sınıfta sanal olmayan sürümleri çağırmak için uygulandı. Brüt, biliyorum, bir işlev çağrısını devralize etmek için açık bir statik downcast yapmak. Yıllardır bu tür şeyleri denemediğim için bunun ne kadar yararlı olduğu hakkında hiçbir fikrim yok. Veri odaklı tasarıma maruz kaldığımda, veri yapılarını ve döngüleri sıcak / soğuk bir şekilde bölme stratejisinin çok daha yararlı olduğunu, optimizasyon stratejileri için daha fazla kapı açtığını (ve çok daha az çirkin) buldum.
Toptan Devrileştirme
Şimdiye kadar hiç optimizasyon zihniyetini uygulayamadığımı itiraf etmeliyim, bu yüzden faydaları hakkında hiçbir fikrim yok. Sadece bir merkezi koşul kümesi olacağını bildiğim durumlarda öngörüde dolaylı işlevlerden kaçındım (örn: tek bir merkezi yer işleme olayı ile olay işleme), ancak hiçbir zaman polimorfik bir zihniyetle başlamadı ve tümüyle optimize edildi buraya kadar.
Teorik olarak, buradaki acil yararlar, bu optimizasyon engellerini tamamen yok etmenin yanı sıra, sanal bir işaretçiden daha fazla bir tür tanımlamanın potansiyel olarak daha küçük bir yolu olabilir (örneğin: 256 benzersiz tür veya daha az olduğu fikrini taahhüt ederseniz tek bir bayt). .
switch
Veri yapılarınızı ve döngülerinizi alt türe göre ayırmak zorunda kalmadan tek bir merkezi deyim kullanırsanız veya bir sipariş varsa, bazı durumlarda bakımı daha kolay kod yazmanız (yukarıdaki optimize edilmiş manuel devrileştirme örneklerine karşı) yardımcı olabilir. - işlerin kesin bir sırayla işlenmesi gereken bu durumlarda bağımlılık (bu, her yerde dallanmamıza neden olsa bile). Bu, yapılması gereken çok fazla yeriniz olmadığı durumlar içindir switch
.
Bu, performansı oldukça kritik bir zihniyet olsa bile, bakımı oldukça kolay olmadıkça bunu tavsiye etmem. "Bakımı kolay" iki baskın faktöre bağlı olma eğilimindedir:
- Gerçek bir genişletilebilirlik gereksinimine sahip olmama (ör: işlenecek tam olarak 8 türünüz olduğundan emin olmak ve asla daha fazla şey bilmemek ).
- Kodunuzda bu türleri kontrol etmesi gereken çok yer olmaması (ör: tek bir merkezi yer).
... yine de çoğu durumda yukarıdaki senaryoyu tavsiye ediyorum ve gerektiğinde kısmi yeniden sanallaştırma ile daha verimli çözümlere doğru ilerliyorum. Genişletilebilirlik ve bakım kolaylığı ihtiyaçlarını performansla dengelemek için size çok daha fazla nefes alan sağlar.
Sanal İşlevler ve İşlev İşaretçileri
Bunun üstesinden gelmek için, burada sanal işlevler ve işlev işaretçileri hakkında bazı tartışmaların olduğunu fark ettim. Sanal işlevlerin aramak için biraz ekstra iş gerektirdiği doğrudur, ancak bu daha yavaş oldukları anlamına gelmez. Sezgisel olarak, onları daha da hızlı hale getirebilir.
Burada sezgisel bir durumdur, çünkü bellek hiyerarşisinin çok daha önemli bir etkiye sahip olma dinamiklerine dikkat etmeden talimatları talimat olarak ölçmeye alışkınız.
class
20 sanal işlev ile struct
20 işlev işaretçisi depolayan ve her ikisi birden çok kez başlatılan bir ile karşılaştırırsak class
, bu durumda her bir örneğin bellek yükü 64 bit makinelerde sanal işaretçi için 8 bayt, bellek yükü struct
160 bayttır.
Pratik maliyet, sanal işaretler (ve muhtemelen yeterince büyük bir giriş ölçeğinde sayfa hataları) kullanarak işlev işaretçileri tablosu ile sınıfa karşı çok daha zorunlu ve zorunlu olmayan önbellek özlüyor olabilir. Bu maliyet, sanal bir tabloyu dizine ekleme işleminin biraz daha fazla yapılmasını engelleme eğilimindedir.
Ayrıca structs
, işlev işaretçileriyle doldurulmuş ve sayısız kez başlatılan eski C kod tabanlarıyla (benden daha eski) uğraştım , aslında sanal işlevlere sahip sınıflara dönüştürerek önemli performans kazançları (% 100'den fazla iyileştirme) verdi ve sadece bellek kullanımındaki büyük azalma, önbellek dostu olma vb.
Flip tarafında, karşılaştırmalar elmalarla elma hakkında daha fazla hale geldiğinde, benzer şekilde, bu tür senaryolarda yararlı olmak için bir C ++ sanal işlev zihniyetinden C stili işlev işaretçi zihniyetine çevirmenin tersi zihniyetini buldum:
class Functionoid
{
public:
virtual ~Functionoid() {}
virtual void operator()() = 0;
};
... sınıfın ölçülebilir bir şekilde geçersiz kılınabilen tek bir işlevi (ya da sanal yıkıcıyı saydığımızda ikisini) sakladığı yerdi. Bu durumlarda, kritik yollarda bunu buna dönüştürmek kesinlikle yardımcı olabilir:
void (*func_ptr)(void* instance_data);
... tehlikeli atıkları gizlemek / atmak için güvenli bir arayüzün arkasında ideal bir şekilde void*
.
Tek bir sanal işleve sahip bir sınıfı kullanmaya meyilli olduğumuz durumlarda, bunun yerine işlev işaretleyicilerini hızlı bir şekilde kullanmaya yardımcı olabilir. Büyük bir neden, bir işlev işaretçisi çağırmanın maliyetinin azalması bile değildir. Çünkü artık onları kalıcı bir yapıda toplarsak, her bir ayrı fonksiyonoidleri yığının dağınık bölgelerine tahsis etme cazibesiyle karşı karşıya değiliz. Bu tür bir yaklaşım, örnek verileri homojense, örneğin yalnızca davranış değişirse, yığınla ilişkili ve bellek parçalanması yükünü önlemeyi kolaylaştırabilir.
Bu nedenle, işlev işaretçileri kullanmanın yardımcı olabileceği bazı durumlar vardır, ancak genellikle bir grup işlev işaretçisi tablosunu sınıf örneği başına yalnızca bir işaretçinin depolanmasını gerektiren tek bir vtable ile karşılaştırırsak, bunu başka bir şekilde buldum. . Bu vtable genellikle bir veya daha fazla L1 önbellek satırında ve sıkı döngülerde oturacaktır.
Sonuç
Her neyse, bu konudaki benim küçük dönüşüm. Bu alanlara dikkatle girmenizi öneririm. Güven ölçümleri, içgüdüsel değildir ve bu optimizasyonların genellikle sürdürülebilirliği azaltma şekli göz önüne alındığında, yalnızca karşılayabildiğiniz kadar uzağa gidin (ve akıllıca bir yol sürdürülebilirlik tarafında hata yapmak olacaktır).