As aulas seladas realmente oferecem benefícios de desempenho?

140

Encontrei muitas dicas de otimização que dizem que você deve marcar suas aulas como seladas para obter benefícios extras de desempenho.

Fiz alguns testes para verificar o diferencial de desempenho e não encontrei nenhum. Estou fazendo algo errado? Estou perdendo o caso em que as aulas seladas fornecerão melhores resultados?

Alguém já fez testes e viu a diferença?

Ajude-me a aprender :)

Vaibhav
fonte
9
Eu não acho que as aulas seladas foram destinadas a dar um aumento no desempenho. O fato de que eles fazem pode ser incidental. Além disso, analise seu aplicativo depois de refatorá-lo para usar classes seladas e determine se valeu a pena. Bloquear sua extensibilidade para fazer uma micro-otimização desnecessária custará a longo prazo. É claro que, se você criar um perfil, e permitir atingir seus valores de referência de perf (em vez de perf por uma questão de perf), poderá tomar uma decisão em equipe se valer o dinheiro gasto. Se você ter selado aulas por razões não-perf, em seguida, manter em :)
Merlyn Morgan-Graham
1
Você já tentou com reflexão? Li algures que instanciar pela reflexão é mais rápido com aulas selados
onof
Que eu saiba, não há. O Sealed existe por um motivo diferente - para bloquear a extensibilidade, que pode ser útil / necessária em muitos casos. A otimização de desempenho não era uma meta aqui.
TomTom
... mas se você pensar no compilador: se sua classe estiver selada, você saberá o endereço de um método que você chama na sua classe no momento da compilação. Se sua classe não for selada, você deverá resolver o método em tempo de execução, pois pode ser necessário substituir uma substituição. Certamente será insignificante, mas pude ver que haveria alguma diferença.
Dan Puzey
Sim, mas isso não se traduz em benefícios reais, como o OP apontou. As diferenças arquitetônicas são / podem ser muito mais relevantes.
TomTom

Respostas:

60

Às vezes, o JITter usa chamadas não virtuais para métodos em classes seladas, pois não há como elas serem estendidas.

Existem regras complexas em relação ao tipo de chamada, virtual / não virtual, e eu não as conheço, então não posso descrevê-las para você, mas se você pesquisar classes e métodos virtuais no Google, poderá encontrar alguns artigos sobre o assunto.

Observe que qualquer tipo de benefício de desempenho que você obteria desse nível de otimização deve ser considerado como último recurso; sempre otimize no nível algorítmico antes de otimizar no nível do código.

Aqui está um link mencionando isso: Divagando sobre a palavra-chave selada

Lasse V. Karlsen
fonte
2
o link 'divagante' é interessante, pois soa como uma bondade técnica, mas na verdade é um absurdo. Leia os comentários no artigo para mais informações. Resumo: as três razões apresentadas são controle de versão, desempenho e segurança / previsibilidade - [veja o próximo comentário]
Steven A. Lowe
1
[continuação] O controle de versão só se aplica quando não há subclasses, mas estenda esse argumento para todas as classes e de repente você não tem herança e adivinhe, a linguagem não é mais orientada a objetos (mas apenas baseada em objetos)! [veja a seguir]
Steven A. Lowe
3
[continuação] o exemplo de desempenho é uma piada: otimizando uma chamada de método virtual; por que uma classe selada teria um método virtual em primeiro lugar, pois não pode ser subclassificado? Por fim, o argumento Segurança / Previsibilidade é claramente falso: 'você não pode usá-lo, portanto é seguro / previsível'. RI MUITO!
Steven A. Lowe
16
@ Steven A. Lowe - Acho que o que Jeffrey Richter estava tentando dizer de maneira um pouco indireta é que, se você deixar sua classe sem lacre, precisará pensar em como as classes derivadas podem / irão usá-lo e se você não tem o tempo ou inclinação para fazer isso corretamente, sele-o, pois é menos provável que cause mudanças no código de outras pessoas no futuro. Isso não é um absurdo, é um bom senso comum.
Greg Beech
6
Uma classe selada pode ter um método virtual, pois pode derivar de uma classe que a declara. Quando, posteriormente, você declarar uma variável da classe descendente selada e chamar esse método, o compilador poderá emitir uma chamada direta para a implementação conhecida, pois sabe que não há como isso ser diferente do que é conhecido. tabela em tempo de compilação para essa classe. Quanto a selado / não lacrado, essa é uma discussão diferente, e eu concordo com os motivos para tornar as aulas seladas por padrão.
Lasse V. Karlsen
143

