.NET Core: intrinsèque x86_64 sur les machines virtuelles

Nous vivons à une époque de domination de l'architecture x86. Tous les processeurs compatibles x86 sont similaires, mais tous sont légèrement différents. Et pas seulement le fabricant, la fréquence et le nombre de cœurs.

L'architecture x86 au cours de son existence (et de sa popularité) a connu de nombreuses mises à jour majeures (par exemple, l'extension à 64 bits - x86_64) et l'ajout de «jeux d'instructions étendues». Les compilateurs, qui génèrent par défaut du code aussi commun que possible pour tous les processeurs, doivent également s'y adapter. Mais parmi les instructions étendues, il en existe de nombreuses intéressantes et utiles. Par exemple, dans les programmes d'échecs , des instructions pour travailler avec des bits sont souvent utilisées : POPCNT, BSF / BSR (ou analogues plus récents TZCNT / LZCNT ), PDEP, BSWAP, etc.

Dans les compilateurs C et C ++, l'accès explicite à ces instructions est implémenté via les "fonctions intrinsèques de ce processeur". example1 example2

Il n'y avait pas un tel accès pratique pour .NET et C #, donc il était une fois j'ai fait mon propre wrapper, qui fournissait l'émulation de telles fonctions, mais si le CPU les supportait, j'ai remplacé leur appel directement dans le code appelant. Heureusement, la plupart des intrinsèques dont j'ai besoin ont été placés dans 5 octets de l'opcode CALL. Les détails peuvent être lus sur le hub à ce lien .

De nombreuses années se sont écoulées depuis lors, dans .NET intrinsèques normaux ne sont jamais apparus. Mais .NET Core est sorti, dans lequel la situation a été corrigée. Viennent d'abord les instructions vectorielles, puis la quasi-totalité * de System.Runtime.Intrinsics.X86 .
* - il n'y a pas de BSF et BSR "obsolètes"

et tout semblait être agréable et pratique. Sauf que la définition de la prise en charge de chaque ensemble d'instructions a toujours été source de confusion (certaines sont incluses immédiatement par les ensembles, pour certaines il y a des drapeaux séparés). Donc .NET Core nous a encore plus confondus avec le fait qu'il existe également des dépendances entre les ensembles "autorisés".

Cela est apparu lorsque j'ai essayé d'exécuter le code sur une machine virtuelle avec l'hyperviseur KVM: des erreurs se sont produites System.PlatformNotSupportedException: Operation is not supported on this platform at System.Runtime.Intrinsics.X86.Bmi1.X64.TrailingZeroCount(UInt64 value). De même pour System.Runtime.Intrinsics.X86.Popcnt.X64.PopCount. Mais si pour POPCNT il était possible de mettre un drapeau assez évident dans les paramètres de virtualisation, alors TZCNT m'a conduit dans une impasse. Dans l'image suivante, la sortie de l'outil qui vérifie la disponibilité des intrinsèques dans netcore (code et binaire à la fin de l'article) et le CPU-Z bien connu:



mais la sortie de l'outil prise à partir de la page MSDN à propos de CPUID :



malgré le fait que le processeur signale la prise en charge de tout nécessaire, l'instruction Intrinsics.X86.Bmi1.X64.TrailingZeroCountcontinuait de tomber avec l'exécution System.PlatformNotSupportedException.

Pour comprendre cela, nous devons regarder le processeur à travers les yeux de NETCore. Quelles sources se trouvent sur github. Cherchons Cupidon là-bas et passons à la méthode. EEJitManager::SetCpuInfo()

Il y a beaucoup de conditions différentes, et certaines sont imbriquées. J'ai pris cette méthode et l'ai copiée dans un projet vide. En plus de cela, j'ai dû prendre quelques autres méthodes et un fichier assembleur complet ( comment ajouter asm à un nouveau studio ). Résultat d'exécution:



Comme vous pouvez le voir, le drapeau InstructionSet_BMI1est toujours activé (bien que certains autres ne le soient pas).

Si vous recherchez cet indicateur dans le référentiel, vous pouvez rencontrer ce code :

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

Elle est donc notre addiction! Si AVX n'est pas défini, alors BMI1 (et certains autres ensembles) est désactivé. Quelle est la logique, ce n'est pas encore clair pour moi, mais nous espérons qu'elle existe toujours. Il reste maintenant à comprendre pourquoi cpu-z et d'autres outils voient AVX, mais pas netcore.

Voyons comment la sortie de notre outil sur différents processeurs diffère:

>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. Le tampon de vérification [8] et 0x02 échoue, c'est PCLMULQDQ
  2. Buffer [11] & 0x18 échoue, c'est AVX & OSXSAVE, AVX est déjà défini (CPU-Z le voit), OSXSAVE est nécessaire
  3. Et derrière, il y a d'autres contrôles qui mènent au drapeau InstructionSet_AVX

Alors que faire du viral? Si possible, il est préférable de mettre libvirt.cpu_mode dans host-passthrough ou host-model .

Mais si cela n'est pas possible, vous devez ajouter toute la soupe des instructions, en particulier ssse3, sse4.1, sse4.2, sse4a, popcnt, abm, bmi1, bmi2, avx, avx2, osxsave, xsave, pclmulqdq. Ici je dis bonjour et mercivdsina_m;)

Et vous pouvez vérifier votre hôte ou votre machine virtuelle pour la prise en charge des instructions et la façon dont .NET Core les examine à l'aide de cet outil: (pour l'instant, zip, je le publierai dans le github plus tard).


All Articles