Mühürlü sınıflar gerçekten performans avantajları sunuyor mu?


140

Ekstra performans avantajları elde etmek için sınıflarınızı mühürlü olarak işaretlemeniz gerektiğini söyleyen bir çok optimizasyon ipucuna rastladım.

Performans farkını kontrol etmek için bazı testler yaptım ve hiçbirini bulamadım. Yanlış bir şey mi yapıyorum? Mühürlü sınıfların daha iyi sonuç vereceği durumu kaçırıyor muyum?

Testler yapan ve bir fark gören var mı?

Öğrenmeme yardım et :)


9
Mühürlü sınıfların mükemmel bir artış sağladığını düşünmüyorum. Yaptıkları gerçeği tesadüfi olabilir. Buna ek olarak, mühürlü sınıfları kullanmak için uyguladıktan sonra uygulamanızı profillendirin ve çabaya değip değmeyeceğini belirleyin. Gereksiz bir mikro optimizasyon yapmak için genişletilebilirliğinizi kilitlemek uzun vadede size mal olacaktır. Tabii ki, profilli olursanız ve mükemmel kriterlerinizi (perf uğruna mükemmel olmaktan ziyade) vurmanıza izin verirse, o zaman harcanan paraya değiyorsa bir takım olarak karar verebilirsiniz. Mükemmel olmayan nedenlerle sınıfları mühürlediyseniz, em :)
Merlyn Morgan-Graham

1
Yansımayı denediniz mi? Yansıma ile örneklemenin mühürlü sınıflarla daha hızlı olduğunu bir yerde okudum
on

Bildiğim kadarıyla yok. Mühürlü farklı bir nedenden dolayı var - birçok durumda yararlı / gerekli olabilecek genişletilebilirliği engellemek. Performans optimizasyonu burada bir hedef değildi.
TomTom

... ancak derleyici hakkında düşünürseniz: sınıfınız mühürlenmişse, derleme zamanında sınıfınızda çağırdığınız bir yöntemin adresini bilirsiniz. Sınıfınız mühürlenmemişse, geçersiz kılma çağrısı yapmanız gerekebileceğinden yöntemi çalışma zamanında çözmeniz gerekir. Kesinlikle ihmal edilebilir olacak, ancak bazı farklar olabileceğini görebiliyordum .
Dan Puzey

Evet, ancak OP'nin belirttiği gibi, bu tür gerçek faydalara dönüşmez. Mimari farklılıklar çok daha alakalı olabilir.
TomTom

Yanıtlar:


60

JITter bazen kapalı sınıflardaki yöntemlere sanal olmayan çağrıları kullanacaktır, çünkü daha fazla genişletilemezler.

Arama türü, sanal / sanal olmayan ile ilgili karmaşık kurallar vardır ve hepsini bilmiyorum, bu yüzden onları sizin için gerçekten özetleyemem, ancak mühürlü sınıflar ve sanal yöntemler için google'ı konu hakkında bazı makaleler bulabilirsiniz.

Bu optimizasyon seviyesinden elde edebileceğiniz her türlü performans avantajının son çare olarak görülmesi gerektiğini unutmayın; kod düzeyinde optimize etmeden önce her zaman algoritmik düzeyde optimize edin.

İşte bundan bahseden bir bağlantı: Mühürlü anahtar kelime ile uğraşmak


2
'başıboş' bağlantı, teknik iyilik gibi görünse de aslında saçmadır. Daha fazla bilgi için makaledeki yorumları okuyun. Özet: Verilen 3 neden Sürüm Oluşturma, Performans ve Güvenlik / Öngörülebilirliktir - [bir sonraki yoruma bakın]
Steven A. Lowe

1
[devam] Versiyonlama sadece alt sınıflar olmadığında geçerlidir, duh, ama bu argümanı her sınıfa genişletin ve aniden hiçbir mirasınız yok ve tahmin edin, dil artık nesne yönelimli değil (sadece nesneye dayalı)! [sonrakine bak]
Steven A. Lowe

3
performans örneği bir şakadır: sanal bir yöntem çağrısını optimize etmek; Mühürlü bir sınıf neden alt sınıflara ayrılamayacağından neden sanal bir yönteme sahip olur? Son olarak, Güvenlik / Öngörülebilirlik argümanı açıkça yorucu: 'bunu kullanamazsınız, böylece güvenli / öngörülebilir.' LOL!
Steven A. Lowe

16
@Steven A. Lowe - Bence Jeffrey Richter'in biraz dolambaçlı bir şekilde söylemeye çalıştığı şey, sınıfınızı mühürsüz bırakırsanız, türetilmiş sınıfların nasıl kullanabileceğini / kullanacağını düşünmeniz gerektiğidir. Bunu yapmak için zaman veya eğim varsa, daha sonra mühürleyin, çünkü gelecekte başkalarının kodunda değişikliklere neden olma olasılığı daha düşüktür. Bu hiç saçma değil, sağduyu iyi.
Greg Beech