A resposta é não, as classes seladas não têm melhor desempenho do que as não seladas.

A questão se resume aos códigos op callvs callvirtIL. Callé mais rápido que callvirte callvirté usado principalmente quando você não sabe se o objeto foi subclassificado. Portanto, as pessoas assumem que, se você selar uma classe, todos os códigos operacionais mudarão de calvirtspara callse serão mais rápidos.

Infelizmente callvirt, outras coisas que o tornam útil também, como procurar referências nulas. Isso significa que, mesmo que uma classe seja selada, a referência ainda pode ser nula e, portanto, a callvirté necessária. Você pode contornar isso (sem a necessidade de selar a classe), mas isso se torna um pouco inútil.

As estruturas são usadas callporque não podem ser subclassificadas e nunca são nulas.

Veja esta pergunta para mais informações:

Call and callvirt

Cameron MacFarland
fonte
5
AFAIK, as circunstâncias em que callsão usadas são: na situação new T().Method(), para structmétodos, para chamadas não virtuais a virtualmétodos (como base.Virtual()) ou para staticmétodos. Em qualquer outro lugar usa callvirt.
porges
1
Uhh ... eu sei que isso é antigo, mas isso não está certo ... a grande vitória com classes seladas é quando o JIT Optimizer pode atender a chamada ... nesse caso, a classe selada pode ser uma grande vitória .
Brian Kennedy
5
Por que esta resposta está errada? Do changelog da Mono: "Otimização da desvirtualização para classes e métodos selados, melhorando o desempenho do pystone do IronPython 2.0 em 4%. Outros programas podem esperar melhorias semelhantes [Rodrigo].". Classes seladas podem melhorar o desempenho, mas, como sempre, depende da situação.
Smilediver
1
@ Smmiveriver Ele pode melhorar o desempenho, mas apenas se você tiver um JIT ruim (não faz ideia de quão bom é o JIT .NET hoje em dia - costumava ser muito ruim nesse sentido). O ponto de acesso, por exemplo, alinha as chamadas virtuais e a otimiza posteriormente, se necessário - portanto, você paga a sobrecarga adicional apenas se subclassificar a classe (e mesmo assim não necessariamente).
Voo
1
-1 O JIT não precisa necessariamente gerar o mesmo código de máquina para os mesmos opcodes de IL. A verificação nula e a chamada virtual são etapas ortogonais e separadas do callvirt. No caso de tipos selados, o compilador JIT ainda pode otimizar parte do callvirt. O mesmo acontece quando o compilador JIT pode garantir que uma referência não será nula.
#
29

Atualização: A partir do .NET Core 2.0 e do .NET Desktop 4.7.1, o CLR agora oferece suporte à desirtualização. Ele pode usar métodos em classes seladas e substituir chamadas virtuais por chamadas diretas - e também pode fazer isso para classes não seladas, se descobrir que é seguro fazê-lo.

Nesse caso (uma classe selada que, de outra forma, o CLR não poderia detectar como seguro para desindualizar), uma classe selada deve realmente oferecer algum tipo de benefício de desempenho.

Dito isso, eu não acho que valeria a pena se preocupar, a menos que você já tenha perfilado o código e determinado que estava em um caminho particularmente quente sendo chamado milhões de vezes, ou algo assim:

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


Resposta original:

Eu fiz o seguinte programa de teste e o descompilei usando o Reflector para ver qual código MSIL foi emitido.

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");
}

Em todos os casos, o compilador C # (Visual studio 2010 na configuração de compilação do Release) emite MSIL idêntico, que é o seguinte:

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 

O motivo muitas vezes citado de que as pessoas dizem que lacrado oferece benefícios de desempenho é que o compilador sabe que a classe não é substituída e, portanto, pode usá-lo em callvez de callvirtnão precisar verificar virtuals, etc. Como comprovado acima, isso não é verdade.

Meu próximo pensamento foi que, embora o MSIL seja idêntico, talvez o compilador JIT trate as classes seladas de maneira diferente?

