C ++ 'da sanal fonksiyonlar neden ve nasıl yavaşlar?


38

Herkes sanal masanın tam olarak nasıl çalıştığını ve sanal işlevler çağrıldığında hangi işaretçilerle ilişkilendirildiğini ayrıntılı olarak açıklayabilir mi?

Aslında daha yavaşlarsa, sanal işlevin yürütülmesi için geçen süreyi normal sınıf yöntemlerinden daha fazla gösterir misiniz? Bazı kodları görmeden, nasıl / ne olup bittiğini takip etmek kolaydır.


5
Bir vtable'dan doğru yöntem çağrısının aranması, doğrudan yapılması gereken yöntemlerden daha uzun süreceği için yapılması gereken bir şeydir . Ne kadar uzun süre ya da bu ek zamanın kendi programınız bağlamında önemli olup olmadığı bir başka sorundur. en.wikipedia.org/wiki/Virtual_method_table
Robert Harvey

10
Tam olarak neyden daha yavaş? Bazı programcıların sanal işlevlerin yavaş olduğunu duymasından dolayı, çok sayıda switch ifadesiyle dinamik davranışın kırılması ve yavaş uygulanmasına neden olan kod gördüm.
Christopher Creutzig

7
Çoğu zaman, sanal aramaların kendileri yavaş değildir, ancak derleyicinin onları satır içi yapamadığı bir şey değildir.
Kevin Hsu

4
@Kevin Hsu: evet bu kesinlikle. Neredeyse herhangi bir zaman birileri size bazı "sanal işlev çağrısı ek yükünü" ortadan kaldırma konusunda bir hız kazandıklarını söylerse, gerçekte tüm hızlandırma işleminin nereden geldiğine bakarsanız, derleyici genelinde optimizasyon yapamayacağı için mümkün olan optimizasyonlardan kaynaklanır. belirsiz çağrı önceden.
tim