6
Mühürlü bir sınıfın, onu bildiren bir sınıftan türetilebileceği için sanal bir yöntemi olabilir. Daha sonra, mühürlü soyundan gelen sınıfın bir değişkenini bildirdiğinizde ve bu yöntemi çağırdığınızda, derleyici bilinen uygulamaya doğrudan çağrı yapabilir, çünkü bunun bilinenlerden farklı olabileceğini bilmez. o sınıf için derleme zamanında seçilebilir. Mühürlü / mühürlü için, bu farklı bir tartışmadır ve sınıfları varsayılan olarak mühürlemenin nedenlerine katılıyorum.
Lasse V. Karlsen

143

Cevap hayırdır, mühürlü sınıflar mühürlü olmayanlardan daha iyi performans göstermez.

Sorun, callvs callvirtIL op kodlarına kadar geliyor. Calldaha hızlıdır callvirtve callvirtesas olarak nesnenin alt sınıfta olup olmadığını bilmediğinizde kullanılır. İnsanlar bir sınıf mühür, tüm op kodları değişecektir farz Yani calvirtsiçincalls ve daha hızlı olacaktır.

Maalesef callvirtnull referansları kontrol etmek gibi yararlı kılan başka şeyler de var. Bu, bir sınıf mühürlenmiş olsa bile, başvurunun yine de boş olabileceği ve dolayısıylacallvirt gerekli . Bunun üstesinden gelebilirsiniz (sınıfı mühürlemeye gerek kalmadan), ama biraz anlamsız hale gelir.

Yapıların kullanımı call alt sınıflara ayrılamadıkları ve hiçbir zaman boş olmadıkları için .

Daha fazla bilgi için bu soruya bakın:

Çağrı ve callvirt


5
AFAIK, callkullanılan koşullar şunlardır: durumda new T().Method(), structyöntemler için, yöntemlere sanal olmayan çağrılar virtual(örneğin base.Virtual()) veya staticyöntemler için. Başka her yerde kullanır callvirt.
porins

1
Uhh ... Bunun eski olduğunu anlıyorum, ama bu doğru değil ... mühürlü sınıflarla büyük kazanç, JIT Optimizer'ın çağrıyı sıralayabildiği zamandır ... bu durumda, mühürlü sınıf büyük bir kazanç olabilir .
Brian Kennedy

5
Bu cevap neden yanlış. Mono changelog'dan: "Mühürlü sınıflar ve yöntemler için sanallaştırma optimizasyonu, IronPython 2.0 pystone performansını% 4 iyileştirme. Diğer programlar da benzer iyileşme bekleyebilir [Rodrigo].". Mühürlü sınıflar performansı artırabilir, ancak her zaman olduğu gibi duruma bağlıdır.
Smilediver

1
@Smilediver Performansı artırabilir, ancak yalnızca kötü bir JIT'iniz varsa (günümüzde .NET JIT'lerin ne kadar iyi olduğu hakkında hiçbir fikriniz yoktur - bu konuda oldukça kötüdür). Örneğin Hotspot, sanal çağrıları satır içine alır ve gerekirse daha sonra deoptimize eder - bu nedenle ek yükü, yalnızca sınıfı gerçekten alt sınıfa ayırırsanız (ve hatta zorunlu olarak değil) ödersiniz.
Voo

1
-1 JIT mutlaka aynı IL opodları için aynı makine kodunu üretmek zorunda değildir. Boş kontrol ve sanal çağrı, callvirt'in dikey ve ayrı adımlarıdır. Sızdırmaz tiplerde JIT derleyicisi hala callvirt'in bir kısmını optimize edebilir. Aynı şey, JIT derleyicisi bir başvurunun boş olmayacağını garanti ettiğinde de geçerlidir.
rightfold

29

Güncelleştirme: .NET Core 2.0 ve .NET Desktop 4.7.1'den itibaren CLR artık sanallaştırmayı destekliyor. Mühürlü sınıflarda yöntem alabilir ve sanal çağrıları doğrudan çağrılarla değiştirebilir ve bunu yapmanın güvenli olduğunu anlayabiliyorsa, mühürlü olmayan sınıflar için de yapabilir.

