Мы живём в эпоху доминирования архитектуры x86. Все x86-совместимые процессоры похожи, но и все при этом немного отличаются. И не только производителем, частотой и количеством ядер.

Архитектура x86 за время своего существования (и популярности) пережила много крупных апдейтов (например, расширение до 64 бит — x86_64) и добавлений «расширенных наборов инструкций». К этому приходится подстраиваться и компиляторам, которые по-умолчанию генерируют максимально общий для всех процессоров код. Но среди расширенных инструкций есть много интересного и полезного. Например, в шахматных программах часто используются инструкции для работы с битами: POPCNT, BSF/BSR (или более свежие аналоги TZCNT/LZCNT), PDEP, BSWAP и т.д.

В компиляторах C и C++ явный доступ к таким инструкциям реализован через «присущие (intrinsic) данному процессору функции». пример1 пример2

Для .NET и C# такого удобного доступа не существовало, поэтому когда-то давно я сделал свою обёртку, которая предоставляла эмуляцию таких функций, но если CPU их поддерживал, то заменяла их вызов прямо в вызывающем коде. Благо, большинство нужных мне интринсиков помещались в 5 байт опкода CALL. Подробности можно почитать на хабре по этой ссылке.

С тех пор прошло много лет, в .NET нормальных интринсиков так и не появилось. Но вышел .NET Core, в котором ситуацию исправили. Сначала появились векторные инструкции, в потом и почти весь* набор System.Runtime.Intrinsics.X86.
* — нет «устаревших» BSF и BSR

И всё вроде-бы стало хорошо и удобно. Если не считать того, что определение поддержки каждого набора инструкций всегда было запутанным (какие-то включаются сразу наборами, для каких-то есть отдельные флаги). Так .NET Core запутало нас ещё сильнее с тем, что между «разрешёнными» наборами есть ещё и какие-то зависимости.

Всплыло это при попытке запустить код на виртуальной машине с гипервизором KVM: посыпались ошибки System.PlatformNotSupportedException: Operation is not supported on this platform at System.Runtime.Intrinsics.X86.Bmi1.X64.TrailingZeroCount(UInt64 value). Аналогично и для System.Runtime.Intrinsics.X86.Popcnt.X64.PopCount. Но если для POPCNT можно было поставить достаточно очевидный флаг в параметрах виртуализации, то TZCNT ввёл меня в тупик. На следующей картинке вывод тулзы, проверяющей доступность интринсиков в netcore (код и бинарник в конце статьи) и всем известного CPU-Z:



А вот вывод тулзы, взятой со страницы MSDN про CPUID:



Несмотря на то, что процессор рапортует о поддержке всего, что требуется, инструкция Intrinsics.X86.Bmi1.X64.TrailingZeroCount всё равно продолжала падать с эксепшеном System.PlatformNotSupportedException.

Чтобы в этом разобраться, нам надо взглянуть на процессор глазами NETCore. Исходники которого лежат на гитхабе. Поищем там cupid и выйдем на метод EEJitManager::SetCpuInfo()

В нём достаточно много разных условий, причём некоторые из них вложенные. Я взял этот метод и скопипастил в пустой проект. Дополнительно к нему пришлось забрать пару других методов и ещё целый ассемблерный файл (как добавить асм в свежую студию). Результат выполнения:



Как видим, флаг InstructionSet_BMI1 всё же выставлен (хотя не выставлены некоторые другие).

Если поискать этот флаг по репозиторию, то можно наткнуться на такой код:

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

Так вот, она наша зависимость! Если не определился AVX, то отключается и BMI1 (и некоторые другие наборы). В чём логика, мне пока не ясно, но будем надеяться на то, что она всё-таки есть. Теперь осталось разобраться, почему cpu-z и другие тулзы видят AVX, а netcore — нет.

Посмотрим, чем отличается вывод нашей тулзы на разных процессорах:

>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. Фейлится проверка buffer[8] & 0x02, это PCLMULQDQ
  2. Фейлится buffer[11] & 0x18, это AVX & OSXSAVE, AVX уже выставлен (это видит CPU-Z), нужен OSXSAVE
  3. А за ней и другие проверки, которые ведут к флагу InstructionSet_AVX

Так что же делать с вируалкой? Если есть возможность, то лучше всего поставить libvirt.cpu_mode в host-passthrough или host-model.

Но если такой возможности нет, то придётся добавлять весь суп из инструкций, в частности ssse3, sse4.1, sse4.2, sse4a, popcnt, abm, bmi1, bmi2, avx, avx2, osxsave, xsave, pclmulqdq. Здесь я передаю привет и спасибо vdsina_m ;)

А проверить ваш хост или виртуалку на поддержку инструкций и то, как на это смотрит .NET Core можно с помощью этой тулзы: (пока что зип, позже выложу на гитхаб).