Eu executei uma versão compilada no depurador do visual studio e vi a saída x86 descompilada. Nos dois casos, o código x86 era idêntico, com exceção dos nomes de classe e endereços de memória de função (que obviamente devem ser diferentes). Aqui está

//            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 

Eu então pensei que talvez rodar sob o depurador faça com que ele execute uma otimização menos agressiva?

Em seguida, executei uma compilação de versão autônoma executável fora de qualquer ambiente de depuração e usei o WinDBG + SOS para interromper após a conclusão do programa e exibir a desmontagem do código x86 compilado pelo JIT.

Como você pode ver no código abaixo, ao executar fora do depurador, o compilador JIT é mais agressivo e inseriu o WriteItmétodo diretamente no chamador. O ponto crucial, no entanto, é que era idêntico ao chamar uma classe selada vs não selada. Não há diferença alguma entre uma classe selada ou não selada.

Aqui está ao chamar uma classe normal:

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

Vs uma classe selada:

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

Para mim, isso fornece uma prova sólida de que não pode haver nenhuma melhoria de desempenho entre chamar métodos em classes seladas ou não seladas ... Acho que estou feliz agora :-)

Orion Edwards
fonte
Algum motivo pelo qual você postou esta resposta duas vezes em vez de fechar a outra como duplicada? Eles parecem duplicados válidos para mim, embora essa não seja a minha área.
Flexo
1
E se os métodos fossem mais longos (mais de 32 bytes de código IL) para evitar inlining e ver qual operação de chamada seria usada? Se estiver embutido, você não verá a ligação, portanto não poderá julgar os efeitos.
ygoe
Estou confuso: por que existe callvirtquando esses métodos não são virtuais?
freakish
@ freakish Não me lembro de onde vi isso, mas li que o CLR usava callvirtpara métodos selados porque ainda precisa checar o objeto antes de chamar a chamada de método, e depois de levar isso em consideração, você também pode use callvirt. Para remover o callvirte simplesmente pular diretamente, eles precisariam modificar o C # para permitir o ((string)null).methodCall()que o C ++ faz, ou precisariam provar estaticamente que o objeto não era nulo (o que eles poderiam fazer, mas não se incomodaram)
Orion Edwards
1
Adereços para o esforço de descer até o nível do código da máquina, mas tenha muito cuidado ao fazer declarações como 'isso fornece uma prova sólida de que não pode haver nenhuma melhoria no desempenho'. O que você mostrou é que, para um cenário específico, não há diferença na saída nativa. É um ponto de dados e não se pode assumir que generaliza para todos os cenários. Para iniciantes, suas classes não definem nenhum método virtual, portanto, as chamadas virtuais não são necessárias.
Matt Craig
24

Como eu sei, não há garantia de benefício de desempenho. Mas há uma chance de diminuir a penalidade de desempenho sob alguma condição específica com o método selado. (a classe selada faz com que todos os métodos sejam selados.)

Mas cabe ao ambiente de implementação e execução do compilador.


Detalhes

Muitas das CPUs modernas usam estrutura de pipeline longa para aumentar o desempenho. Como a CPU é incrivelmente mais rápida que a memória, a CPU precisa buscar o código da memória para acelerar o pipeline. Se o código não estiver pronto no momento adequado, os pipelines ficarão ociosos.

Há um grande obstáculo chamado despacho dinâmico que interrompe essa otimização de 'pré-busca'. Você pode entender isso apenas como uma ramificação condicional.

// 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();

A CPU não pode buscar previamente o próximo código a ser executado neste caso, porque a próxima posição do código é desconhecida até que a condição seja resolvida. Portanto, isso faz com que o perigo faça com que o pipeline fique ocioso. E a penalidade de desempenho por inatividade é enorme regularmente.

Coisa semelhante acontece no caso de substituição de método. O compilador pode determinar a substituição apropriada do método para a chamada de método atual, mas às vezes é impossível. Nesse caso, o método adequado pode ser determinado apenas em tempo de execução. Este também é um caso de despacho dinâmico e, o principal motivo de linguagens de tipo dinâmico é geralmente mais lento que o de estática.