Böyle bir durumda (CLR'nin başka türlü devredışılaşmanın güvenli olduğunu tespit edemediği kapalı bir sınıf), kapalı bir sınıf aslında bir tür performans avantajı sunmalıdır.

Dedi ki, sürece endişelenmeye değer olacağını düşünmezdim zaten kodu profilli ve milyonlarca kez denilen özellikle sıcak bir yolda olduğunu tespit düşünmezdim, ya da böyle bir şey:

https://blogs.msdn.microsoft.com/dotnet/2017/06/29/performance-improvements-in-ryujit-in-net-core-and-net-framework/


Orijinal Yanıt:

Aşağıdaki test programını yaptım ve daha sonra hangi MSIL kodunun yayıldığını görmek için Reflektör kullanarak ayrıştırdım.

public class NormalClass {
    public void WriteIt(string x) {
        Console.WriteLine("NormalClass");
        Console.WriteLine(x);
    }
}

public sealed class SealedClass {
    public void WriteIt(string x) {
        Console.WriteLine("SealedClass");
        Console.WriteLine(x);
    }
}

public static void CallNormal() {
    var n = new NormalClass();
    n.WriteIt("a string");
}

public static void CallSealed() {
    var n = new SealedClass();
    n.WriteIt("a string");
}

Her durumda, C # derleyicisi (Sürüm oluşturma yapılandırmasında Visual studio 2010) özdeş MSIL yayar:

L_0000: newobj instance void <NormalClass or SealedClass>::.ctor()
L_0005: stloc.0 
L_0006: ldloc.0 
L_0007: ldstr "a string"
L_000c: callvirt instance void <NormalClass or SealedClass>::WriteIt(string)
L_0011: ret 

İnsanların mühürlü olduklarının performans faydaları sağladığını söyleyen sık kullanılan neden, derleyicinin sınıfın geçersiz kılındığını ve dolayısıyla callbunun yerine kullanabileceğini bilmesidir .callvirt yukarıda kanıtlanmış gibi vb virtuals, denetlemek zorunda değildir gibi bu değil doğru.

Bir sonraki düşüncem, MSIL aynı olsa da, belki de JIT derleyicisinin kapalı sınıflara farklı davranmasıydı?

Visual studio hata ayıklayıcı altında bir sürüm derlemesi çalıştırdım ve ayrıştırılmış x86 çıktısını gördüm. Her iki durumda da, x86 kodu, sınıf adları ve işlev belleği adresleri (tabii ki farklı olması gerekir) dışında aynıydı. İşte burada

//            var n = new NormalClass();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  cmp         dword ptr ds:[00585314h],0 
0000000d  je          00000014 
0000000f  call        70032C33 
00000014  xor         edx,edx 
00000016  mov         dword ptr [ebp-4],edx 
00000019  mov         ecx,588230h 
0000001e  call        FFEEEBC0 
00000023  mov         dword ptr [ebp-8],eax 
00000026  mov         ecx,dword ptr [ebp-8] 
00000029  call        dword ptr ds:[00588260h] 
0000002f  mov         eax,dword ptr [ebp-8] 
00000032  mov         dword ptr [ebp-4],eax 
//            n.WriteIt("a string");
00000035  mov         edx,dword ptr ds:[033220DCh] 
0000003b  mov         ecx,dword ptr [ebp-4] 
0000003e  cmp         dword ptr [ecx],ecx 
00000040  call        dword ptr ds:[0058827Ch] 
//        }
00000046  nop 
00000047  mov         esp,ebp 
00000049  pop         ebp 
0000004a  ret 

Sonra belki de hata ayıklayıcı altında çalışan daha az agresif optimizasyon yapmak neden olduğunu düşündüm?

Daha sonra herhangi bir hata ayıklama ortamı dışında bağımsız bir sürüm derleme yürütülebilir çalıştırdı ve program tamamlandıktan sonra kırmak için WinDBG + SOS kullandım ve JIT derlenmiş x86 kodunun ayrılması görüntülemek.

Aşağıdaki koddan da görebileceğiniz gibi, hata ayıklayıcının dışında çalışırken JIT derleyicisi daha agresiftir ve WriteItyöntemi doğrudan arayanın içine yerleştirmiştir. Ancak en önemli şey, mühürlü veya mühürlü olmayan bir sınıfı çağırırken aynı olmasıydı. Mühürlü veya mühürlü olmayan bir sınıf arasında hiçbir fark yoktur.

İşte normal bir sınıfı çağırırken:

Normal JIT generated code
Begin 003c00b0, size 39
003c00b0 55              push    ebp
003c00b1 8bec            mov     ebp,esp
003c00b3 b994391800      mov     ecx,183994h (MT: ScratchConsoleApplicationFX4.NormalClass)
003c00b8 e8631fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c00bd e80e70106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00c2 8bc8            mov     ecx,eax
003c00c4 8b1530203003    mov     edx,dword ptr ds:[3302030h] ("NormalClass")
003c00ca 8b01            mov     eax,dword ptr [ecx]
003c00cc 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00cf ff5010          call    dword ptr [eax+10h]
003c00d2 e8f96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c00d7 8bc8            mov     ecx,eax
003c00d9 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c00df 8b01            mov     eax,dword ptr [ecx]
003c00e1 8b403c          mov     eax,dword ptr [eax+3Ch]
003c00e4 ff5010          call    dword ptr [eax+10h]
003c00e7 5d              pop     ebp
003c00e8 c3              ret

Mühürlü bir sınıfla:

Normal JIT generated code
Begin 003c0100, size 39
003c0100 55              push    ebp
003c0101 8bec            mov     ebp,esp
003c0103 b90c3a1800      mov     ecx,183A0Ch (MT: ScratchConsoleApplicationFX4.SealedClass)
003c0108 e8131fdbff      call    00172020 (JitHelp: CORINFO_HELP_NEWSFAST)
003c010d e8be6f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0112 8bc8            mov     ecx,eax
003c0114 8b1538203003    mov     edx,dword ptr ds:[3302038h] ("SealedClass")
003c011a 8b01            mov     eax,dword ptr [ecx]
003c011c 8b403c          mov     eax,dword ptr [eax+3Ch]
003c011f ff5010          call    dword ptr [eax+10h]
003c0122 e8a96f106f      call    mscorlib_ni+0x2570d0 (6f4c70d0) (System.Console.get_Out(), mdToken: 060008fd)
003c0127 8bc8            mov     ecx,eax
003c0129 8b1534203003    mov     edx,dword ptr ds:[3302034h] ("a string")
003c012f 8b01            mov     eax,dword ptr [ecx]
003c0131 8b403c          mov     eax,dword ptr [eax+3Ch]
003c0134 ff5010          call    dword ptr [eax+10h]
003c0137 5d              pop     ebp
003c0138 c3              ret

Bana göre bu, mühürlü ve mühürlü olmayan sınıflarda çağrı yapma yöntemleri arasında herhangi bir performans artışı sağlanamayacağına dair sağlam bir kanıt sağlar ... Sanırım şimdi mutluyum :-)


