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 :)
.net
optimization
frameworks
performance
Vaibhav
fonte
fonte
Respostas:
À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
fonte
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
call
vscallvirt
IL.Call
é mais rápido quecallvirt
ecallvirt
é 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 decalvirts
paracalls
e 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, acallvirt
é 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
call
porque não podem ser subclassificadas e nunca são nulas.Veja esta pergunta para mais informações:
Call and callvirt
fonte
call
são usadas são: na situaçãonew T().Method()
, parastruct
métodos, para chamadas não virtuais avirtual
métodos (comobase.Virtual()
) ou parastatic
métodos. Em qualquer outro lugar usacallvirt
.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.
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:
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
call
vez decallvirt
nã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á
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
WriteIt
mé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:
Vs uma classe selada:
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 :-)
fonte
callvirt
quando esses métodos não são virtuais?callvirt
para 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 usecallvirt
. Para remover ocallvirt
e 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)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.
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 dosealed
.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.
fonte
<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>
fonte
Marcar uma classe não
sealed
deve ter impacto no desempenho.Há casos em que
csc
pode ser necessário emitir umcallvirt
código de operação em vez de umcall
có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
callvirt
que seriacall
, 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.)
fonte
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.
fonte
call
vez decallvirt
... Eu adoraria tipos de referência não nulos por muitas outras razões também ... O que você achou do artigoConsidero 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".
fonte
@ 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.
fonte
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.
fonte
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.Para realmente vê-los, é necessário analisar o código e compilado pelo JIT (último).
Código C #
Código MIL
Código Compilado JIT
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 é:
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.
fonte
Execute esse código e você verá que as classes seladas são duas vezes mais rápidas:
saída: classe selada: 00: 00: 00.1897568 classe não selada: 00: 00: 00.3826678
fonte