Algumas CPUs (incluindo os recentes chips x86 da Intel) usam uma técnica chamada execução especulativa para utilizar o pipeline mesmo na situação. Basta pré-buscar um caminho de execução. Mas a taxa de acertos dessa técnica não é tão alta. E a falha na especulação causa um empate no pipeline, o que também gera uma enorme penalidade de desempenho. (isso ocorre completamente pela implementação da CPU. alguma CPU móvel é conhecida como esse tipo de otimização, para economizar energia)

Basicamente, o C # é uma linguagem compilada estaticamente. Mas não sempre. Não sei a condição exata e isso depende inteiramente da implementação do compilador. Alguns compiladores podem eliminar a possibilidade de envio dinâmico, impedindo a substituição do método se o método estiver marcado como sealed. Compiladores estúpidos podem não. Este é o benefício de desempenho do sealed.


Esta resposta ( por que é mais rápido processar uma matriz classificada do que uma matriz não classificada? ) Está descrevendo a previsão de ramificação muito melhor.

eonil
fonte
1
As CPUs da classe Pentium pré-buscam diretamente o despacho indireto. Às vezes, o redirecionamento de ponteiro de função é mais rápido do que se (não pode ser ignorado) por esse motivo.
217 Joshua Joshua
2
Uma vantagem da função não virtual ou selada é que elas podem ser incorporadas em mais situações.
CodesInChaos
4

<off-topic-rant>

Eu detesto aulas seladas. Mesmo que os benefícios de desempenho sejam impressionantes (o que duvido), eles destroem o modelo orientado a objetos, impedindo a reutilização por herança. Por exemplo, a classe Thread é selada. Embora eu possa ver que é possível que os threads sejam o mais eficientes possível, também posso imaginar cenários em que a subclasse de Thread traria grandes benefícios. Autores de classe, se você deve selar suas aulas por motivos de "desempenho", forneça uma interface no mínimo, para que não tenhamos que substituir e substituir em todos os lugares em que precisamos de um recurso que você esqueceu.

Exemplo: SafeThread teve que quebrar a classe Thread porque o Thread está selado e não há interface IThread; O SafeThread intercepta automaticamente exceções não tratadas em threads, algo completamente ausente da classe Thread. [e não, os eventos de exceção não tratada não capturam exceções não tratadas em threads secundários].

</off-topic-rant>

Steven A. Lowe
fonte
35
Não selo minhas aulas por razões de desempenho. Eu os selo por motivos de design. Projetar para herança é difícil, e esse esforço será desperdiçado na maioria das vezes. Eu concordo totalmente em fornecer interfaces - essa é uma solução muito superior às classes de cancelamento de selagem.
Jon Skeet
6
O encapsulamento geralmente é uma solução melhor que a herança. Para dar um exemplo específico de encadeamento, a captura de exceções de encadeamento quebra o Princípio de Substituição de Liskov porque você alterou o comportamento documentado da classe Thread, portanto, mesmo que você possa derivar dela, não seria razoável dizer que você poderia usar o SafeThread em qualquer lugar você poderia usar o Thread. Nesse caso, seria melhor encapsular o Thread em outra classe com comportamento documentado diferente, o que você pode fazer. Às vezes as coisas são seladas para o seu próprio bem.
Greg Beech
1
@ [Greg Beech]: opinião, não fato - ser capaz de herdar do Thread para corrigir uma supervisão hedionda em seu design NÃO é uma coisa ruim ;-) E eu acho que você está exagerando o LSP - a propriedade provável q (x) em neste caso é 'uma excepção não destrói o programa', que não é uma "propriedade desejável" :-)
Steven A. Lowe
1
Não, mas tive minha parte do código de baixa qualidade em que permiti que outros desenvolvedores abusassem de minhas coisas por não selá-las ou permitir casos de canto. Atualmente, a maior parte do meu código é asserções e outras coisas relacionadas a contratos. E eu sou bastante aberto sobre o fato de fazer isso apenas para ser uma dor no traseiro.
Turing Complete
2
Como estamos fazendo discursos fora do assunto aqui, assim como você detesta aulas seladas, eu detesto engolir exceções. Não há nada pior do que quando algo dá errado, mas o programa continua. JavaScript é o meu favorito. Você altera um código e, de repente, clicar em um botão não faz absolutamente nada. Ótimo! ASP.NET e UpdatePanel é outro; sério, se meu manipulador de botões lança é um grande negócio e precisa bater, para que eu saiba que há algo que precisa ser consertado! Um botão que não faz nada é mais inútil do que um botão que abre uma tela de falha!
Roman Starkov 26/08/13
4