Bu cevabı, diğerini kopyalamak yerine iki kez göndermenizin bir nedeni var mı? Bu benim alanım olmasa da bana geçerli kopyaları gibi görünüyorlar.
Flekso

1
Satırları önlemek ve hangi çağrı işleminin kullanılacağını görmek için yöntemler daha uzun olsaydı (32 bayttan fazla IL kodu)? Satır içi ise, aramayı göremezsiniz, bu yüzden etkileri değerlendiremezsiniz.
ygoe

Kafam karıştı: callvirtBu yöntemler başlamak için sanal olmadığında neden var ?
ucube

@freakish Bunu nerede gördüğümü hatırlayamıyorum, ancak CLR'nin callvirtmühürlü yöntemler için kullanıldığını okudum çünkü yöntem çağrısını başlatmadan önce nesneyi null kontrol etmesi gerekiyor ve bunu hesaba kattığınızda, sadece kullanın callvirt. Kaldırmak callvirtve doğrudan atlamak için, ((string)null).methodCall()C ++ 'ın yaptığı gibi izin vermek için C #' ı değiştirmeleri veya nesnenin boş olmadığını (yapabildikleri, ancak rahatsız etmedikleri) statik olarak kanıtlamaları gerekir.
Orion Edwards

1
Makine kodu seviyesine inme çabasına giden sahne , ancak 'bu, herhangi bir performans iyileştirmesi yapılamayacağının sağlam bir kanıtıdır' gibi ifadeler yaparken çok dikkatli olun . Gösterdiğiniz şey, belirli bir senaryo için yerel çıktıda fark olmamasıdır. Bu bir veri noktasıdır ve tüm senaryolar için genel olduğunu varsayamaz. Yeni başlayanlar için, sınıflarınız herhangi bir sanal yöntem tanımlamaz, bu nedenle sanal aramalara hiç gerek yoktur.
Matt Craig

24

Bildiğim gibi, performans avantajı garantisi yok. Ancak, belirli bir koşul altında performans cezasını azaltma şansı vardır mühürlü yöntemle . (mühürlü sınıf mühürlü tüm yöntemleri yapar.)

Ancak derleyici uygulama ve yürütme ortamına bağlıdır.


ayrıntılar

Modern CPU'ların çoğu performansı artırmak için uzun boru hattı yapısı kullanır. CPU bellekten inanılmaz derecede hızlı olduğu için, boru hattını hızlandırmak için CPU'nun bellekten kodu önceden alması gerekir. Kod uygun zamanda hazır değilse, boru hatları boşta kalır.

Bu 'ön getirme' optimizasyonunu bozan dinamik dağıtım adı verilen büyük bir engel var . Bunu sadece koşullu bir dallanma olarak anlayabilirsiniz.

// Value of `v` is unknown,
// and can be resolved only at runtime.
// CPU cannot know which code to prefetch.
// Therefore, just prefetch any one of a() or b().
// This is *speculative execution*.
int v = random();
if (v==1) a();
else b();

Bu durumda CPU, bir sonraki kodu çalıştırmak için önceden getiremez, çünkü koşul çözülene kadar bir sonraki kod konumu bilinmemektedir. Bu tehlike yaratıyor boru hattının kullanılmamasına neden olur. Ve rölantide performans cezası düzenli olarak çok büyük.

Yöntemin geçersiz kılınması durumunda da benzer şeyler olur. Derleyici, geçerli yöntem çağrısı için uygun yöntem geçersiz kılmayı belirleyebilir, ancak bazen imkansızdır. Bu durumda, uygun yöntem sadece çalışma zamanında belirlenebilir. Bu aynı zamanda dinamik bir gönderim durumudur ve dinamik olarak yazılan dillerin ana nedeni genellikle statik olarak yazılan dillerden daha yavaştır.

Bazı CPU'lar (en son Intel'in x86 yongaları dahil) spekülatif yürütme adı verilen tekniği kullanıyor , durumdayken bile boru hattını kullanmak için . Yürütme yolundan birini önceden getirmeniz yeterlidir. Ancak bu tekniğin isabet oranı o kadar yüksek değil. Spekülasyon başarısızlığı boru hattı durmasına neden olur ve bu da büyük performans cezası verir. (bu tamamen CPU uygulamasıdır. Bazı mobil CPU, enerji tasarrufu için bu tür bir optimizasyon olmadığı bilinir)

