.NET Core: x86_64 intrínseco em máquinas virtuais

Vivemos em uma era de domínio da arquitetura x86. Todos os processadores compatíveis com x86 são semelhantes, mas todos são ligeiramente diferentes. E não apenas o fabricante, a frequência e o número de núcleos.

A arquitetura x86 durante sua existência (e popularidade) sofreu muitas atualizações importantes (por exemplo, a extensão para 64 bits - x86_64) e a adição de "conjuntos de instruções estendidos". Os compiladores, que por padrão geram código o mais comum possível para todos os processadores, também precisam se adaptar a isso. Mas entre as instruções estendidas, há muitas interessantes e úteis. Por exemplo, em programas de xadrez , as instruções para trabalhar com bits são frequentemente usadas : POPCNT, BSF / BSR (ou análogos mais recentes TZCNT / LZCNT ), PDEP, BSWAP, etc.

Nos compiladores C e C ++, o acesso explícito a essas instruções é implementado através de "funções intrínsecas deste processador". exemplo1 exemplo2

Não havia acesso tão conveniente para .NET e C #, então, uma vez, criei meu próprio invólucro, que fornecia emulação de tais funções, mas, se a CPU os suportasse, substitui a chamada diretamente no código de chamada. Felizmente, a maioria dos intrínsecos que eu preciso foram colocados em 5 bytes do código de operação CALL. Os detalhes podem ser lidos no hub neste link .

Muitos anos se passaram desde então, no .NET intrínsecos normais nunca apareceram. Mas saiu o .NET Core, no qual a situação foi corrigida. Primeiro vieram as instruções do vetor e, em seguida, quase todo o conjunto * de System.Runtime.Intrinsics.X86 .
* - não há BSF e BSR "ultrapassados" e

tudo parecia agradável e conveniente. Exceto que a definição de suporte para cada conjunto de instruções sempre foi confusa (algumas são incluídas imediatamente pelos conjuntos, para outras existem sinalizadores separados). Portanto, o .NET Core nos confundiu ainda mais com o fato de que também existem algumas dependências entre os conjuntos "permitidos".

Isso surgiu quando tentei executar o código em uma máquina virtual com o hipervisor KVM: erros ocorreram System.PlatformNotSupportedException: Operation is not supported on this platform at System.Runtime.Intrinsics.X86.Bmi1.X64.TrailingZeroCount(UInt64 value). Da mesma forma para System.Runtime.Intrinsics.X86.Popcnt.X64.PopCount. Mas se para o POPCNT era possível colocar uma sinalização bastante óbvia nos parâmetros da virtualização, o TZCNT me levou a um beco sem saída. Na figura a seguir, a saída da ferramenta que verifica a disponibilidade de intrínsecos no netcore (código e binário no final do artigo) e a bem conhecida CPU-Z:



Mas a saída da ferramenta retirada da página do MSDN sobre CPUID :



Apesar do fato de o processador relatar suporte para tudo necessário, a instrução Intrinsics.X86.Bmi1.X64.TrailingZeroCountcontinuava caindo com a execução System.PlatformNotSupportedException.

Para descobrir isso, precisamos olhar o processador através dos olhos do NETCore. Quais fontes estão no github. Vamos procurar o cupido lá e seguir o método.Há EEJitManager::SetCpuInfo()

muitas condições diferentes e algumas delas estão aninhadas. Peguei esse método e o copiei em um projeto vazio. Além disso, eu tive que escolher alguns outros métodos e um arquivo montador inteiro ( como adicionar asm a um novo estúdio ). Resultado da execução:



como você pode ver, o sinalizador InstructionSet_BMI1ainda está definido (embora alguns outros não estejam definidos).

Se você procurar esse sinalizador no repositório, poderá encontrar este código :

if (resultflags.HasInstructionSet(InstructionSet_BMI1) && !resultflags.HasInstructionSet(InstructionSet_AVX))
    resultflags.RemoveInstructionSet(InstructionSet_BMI1);

Então, ela é o nosso vício! Se o AVX não estiver definido, o IMC1 (e alguns outros conjuntos) será desativado. Qual é a lógica, ainda não está claro para mim, mas esperamos que ela ainda exista. Agora resta entender por que o cpu-z e outras ferramentas veem o AVX, mas o netcore não.

Vamos ver como a saída da nossa ferramenta em diferentes processadores difere:

>diff a b
7c7,8
< Test ((buffer[8] & 0x02) != 0) -> 0
---
> Test ((buffer[8] & 0x02) != 0) -> 1
> ==> Set InstructionSet_PCLMULQDQ
18c19,32
< Test ((buffer[11] & 0x18) == 0x18) -> 0
---
> Test ((buffer[11] & 0x18) == 0x18) -> 1
> Test (hMod == NULL) -> 0
> Test (pfnGetEnabledXStateFeatures == NULL) -> 0
> Test ((FeatureMask & XSTATE_MASK_AVX) == 0) -> 0
> Test (DoesOSSupportAVX() && (xmmYmmStateSupport() == 1)) -> 1
> Test (hMod == NULL) -> 0
> Test (pfnGetEnabledXStateFeatures == NULL) -> 0
> Test ((FeatureMask & XSTATE_MASK_AVX) == 0) -> 0
> ==> Set InstructionSet_AVX
> Test ((buffer[9] & 0x10) != 0) -> 1
> ==> Set InstructionSet_FMA
> Test (maxCpuId >= 0x07) -> 1
> Test ((buffer[4] & 0x20) != 0) -> 1
> ==> Set InstructionSet_AVX2

  1. O buffer de verificação [8] e 0x02 falha, é PCLMULQDQ
  2. O buffer [11] e 0x18 falham, é AVX e OSXSAVE, AVX já está definido (CPU-Z vê isso), é necessário OSXSAVE
  3. E por trás disso, há outras verificações que levam ao sinalizador InstructionSet_AVX

Então, o que fazer com o viral? Se possível, é melhor colocar libvirt.cpu_mode em passagem de host ou modelo de host .

Mas se isso não for possível, você precisará adicionar toda a sopa das instruções, em particular ssse3, sse4.1, sse4.2, sse4a, popcnt, abm, bmi1, bmi2, avx, avx2, osxsave, xsave, pclmulqdq. Aqui eu digo olá e obrigadovdsina_m;)

E você pode verificar o host ou a máquina virtual para obter suporte de instruções e como o .NET Core a analisa com a ajuda desta ferramenta: (por enquanto, zip, postarei no github posteriormente).


All Articles