Marcar uma classe não sealeddeve ter impacto no desempenho.

Há casos em que cscpode ser necessário emitir um callvirtcódigo de operação em vez de um callcódigo de operação. No entanto, parece que esses casos são raros.

E parece-me que o JIT deve ser capaz de emitir a mesma chamada de função não virtual do callvirtque seria call, se souber que a classe ainda não possui nenhuma subclasse. Se existir apenas uma implementação do método, não há sentido em carregar o endereço de uma vtable - basta chamar a única implementação diretamente. Nesse caso, o JIT pode até alinhar a função.

É um pouco arriscado da parte do JIT, porque se uma subclasse for carregada mais tarde, o JIT terá que jogar fora esse código de máquina e compilá-lo novamente, emitindo uma chamada virtual real. Meu palpite é que isso não acontece frequentemente na prática.

(E sim, os designers de VM realmente buscam agressivamente essas pequenas vitórias de desempenho.)

Jason Orendorff
fonte
3

Classes seladas devem fornecer uma melhoria de desempenho. Como uma classe selada não pode ser derivada, quaisquer membros virtuais podem ser transformados em membros não virtuais.

Claro, estamos falando de ganhos realmente pequenos. Eu não marcaria uma classe como selada apenas para obter uma melhoria de desempenho, a menos que os perfis revelassem que era um problema.

Karl Seguin
fonte
Eles deveriam , mas parece que não. Se o CLR tivesse o conceito de tipos de referência não nulos, uma classe selada seria realmente melhor, pois o compilador poderia emitir em callvez de callvirt... Eu adoraria tipos de referência não nulos por muitas outras razões também ... O que você achou do artigo
Orion Edwards
3

Considero as classes "seladas" o caso normal e SEMPRE tenho um motivo para omitir a palavra-chave "selada".

As razões mais importantes para mim são:

a) Melhor verificação do tempo de compilação (a conversão para interfaces não implementadas será detectada no tempo de compilação, não apenas no tempo de execução)

e, principal motivo:

b) O abuso das minhas aulas não é possível assim

Eu gostaria que a Microsoft tivesse "selado" o padrão, não "não lacrado".

Turing Complete
fonte
Eu acredito que "omitir" (deixar de fora) deve ser "emitir" (produzir)?
user2864740
2

@ Vaibhav, que tipo de teste você executou para medir o desempenho?

Eu acho que seria preciso usar o Rotor e detalhar o CLI e entender como uma classe selada melhoraria o desempenho.

SSCLI (rotor)
SSCLI: infraestrutura de idioma comum de origem compartilhada

A Common Language Infrastructure (CLI) é o padrão ECMA que descreve o núcleo do .NET Framework. A CLI de origem compartilhada (SSCLI), também conhecida como Rotor, é um arquivo compactado do código-fonte para uma implementação funcional da CLI ECMA e da especificação de linguagem ECMA C #, tecnologias no coração da arquitetura .NET da Microsoft.

alta tecnologia
fonte
O teste incluiu a criação de uma hierarquia de classes, com alguns métodos que estavam realizando trabalhos fictícios (principalmente manipulação de strings). Alguns desses métodos eram virtuais. Eles estavam se chamando aqui e ali. Em seguida, chamando esses métodos 100, 10000 e 100000 vezes ... e medindo o tempo decorrido. Em seguida, execute-os após marcar as classes como seladas. e medindo novamente. Não há diferença neles.
Vaibhav
2

as classes seladas serão pelo menos um pouco mais rápidas, mas às vezes podem ser muito mais rápidas ... se o JIT Optimizer puder integrar chamadas que de outra forma seriam chamadas virtuais. Portanto, onde há métodos frequentemente chamados de pequenos o suficiente para serem incorporados, considere definitivamente selar a classe.

No entanto, o melhor motivo para selar uma classe é dizer "Eu não projetei isso para ser herdado, então não vou deixar você se queimar assumindo que ele foi projetado para ser assim, e não vou me queimar ficando preso em uma implementação porque eu deixo você derivar dela. "