Temel olarak, C # statik olarak derlenmiş bir dildir. Ama her zaman değil. Kesin koşulu bilmiyorum ve bu tamamen derleyici uygulamasına bağlı. Bazı derleyiciler, yöntem olarak işaretlenirse yöntemin geçersiz kılınmasını engelleyerek dinamik gönderme olasılığını ortadan kaldırabilir sealed. Aptal derleyiciler olmayabilir. Bu performans avantajı sealed.


Bu cevap ( Sıralanmış bir diziyi işlemek neden sıralanmamış bir diziden daha hızlıdır? ), Şube tahminini çok daha iyi açıklamaktadır.


1
Pentium sınıfı CPU'lar dolaylı gönderiyi doğrudan alır. Bazen işlev işaretçisi yeniden yönlendirmesi bu nedenle (tahmin edilemez) olandan daha hızlıdır.
Joshua

2
Sanal olmayan veya mühürlü fonksiyonun bir avantajı, daha fazla durumda satır içi olabilmeleridir.
CodesInChaos

4

<Konu dışı-rant>

Mühürlü sınıflardan nefret ediyorum . Performans faydaları şaşırtıcı olsa bile (ki şüpheliyim), miras yoluyla yeniden kullanılmasını engelleyerek nesne yönelimli modeli yok ederler. Örneğin, Thread sınıfı mühürlenir. Ben bir iş parçacığı mümkün olduğunca verimli olmasını isteyebilirsiniz görebiliyorum da, aynı zamanda iş parçacığı alt sınıf mümkün olmanın büyük faydaları olurdu senaryoları hayal edebiliyorum. Sınıf yazarları, sınıflarınızı "performans" nedenleriyle mühürlemeniz gerekiyorsa , lütfen en azından bir arayüz sağlayın, böylece unuttuğunuz bir özelliğe ihtiyaç duyduğumuz her yere sarmak ve değiştirmek zorunda kalmayız.

Örnek: SafeThread Konu mühürlü ve hiçbir IThread arayüzü yoktur çünkü Konu sınıfını sarmak zorunda; SafeThread, Thread sınıfında tamamen eksik olan, iş parçacıklarındaki işlenmeyen istisnaları otomatik olarak yakalar. [ve hayır, işlenmeyen istisna olayları ikincil evrelerde işlenmeyen istisnaları almaz ].

</ Konu dışı-rant>


35
Performans nedenleriyle derslerimi mühürlemiyorum. Onları tasarım nedenleriyle mühürlüyorum. Kalıtım için tasarım yapmak zordur ve bu çaba çoğu zaman boşa gidecektir. Yine de arayüzler sağlamaya tamamen katılıyorum - bu mühürlü sınıflar için çok daha üstün bir çözüm.
Jon Skeet

6
Kapsülleme genellikle kalıtımdan daha iyi bir çözümdür. Özel iş parçacığı örneğinizi almak için, iş parçacığı özel durumlarını yakalamak Liskov İkame İlkesi'ni ihlal eder, çünkü Thread sınıfının belgelenmiş davranışını değiştirmişsinizdir, bu nedenle ondan türetebilseniz bile, SafeThread'i her yerde kullanabileceğinizi söylemek mantıklı olmaz Thread kullanabilirsiniz. Bu durumda, iş parçacığını yapabileceğiniz farklı belgelenmiş davranışa sahip başka bir sınıfa kapsüllemek daha iyi olur. Bazen işler kendi iyiliğiniz için mühürlenir.
Greg Beech

1
@ [Greg Beech]: fikir, gerçek değil - tasarımında iğrenç bir gözetimi düzeltmek için Thread'den miras alabilmek kötü bir şey DEĞİL ;-) Ve sanırım LSP'yi abartıyorsunuz - olası özellik q (x) Bu dava 'işlenmeyen bir istisna programı yok eder' bu bir "arzu edilen özellik" değil :-)
Steven A. Lowe

1
Hayır, ancak diğer geliştiricilerin eşyalarımı mühürlemeyerek veya köşe vakalarına izin vererek kötüye kullanmalarını sağladığım crappy kodundan payımı aldım. Günümüzde kodumun çoğu, iddialar ve sözleşmeyle ilgili diğer şeyler. Ve bunu sadece *** 'da acı çekmek için yaptığım konusunda oldukça açıkım.
Turing

2
Burada konu dışı sıralamalar yaptığımız için, tıpkı mühürlü sınıfları tiksinmiş gibi , yutulmuş istisnaları tiksindiriyorum . Bir şey titsup gittiğinde daha kötü bir şey yok ama program devam ediyor. JavaScript benim favorim. Bazı kodlarda değişiklik yaparsınız ve aniden bir düğmeye tıklamak kesinlikle hiçbir şey yapmaz. Harika! ASP.NET ve UpdatePanel başka bir tanesidir; Cidden, eğer düğme işleyicim atarsa ​​Büyük bir anlaşma ve CRASH'a ihtiyacı var, böylece düzeltilmesi gereken bir şey olduğunu biliyorum! Hiçbir şey yapmayan bir düğme, kilitlenme ekranını getiren bir düğmeden daha işe yaramaz!
Roman Starkov

