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

    Мы живём в эпоху доминирования архитектуры 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 можно с помощью этой тулзы: (пока что зип, позже выложу на гитхаб).

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 1

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое