- PVSM.RU - https://www.pvsm.ru -

.NET Core: интринсики x86_64 на виртуальных машинах

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

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

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

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

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

И всё вроде-бы стало хорошо и удобно. Если не считать того, что определение поддержки каждого набора инструкций всегда было запутанным [7] (какие-то включаются сразу наборами, для каких-то есть отдельные флаги). Так .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:

.NET Core: интринсики x86_64 на виртуальных машинах - 1

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

.NET Core: интринсики x86_64 на виртуальных машинах - 2

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

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

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

.NET Core: интринсики x86_64 на виртуальных машинах - 3

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

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

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 [13].

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

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

Автор: Антон Д

Источник [17]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/c-2/351725

Ссылки в тексте:

[1] часто используются: https://www.chessprogramming.org/BitScan

[2] TZCNT/LZCNT: https://fgiesen.wordpress.com/2013/10/18/bit-scanning-equivalencies/

[3] пример1: https://docs.microsoft.com/en-us/cpp/intrinsics/popcnt16-popcnt-popcnt64?view=vs-2019

[4] пример2: https://clang.llvm.org/doxygen/popcntintrin_8h_source.html

[5] на хабре по этой ссылке: https://habr.com/ru/post/239619/

[6] System.Runtime.Intrinsics.X86: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.intrinsics.x86?view=netcore-3.1

[7] было запутанным: https://en.wikipedia.org/wiki/CPUID#EAX=1:_Processor_Info_and_Feature_Bits

[8] страницы MSDN про CPUID: https://docs.microsoft.com/en-us/cpp/intrinsics/cpuid-cpuidex?view=vs-2019

[9] на гитхабе: https://github.com/dotnet/runtime/

[10] EEJitManager::SetCpuInfo: https://github.com/dotnet/runtime/blob/5d874beea87e415fb81522970cf86431d08bc7ed/src/coreclr/src/vm/codeman.cpp#L1267

[11] как добавить асм в свежую студию: https://habr.com/ru/post/252647/

[12] такой код: https://github.com/dotnet/runtime/blob/469e98aae6052647b9a0a072bfca0359b4e2910a/src/coreclr/src/inc/corinfoinstructionset.h#L255

[13] libvirt.cpu_mode в host-passthrough или host-model: https://docs.openstack.org/nova/latest/admin/configuration/hypervisor-kvm.html#specify-the-cpu-model-of-kvm-guests

[14] vdsina_m: https://habr.com/ru/users/vdsina_m/

[15] core_intrin_check.bin.win64.zip: https://yadi.sk/d/Mi_rrnjdKfdfbQ

[16] core_intrin_check.source.zip: https://yadi.sk/d/QtcL4h6XcoswJw

[17] Источник: https://habr.com/ru/post/495462/?utm_source=habrahabr&utm_medium=rss&utm_campaign=495462