4

Bir sınıfı işaretlemenin sealedhiçbir performans etkisi olmamalıdır.

cscBir callvirtopcode yerine bir opcode yayınlamak zorunda kalabileceğiniz durumlar vardır call. Ancak, bu vakaların nadir olduğu görülmektedir.

Ve bana öyle geliyor ki, JIT , sınıfın herhangi bir alt sınıfa sahip olmadığını (henüz) bilmiyorsa, callvirtbunun için aynı sanal olmayan işlev çağrısını yayınlayabilmelidir call. Yöntemin yalnızca bir uygulaması varsa, adresini bir vtable'dan yüklemenin anlamı yoktur; yalnızca bir uygulamayı doğrudan arayın. Bu nedenle, JIT işlevi satır içine alabilir.

JIT kısmında biraz kumar var, çünkü bir alt sınıf daha sonra yüklenirse, JIT bu makine kodunu atmak ve gerçek bir sanal çağrı yayarak kodu tekrar derlemek zorunda kalacak. Tahminimce bu uygulamada sık sık gerçekleşmiyor.

(Ve evet, VM tasarımcıları bu küçük performans kazançlarını gerçekten agresif bir şekilde takip ediyorlar.)


3

Sızdırmaz sınıfları gereken bir performans artışı sağlamaktadır. Kapalı bir sınıf türetilemediğinden, sanal üyeler sanal olmayan üyelere dönüştürülebilir.

Tabii ki, gerçekten küçük kazançlardan bahsediyoruz. Profil oluşturma bir sorun olduğunu göstermedikçe, bir performansı iyileştirmek için bir sınıfı kapalı olarak işaretlemezdim.


Onlar gerektiği , ancak onlar değil anlaşılmaktadır. CLR boş olmayan başvuru türleri kavramını olsaydı derleyici yayarlar olabilir, sonra kapalı bir sınıf gerçekten iyi olurdu callyerine callvirtben çok fazla sayıda başka nedenlerden dolayı boş olmayan referans tiplerini isteriz ... ... iç çekiş :-(
Orion Edwards

3

"Mühürlü" sınıfları normal durum olarak görüyorum ve DAİMA "mühürlü" anahtar kelimeyi atlamak için bir nedenim var.

Benim için en önemli nedenler:

a) Daha iyi derleme zamanı kontrolleri (uygulanmayan arayüzlere döküm sadece çalışma zamanında değil, derleme zamanında algılanacaktır)

ve başlıca nedeni:

b) Derslerimin kötüye kullanılması bu şekilde mümkün değildir

Keşke Microsoft "mühürlü" değil, "mühürlü" standart yapmış olsaydı.


Ben "ihmal" (dışarıda bırakmak) (yayar) (üretmek için) olması gerektiğine inanıyorum?
user2864740

2

@Vaibhav, performansı ölçmek için ne tür testler yaptınız?

Sanırım biri Rotor kullanmak ve CLI'ye girmek ve mühürlü bir sınıfın performansı nasıl artıracağını anlamak zorunda kalacaktı.

SSCLI (Rotor)
SSCLI: Paylaşılan Kaynak Ortak Dil Altyapısı

Ortak Dil Altyapısı (CLI), .NET Framework'ün çekirdeğini tanımlayan ECMA standardıdır. Rotor olarak da bilinen Paylaşılan Kaynak CLI (SSCLI), Microsoft'un .NET mimarisinin merkezindeki ECMA CLI ve ECMA C # dil belirtiminin çalışan bir uygulamasına yönelik kaynak kodunun sıkıştırılmış bir arşividir.


Test, kukla çalışma yapan bazı yöntemlerle (çoğunlukla dize manipülasyonu) bir sınıf hiyerarşisi oluşturmayı içeriyordu. Bu yöntemlerden bazıları sanaldı. Orada ve burada birbirlerini çağırıyorlardı. Sonra bu yöntemleri 100, 10000 ve 100000 kez çağırmak ... ve geçen süreyi ölçmek. Daha sonra sınıfları kapalı olarak işaretledikten sonra bunları çalıştırın. ve tekrar ölçüyoruz. Aralarında fark yok.
Vaibhav

2

mühürlü sınıflar en azından biraz daha hızlı olacaktır, ancak bazen daha hızlı olabilir ... JIT Optimizer aksi takdirde sanal çağrılar olacak olan çağrıları satır içi yapabiliyorsa. Bu nedenle, sıralı olacak kadar küçük yöntemler denildiğinde, kesinlikle sınıfı mühürlemeyi düşünün.

Bununla birlikte, bir sınıfı mühürlemenin en iyi nedeni, "Bunu miras alınacak şekilde tasarlamadım, bu yüzden öyle tasarlandığını varsayarak yanmanıza izin vermeyeceğim ve gitmiyorum. kendimi bir uygulamaya kilitlenerek yakmak çünkü ondan türetmene izin veriyorum. "