7
Montaj kodunu okuyabilen bir kişi bile, fiili CPU uygulamasında genel giderini doğru bir şekilde tahmin edemez. Masaüstü tabanlı CPU üreticileri, yıllarca süren araştırmalara yalnızca branş tahmininde değil aynı zamanda sanal işlevlerin gecikmesini maskelemenin birincil nedeni için değer tahmininde ve spekülatif uygulamalarda yatırım yaptılar. Neden? Çünkü masaüstü işletim sistemleri ve yazılımlar onları çok kullanıyor. (Mobil CPU'lar hakkında aynı şey söyleyemem.)
04

Yanıtlar:


55

Sanal yöntemler genellikle işlev işaretçilerinin depolandığı sanal yöntem tabloları (kısaca seçilebilir) aracılığıyla uygulanır. Bu, asıl aramaya dolaysızlık ekler (sadece vtable'dan çağrılacak fonksiyonun adresini getirmeli, sonra onu çağırmalıyız - tam olarak onu çağırmak yerine). Tabii ki, bu biraz zaman ve biraz daha kod alır.

Bununla birlikte, ille de yavaşlamanın ana nedeni değildir. Asıl sorun, derleyicinin (genellikle / genellikle) hangi fonksiyonun çağrılacağını bilmemesidir . Dolayısıyla satır içi yapamaz ya da başka bir optimizasyon gerçekleştiremez. Bu tek başına bir düzine anlamsız talimat ekleyebilir (kayıtları hazırlamak, çağırmak, daha sonra durumu geri yüklemek) ve görünüşte alakasız optimizasyonları engelleyebilir. Dahası, birçok farklı uygulamayı çağırarak çılgın gibi dallarsanız, diğer yollarla çılgın gibi dallanacağınız hitlere benzer şekilde katlanırsınız: Önbellek ve dal tahmincisi size yardımcı olmaz, dallar mükemmel bir öngörülebilirden daha uzun sürer dalı.

Büyük ama : Bu performans isabetleri genellikle madde için çok küçük. Yüksek performanslı bir kod oluşturmak isteyip istemediğinize endişe verici bir sıklıkta çağrılacak sanal bir fonksiyon eklemeyi düşünebilirsiniz. Ancak, aynı zamanda diğer dallanma araçlarla sanal işlev çağrıları yerine (unutmayın if .. else, switchişlev işaretçileri, vs.) temel sorununu çözmez - çok iyi yavaş olabilir. Sorun (eğer varsa) sanal işlevler değil (gereksiz) dolaylı yüklemelerdir.

Düzenleme: Arama talimatlarındaki fark diğer cevaplarda açıklanmıştır. Temel olarak, statik ("normal") bir çağrının kodu:

  • Çağrılan işlevin bu kayıtları kullanmasına izin vermek için yığındaki bazı kayıtları kopyalayın.
  • Argümanları önceden tanımlanmış konumlara kopyalayın, böylece çağrılan işlev, çağrıldığı yerden bağımsız olarak bunları bulabilir.
  • Dönüş adresini it.
  • Bir derleme zamanı adresi olan fonksiyon koduna dallama / atlama ve dolayısıyla derleyici / linker tarafından ikili kodda kodlanmış.
  • Önceden tanımlanmış bir konumdan dönüş değerini alın ve kullanmak istediğimiz kayıtları geri yükleyin.

Sanal bir çağrı, işlev adresinin derleme zamanında bilinmemesi dışında tamamen aynı şeyi yapar. Bunun yerine, birkaç talimat ...

  • Nesneden, her sanal işlev için bir işlev işaretçisi dizisine (işlev adresleri) işaret eden vtable işaretçisini alın.
  • Vtable'den doğru fonksiyon adresini bir sicile kaydedin (derleme zamanında doğru fonksiyon adresinin saklandığı indekse karar verilir).
  • Kodlanmış bir adrese atlamak yerine, o kayıttaki adrese atlayın.

Dallara gelince: Dal, bir sonraki komutun çalıştırılmasına izin vermek yerine başka bir komutla atlayan herhangi bir şeydir. Bunlar arasında if, switchbazen çeşitli döngüler, fonksiyon çağrıları vb ve olmayan derleyici uygular şeyler parçaları aslında başlık altında bir şube ihtiyacı olduğu şekilde dal gibi görünüyor. Bkz Neden sırasız dizilerde daha hızlı sıralanmış bir dizi işliyor? Bunun neden yavaş olabileceğine, işlemcilerin bu yavaşlamaya karşı koymak için ne yaptığı ve bunun nasıl bir tedavi olmadığı.


6
@ JörgWMittag hepsi tercüman şeyler ve hala C ++ derleyicileri tarafından oluşturulan ikili kodlardan daha yavaşlar
Sam

13
@ JörgWMittag Bu optimizasyonlar öncelikle serbest indirection / geç bağlama (neredeyse) yapmak için var gerekmediğinde zaman bu dillerde çünkü her çağrı teknik olarak sonradan bağlanan edilir. Kısa bir süre içinde bir yerden bir çok farklı sanal yöntemi gerçekten ararsanız, bu optimizasyonlar yardımcı olmaz veya aktif olarak zarar vermez (kullanım için çok fazla kod oluşturun). C ++ adamları bu optimizasyonlarla pek ilgilenmiyorlar çünkü çok farklı bir

10
@ JörgWMittag ... C ++ adamları bu optimizasyonlarla pek ilgilenmiyorlar çünkü çok farklı bir durumdalar: AOT-derlenmiş oy verme şekli zaten oldukça hızlı, çok az çağrı aslında sanal, birçok polimorfizm vakası erken- bağlı (şablonlar aracılığıyla) ve dolayısıyla AOT optimizasyonuna övgüye değer. Son olarak, bu optimizasyonları uyarlamalı olarak yapmak (sadece derleme zamanında spekülasyon yapmak yerine), tonlarca baş ağrısı getiren çalışma zamanı kodu oluşturma gerektirir . JIT derleyicileri bu sorunları zaten başka nedenlerle çözmüştür, bu yüzden aldırmazlar, ancak AOT derleyicileri bundan kaçınmak ister.

3
büyük cevap, +1. Yine de not edilmesi gereken bir nokta, bazen dallanma sonuçları derleme zamanında bilinmektedir, örneğin farklı kullanımları desteklemesi gereken çerçeve sınıfları yazdığınızda, ancak uygulama kodu bu sınıflarla etkileşime girdiğinde, belirli kullanım zaten bilinmektedir. Bu durumda, sanal fonksiyonlara alternatif, C ++ şablonları olabilir. İyi örnek, sanal işlev davranışını herhangi bir değişken olmadan taklit eden CRTP olacaktır: en.wikipedia.org/wiki/Curiously_recurring_template_pattern
DXM

3
@James Bir noktanız var. Söylemeye çalıştığım şey: Herhangi bir dolaylı işlemin aynı sorunları var, buna özgü bir şey yok virtual.

23

Sırasıyla sanal işlev çağrısı ve sanal olmayan çağrının demonte ettiği bazı kodlar:

mov    -0x8(%rbp),%rax
mov    (%rax),%rax
mov    (%rax),%rax
callq  *%rax

callq  0x4007aa

Sanal aramanın doğru adrese bakmak için üç ek talimat gerektirdiğini, sanal olmayan aramanın adresinin derlenebildiğini görebilirsiniz.

Ancak, fazladan arama süresinin çoğu zaman ihmal edilebilir olarak kabul edilebileceğini unutmayın. Arama süresinin önemli olacağı durumlarda, bir döngüde olduğu gibi, değer genellikle döngüden önceki ilk üç talimatı yaparak önbelleğe alınabilir.

Arama süresinin önemli hale geldiği diğer durum, nesneler koleksiyonunuz varsa ve her birine sanal bir işlev çağrısı yaparak döngü yapıyor olmanızdır. Bununla birlikte, bu durumda, hangi fonksiyonu arayacağınıza karar vermek için bazı araçlara ihtiyacınız olacak ve sanal masa araması herhangi bir araç kadar iyi. Aslında, vtable arama kodu çok yaygın bir şekilde kullanıldığı için yoğun bir şekilde optimize edilmiştir, bu yüzden etrafta el ile çalışmaya çalışmak daha kötü performansa neden olma şansına sahiptir .


1
Anlaşılması gereken şey, kararsız arama ve dolaylı çağrının hemen hemen her durumda, çağrılan yöntemin toplam çalışma süresi üzerinde ihmal edilebilir bir etkisi olacağıdır.
John R. Strohm

11
@ JohnR.Strohm Bir erkeğin ihmal edilebilirliği başka bir erkeğin darboğazıdır
James

1
-0x8(%rbp). oh benim ... bu AT&T sözdizimi.
Abyx

" üç ilave talimat " hayır, sadece iki tane: vptr'in yüklenmesi ve işlev göstergesinin yüklenmesi
curiousguy

@curiousguy bu aslında üç ek talimattır. Sanal bir yöntemin her zaman bir işaretçide çağrıldığını unuttum , bu yüzden önce işaretçiyi bir register'a yüklemelisiniz. Özetlemek gerekirse, ilk adım, işaretçi değişkeninin% rax registerına tutan adresi yüklemek, ardından register içindeki adrese göre,% rax'i kaydetmek için bu adrese vtpr'yi yüklemek, ardından bu adrese göre % rax'a çağrılacak yöntemin adresini kaydedin, ardından callq *% rax !.
Gab,

18

Neyden daha yavaş ?

Sanal işlevler, doğrudan işlev çağrılarıyla çözülemeyen bir sorunu çözer. Genel olarak, yalnızca aynı şeyi hesaplayan iki programı karşılaştırabilirsiniz. "Bu ışın izleyici, derleyiciden daha hızlıdır" anlamsızdır ve bu ilke, bireysel işlevler veya programlama dili yapıları gibi küçük şeyler için bile geneldir.

Dinamik olarak bir nesnenin türü gibi bir verilere dayanan bir kod parçasına geçmek için sanal bir işlev kullanmazsanız switch, aynı şeyi gerçekleştirmek için bir ifade gibi başka bir şey kullanmanız gerekir . Başka bir şeyin kendi genel giderleri, artı programın organizasyonu üzerindeki sürdürülebilirliğini ve küresel performansını etkileyen etkileri vardır.

C ++ 'da sanal işlevlere yapılan çağrıların her zaman dinamik olmadığını unutmayın. Çağrılar, tam tipi bilinen bir nesnede yapıldığında (nesne bir işaretçi ya da başvuru olmadığından ya da türü aksi takdirde statik olarak çıkarılabildiğinden), çağrılar yalnızca normal üye işlev çağrılarıdır. Bu, yalnızca genel gider gönderilmediği anlamına gelmez, aynı zamanda bu aramaların normal aramalarla aynı şekilde çizilebileceği anlamına gelir.

Başka bir deyişle, C ++ derleyiciniz sanal işlevler sanal gönderim gerektirmediğinde işe yarayabilir, bu nedenle genellikle sanal olmayan işlevlere göre performansları için endişelenmenize gerek yoktur.

Yeni: Ayrıca, paylaşılan kütüphaneleri de unutmamalıyız. Paylaşılan bir kütüphanede bulunan bir sınıf kullanıyorsanız, sıradan bir üye işlevine yapılan çağrı, yalnızca basit bir komut dizisi olmayacaktır callq 0x4007aa. Bir "program bağlantı tablosu" ndan veya dolayısı ile böyle bir yapıdan dolaylı olarak geçmek gibi, birkaç çember içinden geçmesi gerekir. Bu nedenle, paylaşılan kitaplık yönlendirmesi (tamamen olmasa da) sanal arama ile doğrudan arama arasındaki maliyet farkını bir dereceye kadar seviyelendirebilir. Bu yüzden sanal fonksiyon değişimlerinde akıl yürütme, programın nasıl inşa edildiğini hesaba katmalıdır: Hedef nesnenin sınıfının tek çağrı yaparak programa bağlı olup olmadığı.


4
"Ne daha yavaş?" - Olması gerekmeyen bir yöntemi sanal hale getirirseniz, oldukça iyi bir karşılaştırma malzemesine sahip olursunuz.
tdammers

2
Sanal işlevlere yapılan çağrıların her zaman dinamik olmadığını belirttiğiniz için teşekkür ederiz. Buradaki diğer her yanıt, sanal olarak bir durumun ilan edilmesine benziyor, durum ne olursa olsun, otomatik bir performans isabeti anlamına geliyor.
Syndog

12

çünkü sanal bir arama eşdeğerdir

res_t (*foo)(arg_t);
foo = (obj->vtable[foo_offset]);
foo(obj,args)

sanal olmayan bir işlevle, derleyici ilk satırı sabit olarak katlayabiliyorsa, bu bir zorunluluktur; bir ekleme ve sadece statik bir çağrıya dönüştürülmüş dinamik bir çağrı

Bu aynı zamanda fonksiyonun satır içi olmasını sağlar (tüm gerekli optimizasyon sonuçlarıyla birlikte)

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.