Eu sei que alguns aqui disseram que odeiam classes seladas porque querem a oportunidade de derivar de qualquer coisa ... mas essa não é a opção mais sustentável de manter ... porque a exposição de uma classe a derivações impede você muito mais do que não expor tudo aquele. É semelhante a dizer "Eu detesto classes que têm membros particulares ... muitas vezes não consigo fazer com que a classe faça o que quero, porque não tenho acesso". O encapsulamento é importante ... a vedação é uma forma de encapsulamento.

Brian Kennedy
fonte
O compilador C # ainda usará callvirt(chamada de método virtual) por exemplo métodos em classes seladas, porque ainda precisa fazer uma verificação de objeto nulo nelas. Com relação ao inlining, o CLR JIT pode (e faz) o método virtual embutido exige classes seladas e não seladas ... então sim. O desempenho é um mito.
Orion Edwards
1

Para realmente vê-los, é necessário analisar o código e compilado pelo JIT (último).

Código C #

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();
}

Código MIL

.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

Código Compilado JIT

--- 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  

Enquanto a criação dos objetos é a mesma, as instruções executadas para invocar os métodos da classe selada e derivada / base são ligeiramente diferentes. Após mover os dados para registradores ou RAM (instrução mov), a invocação do método selado, execute uma comparação entre dword ptr [ecx], ecx (instrução cmp) e chame o método enquanto a classe derivada / base executa diretamente o método. .

De acordo com o relatório escrito por Torbjëorn Granlund, latências e taxa de transferência de instruções para processadores AMD e Intel x86 , a velocidade da seguinte instrução em um Intel Pentium 4 é:

  • mov : possui 1 ciclo como latência e o processador pode sustentar 2,5 instruções por ciclo desse tipo
  • cmp : possui 1 ciclo como latência e o processador pode sustentar 2 instruções por ciclo desse tipo

Link : https://gmplib.org/~tege/x86-timing.pdf

Isso significa que, idealmente , o tempo necessário para invocar um método selado é de 2 ciclos, enquanto o tempo necessário para invocar um método derivado ou de classe base é de 3 ciclos.

A otimização dos compiladores fez a diferença entre os desempenhos de uma classe selada e não selada tão baixa que estamos falando de círculos de processador e, por esse motivo, são irrelevantes para a maioria das aplicações.

Ivan Porta
fonte
-10

Execute esse código e você verá que as classes seladas são duas vezes mais rápidas:

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";
    }
}

saída: classe selada: 00: 00: 00.1897568 classe não selada: 00: 00: 00.3826678

RuslanG
fonte
9
Há alguns problemas com isso. Primeiro, você não está redefinindo o cronômetro entre o primeiro e o segundo teste. Segundo, a maneira como você está chamando o método significa que todos os códigos op serão chamados e não callvirt, portanto o tipo não importa.
Cameron MacFarland
1
RuslanG, você esqueceu de chamar watch.Reset () depois de executar o primeiro teste. o_O:]
Sim, o código acima possui bugs. Por outro lado, quem pode se gabar de nunca introduzir erros no código? Mas essa resposta tem uma coisa importante melhor do que todas as outras: ela tenta medir o efeito em questão. E essa resposta também compartilha o código para todos vocês inspecionarem e votarem em massa (eu me pergunto por que a votação em massa é necessária?). Em contraste com outras respostas. Isso merece respeito na minha opinião ... Observe também que o usuário é novato, portanto não é uma abordagem muito acolhedora. Algumas correções simples e esse código se tornam úteis para todos nós. Fix + share versão fixa, se você se atreve
Roland Pihlakas
Não concordo com @RolandPihlakas. Como você disse, "quem pode se gabar de si mesmo para nunca introduzir erros no código". Resposta -> Não, nenhuma. Mas esse não é o ponto. O ponto é que é uma informação errada que pode enganar outro novo programador. Muitas pessoas podem facilmente perder que o cronômetro não foi zerado. Eles poderiam acreditar que as informações de referência são verdadeiras. Isso não é mais prejudicial? Alguém que está rapidamente procurando uma resposta pode nem ler esses comentários, ao contrário, ele / ela veria uma resposta e poderá acreditar nisso.
Emran Hussain