Burada bazılarının mühürlü sınıflardan nefret ettiklerini söyledikleri için biliyorum, çünkü herhangi bir şeyden türetme fırsatı istiyorlar ... ama bu en sürdürülebilir seçenek değil ... çünkü bir sınıfı türetmeye maruz bırakmak sizi herkesi açığa çıkarmamaktan çok daha fazla kilitliyor söyledi. "Özel üyeleri olan sınıflardan nefret ediyorum ... Sık sık sınıfın istediğim şeyi yapmasını sağlayamıyorum çünkü erişimim yok." Kapsülleme önemlidir ... sızdırmazlık bir kapsülleme biçimidir.


C # derleyicisi callvirt, mühürlü sınıflardaki yöntemler için (sanal yöntem çağrısı) kullanmaya devam eder , çünkü yine de üzerlerinde boş nesne denetimi yapmak zorundadır. Satır içi ile ilgili olarak, CLR JIT satır içi sanal yöntem hem mühürlü hem de mühürlü olmayan sınıfları arayabilir (ve yapar) ... yani evet. Performans meselesi bir efsanedir.
Orion Edwards

1

Onları gerçekten görmek için JIT-Compiled kodunu (sonuncusu) analiz etmeniz gerekir .

C # Kodu

public sealed class Sealed
{
    public string Message { get; set; }
    public void DoStuff() { }
}
public class Derived : Base
{
    public sealed override void DoStuff() { }
}
public class Base
{
    public string Message { get; set; }
    public virtual void DoStuff() { }
}
static void Main()
{
    Sealed sealedClass = new Sealed();
    sealedClass.DoStuff();
    Derived derivedClass = new Derived();
    derivedClass.DoStuff();
    Base BaseClass = new Base();
    BaseClass.DoStuff();
}

MIL Kodu

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       41 (0x29)
  .maxstack  8
  IL_0000:  newobj     instance void ConsoleApp1.Program/Sealed::.ctor()
  IL_0005:  callvirt   instance void ConsoleApp1.Program/Sealed::DoStuff()
  IL_000a:  newobj     instance void ConsoleApp1.Program/Derived::.ctor()
  IL_000f:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0014:  newobj     instance void ConsoleApp1.Program/Base::.ctor()
  IL_0019:  callvirt   instance void ConsoleApp1.Program/Base::DoStuff()
  IL_0028:  ret
} // end of method Program::Main

JIT- Derlenmiş Kod

--- C:\Users\Ivan Porta\source\repos\ConsoleApp1\Program.cs --------------------
        {
0066084A  in          al,dx  
0066084B  push        edi  
0066084C  push        esi  
0066084D  push        ebx  
0066084E  sub         esp,4Ch  
00660851  lea         edi,[ebp-58h]  
00660854  mov         ecx,13h  
00660859  xor         eax,eax  
0066085B  rep stos    dword ptr es:[edi]  
0066085D  cmp         dword ptr ds:[5842F0h],0  
00660864  je          0066086B  
00660866  call        744CFAD0  
0066086B  xor         edx,edx  
0066086D  mov         dword ptr [ebp-3Ch],edx  
00660870  xor         edx,edx  
00660872  mov         dword ptr [ebp-48h],edx  
00660875  xor         edx,edx  
00660877  mov         dword ptr [ebp-44h],edx  
0066087A  xor         edx,edx  
0066087C  mov         dword ptr [ebp-40h],edx  
0066087F  nop  
            Sealed sealedClass = new Sealed();
00660880  mov         ecx,584E1Ch  
00660885  call        005730F4  
0066088A  mov         dword ptr [ebp-4Ch],eax  
0066088D  mov         ecx,dword ptr [ebp-4Ch]  
00660890  call        00660468  
00660895  mov         eax,dword ptr [ebp-4Ch]  
00660898  mov         dword ptr [ebp-3Ch],eax  
            sealedClass.DoStuff();
0066089B  mov         ecx,dword ptr [ebp-3Ch]  
0066089E  cmp         dword ptr [ecx],ecx  
006608A0  call        00660460  
006608A5  nop  
            Derived derivedClass = new Derived();
006608A6  mov         ecx,584F3Ch  
006608AB  call        005730F4  
006608B0  mov         dword ptr [ebp-50h],eax  
006608B3  mov         ecx,dword ptr [ebp-50h]  
006608B6  call        006604A8  
006608BB  mov         eax,dword ptr [ebp-50h]  
006608BE  mov         dword ptr [ebp-40h],eax  
            derivedClass.DoStuff();
006608C1  mov         ecx,dword ptr [ebp-40h]  
006608C4  mov         eax,dword ptr [ecx]  
006608C6  mov         eax,dword ptr [eax+28h]  
006608C9  call        dword ptr [eax+10h]  
006608CC  nop  
            Base BaseClass = new Base();
006608CD  mov         ecx,584EC0h  
006608D2  call        005730F4  
006608D7  mov         dword ptr [ebp-54h],eax  
006608DA  mov         ecx,dword ptr [ebp-54h]  
006608DD  call        00660490  
006608E2  mov         eax,dword ptr [ebp-54h]  
006608E5  mov         dword ptr [ebp-44h],eax  
            BaseClass.DoStuff();
006608E8  mov         ecx,dword ptr [ebp-44h]  
006608EB  mov         eax,dword ptr [ecx]  
006608ED  mov         eax,dword ptr [eax+28h]  
006608F0  call        dword ptr [eax+10h]  
006608F3  nop  
        }
0066091A  nop  
0066091B  lea         esp,[ebp-0Ch]  
0066091E  pop         ebx  
0066091F  pop         esi  
00660920  pop         edi  
00660921  pop         ebp  

00660922  ret  

Nesnelerin oluşturulması aynı olmakla birlikte, kapalı ve türetilmiş / taban sınıfının yöntemlerini çağırmak için yürütülen talimat biraz farklıdır. Verileri yazmaçlara veya RAM'e (mov komutu) taşıdıktan sonra, mühürlü yöntemin çağrılması, dword ptr [ecx], ecx (cmp komutu) arasında bir karşılaştırma yapın ve sonra türetilmiş / temel sınıf doğrudan yöntemi yürütürken yöntemi çağırın. .

Torbj¨orn Granlund tarafından yazılan rapora göre, AMD ve Intel x86 işlemciler için yönerge gecikmeleri ve işlem hacmi , Intel Pentium 4'te aşağıdaki talimatın hızı:

  • mov : gecikme olarak 1 döngüye sahiptir ve işlemci bu tür döngü başına 2,5 komutu sürdürebilir
  • cmp : gecikme olarak 1 döngüye sahiptir ve işlemci bu tür döngü başına 2 komutu sürdürebilir

Bağlantı : https://gmplib.org/~tege/x86-timing.pdf

Bu, ideal olarak , kapalı bir yöntemi çağırmak için gereken sürenin 2 döngü olduğu, türetilmiş veya baz sınıfı bir yöntemi çağırmak için gereken sürenin 3 döngü olduğu anlamına gelir.

Derleyicilerin optimizasyonu, kapalı ve kapalı olmayan bir sınıfın performansları arasındaki farkı o kadar düşük hale getirdi ki, işlemci çevrelerinden bahsediyoruz ve bu nedenle uygulamaların çoğu için önemsiz.


-10

Bu kodu çalıştırın ve kapalı sınıfların 2 kat daha hızlı olduğunu göreceksiniz:

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new SealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("Sealed class : {0}", watch.Elapsed.ToString());

        watch.Start();
        for (int i = 0; i < 10000000; i++)
        {
            new NonSealedClass().GetName();
        }
        watch.Stop();
        Console.WriteLine("NonSealed class : {0}", watch.Elapsed.ToString());

        Console.ReadKey();
    }
}

sealed class SealedClass
{
    public string GetName()
    {
        return "SealedClass";
    }
}

class NonSealedClass
{
    public string GetName()
    {
        return "NonSealedClass";
    }
}

çıktı: Mühürlü sınıf: 00: 00: 00.1897568 Mühürlü sınıf: 00: 00: 00.3826678


9
Bununla ilgili birkaç sorun var. İlk olarak, birinci ve ikinci test arasında kronometreyi sıfırlamıyorsunuz. İkincisi, yöntemi arama şekliniz, tüm op kodlarının callvirt değil çağrılacağı anlamına gelir, böylece tür önemli değildir.
Cameron MacFarland

1
RuslanG, İlk testi yaptıktan sonra watch.Reset () yöntemini çağırmayı unuttunuz. o_O:]

Evet, yukarıdaki kodda hatalar var. Sonra tekrar, kim asla kod hatalarını tanıtmak için kendini övünebilir? Ancak bu cevabın diğerlerinden daha önemli bir şeyi vardır: Söz konusu etkiyi ölçmeye çalışır. Ve bu cevap aynı zamanda incelemeniz ve [kitle] inişiniz için de kodu paylaşıyor (toplu inişlerin neden gerekli olduğunu merak ediyorum?). Diğer cevapların aksine. Bu benim görüşüme göre saygıyı hak ediyor ... Ayrıca lütfen kullanıcının acemi olduğunu, bu yüzden çok hoş bir yaklaşım olmadığını unutmayın. Bazı basit düzeltmeler ve bu kod hepimiz için yararlı olur.
Cesaretin

@RolandPihlakas'a katılmıyorum. Dediğiniz gibi, "kim asla kod hatalar tanıtmak için kendini övünmek". Cevap -> Hayır, yok. Ama mesele bu değil. Mesele şu ki, yeni bir programcıyı yanlış yönlendirebilecek yanlış bir bilgidir. Birçok kişi kronometrenin sıfırlanmadığını kolayca özleyebilir. Karşılaştırma bilgilerinin doğru olduğuna inanabilirler. Bu daha zararlı değil mi? Hızlı bir şekilde cevap arayan biri bu yorumları bile okumaz, bunun yerine bir cevap görür ve buna inanabilir.
Emran Hussain
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.