Прикладной SIMD 003: прежде, чем начать (часть 1)

Дата публикации 12 мар 2018 | Редактировалось 1 апр 2020
Предисловие

В этой статье мы рассмотрим необходимый минимум действий для того, чтобы подготовить почву для работы с SIMD. Для этого необходимо выполнить ряд действий:
  • удобно организовать проект;
  • написать функцию идентификации процессора и поиска расширенных наборов инструкций;
  • реализовать механизм инициализации всего DSP-модуля;
  • написать функции инициализации и восстановления машинного контекста;
  • собрать всё воедино с нужными опциями.

Организация проекта

Для того, чтобы впоследствии было удобно разрабатывать код, совместимый как с 32-разрядной, так и 64-разрядной архитектурой, нужно свести к минимуму условную компиляцию внутри файлов, а также разнести код, предназначенный для разных архитектур, в разные подкаталоги. Также очень полезным будет объявить в отдельном заголовочном файле ряд вспомогательных макросов.

Начнём с того, что i386 и x86_64 – достаточно близкие архитектуры, и разносить их по отдельным подкаталогам нет большой необходимости. Однако у нас есть ещё как минимум один вид архитектуры – нативная архитектура (native), в случае, если мы захотим скомпилировать код под иную аппаратную платформу. Поэтому сразу в каталоге проекта создадим следующие подкаталоги:

  • include/dsp/x86 – оптимизированный под x86 и x86_64 код;
  • include/dsp/native – нативный код на C/C++;
  • src/dsp – файлы C/C++, объединяющие заголовочные файлы из каталогов include/dsp/*.

Все функции мы поместим в отдельное пространство имён dsp, а расширения под определённые наборы инструкций будем помещать в соответствующие вложенные пространства имён.

Первым делом создадим файл include/dsp/defs.h, в котором подключим стандартные заголовочные файлы с объявлением типов данных фиксированной длины stdint.h (int32_t, uint64_t и т. д.) и определим архитектуру и платформу, под которую будут компилироваться исходные коды приложения.

Помимо прочего, запретим включение этого файла напрямую из других файлов:
Код (C):
  1.  
  2. #ifndef DSP_H_IMPL_
  3.     #error "This header should not be included directly"
  4. #endif /* DSP_H_IMPL_ */
  5.  
А в заголовочном файле include/dsp.h сделаем его правильное подключение:
Код (C):
  1.  
  2. #define DSP_H_IMPL_
  3.     #include <dsp/defs.h>
  4. #undef DSP_H_IMPL_
  5.  
В самом же файле include/dsp/defs.h объявим полезные макросы:
  • ARCH_X86 – макрос объявлен в том случае, когда код компилируется под x86-совместимую архитектуру;
  • ARCH_I386 – макрос объявлен только в том случае, когда код компилируется под 32-разрядную архитектуру x86;
  • ARCH_X86_64 – макрос объявлен только в том случае, когда код компилируется под 64-разрядную архитектуру x86;
  • ARCH_LE – макрос объявлен в случае, если архитектура работает в режиме Little endian (наименее значащий байт имеет наименьший адрес).
  • ARCH_BE – макрос объявлен в случае, если архитектура работает в режиме Big endian (наиболее значащий байт имеет наименьший адрес).
  • __IF_32(code) – подставляет код code только в том случае, если код компилируется под 32-разрядную архитектуру.
  • __IF_64(code) – подставляет код code только в том случае, если код компилируется под 64-разрядную архитектуру.
  • __IF_LE(code) – подставляет код code только в том случае, если процессор работает в ржеиме Little Endian.
  • __IF_BE(code) – подставляет код code только в том случае, если процессор работает в режиме Big Endian.
  • __IF_LEBE(le, be) – подставляет только код le в том случае, если ппроцессор работает в режиме Little Endian, в противном случае подставляет код be.
  • PLATFORM_UNIX – объявлен, если код компилируется под операционную систему Unix.
  • PLATFORM_LINUX – объявлен, если код компилируется под операционную систему на базе ядра Linux.
  • PLATFORM_BSD – объявлен, если код компилируется под операционную систему семейства BSD.
  • PLATFORM_MACOSX – объявлен, если код компилируется под операционную систему семейства MacOS.
  • PLATFORM_UNIX_COMPATIBLE – объявлен, если в качестве платформы используется Unix-совместимая (POSIX-совместимая) операционная система.
  • PLATFORM_WINDOWS – объявлен в случае, если код компилируется под Windows.
  • __ASM_EMIT(code) – генерирует ассемблерную строку code для встроенного в GCC ассемблера, добавляя символ переноса и табуляции в конце строки
  • __ASM_EMIT32(code) – аналогично __ASM_EMIT, но только для 32-разрядной архитектуры;
  • __ASM_EMIT64(code) – аналогично __ASM_EMIT, но только для 64-разрядной архитектуры;
  • __dsp_aligned16 – свойство, задающее выравнивание адреса структуры по границе 16 байт;
  • __dsp_aligned32 – свойство, задающее выравнивание адреса структуры по границе 32 байт;
  • __dsp_aligned64 – свойство, задающее выравнивание адреса структуры по границе 64 байт;
  • __dsp_aligned(align) – свойство, задающее выравнивание адреса структуры по границе align байт.

Эти макросы позволяют сократить необходимое количество действий при добавлении условной компиляции в исходный код и частично абстрагироваться от самого компилятора.

Идентификация процессора и его расширений

Следующим шагом, соответственно, будет необходимо определить модель процессора и перечень его расширений. В этом нам поможет инструкция CPUID, поддерживаемая процессорами x86 начиная с линейки некоторых процессоров 80386 и полностью поддерживаемая более поздними моделями. Тем не менее, для 32-битных платформ всё же будет полезным на всякий случай проверить поддержку этой инструкции. Для этого надо попытаться изменить значение 21-го бита (ID) регистра EFLAGS. Для этого заведём специальный файл include/dsp/x86/cpuid.h и напишем соответствующую функцию cpuid_supported:
Код (C):
  1.  
  2. #if defined(ARCH_I386)
  3.     bool cpuid_supported()
  4.     {
  5.         bool result;
  6.         __asm__ __volatile__
  7.         (
  8.             __ASM_EMIT("pushfl")
  9.             __ASM_EMIT("pop         %%eax")
  10.             __ASM_EMIT("mov         %%eax, %%edx")
  11.             __ASM_EMIT("xor         $0x200000, %%eax")
  12.             __ASM_EMIT("push        %%eax")
  13.             __ASM_EMIT("popfl")
  14.             __ASM_EMIT("pushfl")
  15.             __ASM_EMIT("pop         %%eax")
  16.             __ASM_EMIT("xor         %%edx, %%eax")
  17.             __ASM_EMIT("shr         $21, %%eax")
  18.             __ASM_EMIT("and         $1, %%eax")
  19.             : "=a"(result)
  20.             : : "cc", "%edx"
  21.         );
  22.  
  23.         return result;
  24.     }
  25. #elif defined(ARCH_X86_64)
  26.     inline bool cpuid_supported()
  27.     {
  28.         return true;
  29.     }
  30. #endif /* ARCH_I386 */
  31.  
Как видно из кода, в случае 64-разрядной архитектуры функция безусловно возвращает истину, а в 32-разрядной реализации мы проделываем следующие операции:
  1. сохраняем регистр флагов в стек;
  2. восстанавливаем его значение в регистр EAX и запоминаем копию в EDX;
  3. пытаемся операцией XOR проинвертировать 21-ый бит в регистре EAX;
  4. сохраняем значение регистра EAX в стек, восстанавливаем регистр флагов, снова сохраняем регистр флагов в стек и выталкиваем значение из стека в EAX;
  5. делаем XOR между сохранённым и последним восстановленным значением регистра флагов, если бит ID сохранился, то он должен принять ненулевое значение;
  6. преобразуем значение в EAX при помощи операций SHR и AND так, чтобы оно принимало значение 1, если бит ID был изменён, либо 0 в противном случае.

Отлично, теперь мы можем вызывать эту функцию из нашего кода, чтобы убедиться, что CPUID поддерживается. Осталось реализовать функцию, которая будет вызывать инструкцию CPUID и сохранять возвращаемый ею результат в некоторую структуру в памяти. Инструкция CPUID в качестве аргумента принимает в регистре EAX номер функции идентификации и, опционально, номер подфункции в регистре ECX. Результат выполнения функции идентификации сохраняется в регистрах EAX, EBX, ECX и EDX, вне зависимости от разрядности архитектуры. Поэтому для сохранения результата предварительно объявим следующую упакованную структуру данных:
Код (C):
  1.  
  2. #pragma pack(push, 1)
  3. typedef struct cpuid_info_t
  4. {
  5.     uint32_t        eax;
  6.     uint32_t        ebx;
  7.     uint32_t        ecx;
  8.     uint32_t        edx;
  9. } cpuid_info_t;
  10. #pragma pack(pop)
  11.  
Также реализация функции будет несколько отличаться для 32-разрядной архитектуры и 64-разрядной архитектуры. Дело в том, что в 32-разрядной архитектуре генерируемый компилятором GCC базонезависимый (PIC, Position-Independent Code) код активно использует регистр EBX. Это означает, что регистр EBX ни в коем случае нельзя модифицировать, поэтому для 32-битной компиляции мы добавим его обязательное сохранение и восстановление, а для 64-битной компиляции просто поместим в clobber-список.

Код (C):
  1.  
  2. inline void cpuid(cpuid_info_t *info, uint32_t leaf, uint32_t subleaf)
  3. {
  4.     __asm__ __volatile__
  5.     (
  6.         __ASM_EMIT32("push      %%ebx")
  7.         __ASM_EMIT("cpuid")
  8.         __ASM_EMIT("mov         %%eax, 0x0(%[info])")
  9.         __ASM_EMIT("mov         %%ebx, 0x4(%[info])")
  10.         __ASM_EMIT("mov         %%ecx, 0x8(%[info])")
  11.         __ASM_EMIT("mov         %%edx, 0xc(%[info])")
  12.         __ASM_EMIT32("pop       %%ebx")
  13.  
  14.         : "+a"(leaf), "+c"(subleaf)
  15.         : [info] "D" (info)
  16.         : "cc", "memory",
  17.           __IF_64("%ebx", ) "%edx"
  18.     );
  19. }
  20.  
Помимо этого, в отдельном файле dsp/x86/features.h бъявим интересующие нас константы и тип данных cpu_features_t:

Код (C):
  1.  
  2. namespace dsp
  3. {
  4.     namespace x86
  5.     {
  6.         enum cpu_features_enum
  7.         {
  8.             // Different legacy feature set
  9.             CPU_OPTION_FPU              = 1 << 0,
  10.             CPU_OPTION_CMOV             = 1 << 1,
  11.             CPU_OPTION_MMX              = 1 << 2,
  12.  
  13.             // SSE feature set
  14.             CPU_OPTION_FXSAVE           = 1 << 3,
  15.             CPU_OPTION_SSE              = 1 << 4,
  16.             CPU_OPTION_SSE2             = 1 << 5,
  17.             CPU_OPTION_SSE3             = 1 << 6,
  18.             CPU_OPTION_SSSE3            = 1 << 7,
  19.             CPU_OPTION_SSE4_1           = 1 << 8,
  20.             CPU_OPTION_SSE4_2           = 1 << 9,
  21.             CPU_OPTION_SSE4A            = 1 << 10,
  22.  
  23.             // AVX and FMA feature set
  24.             CPU_OPTION_OSXSAVE          = 1 << 11,
  25.             CPU_OPTION_FMA3             = 1 << 12,
  26.             CPU_OPTION_FMA4             = 1 << 13,
  27.             CPU_OPTION_AVX              = 1 << 14,
  28.             CPU_OPTION_AVX2             = 1 << 15,
  29.  
  30.             // AVX-512 feature set
  31.             CPU_OPTION_AVX512F          = 1 << 16,
  32.             CPU_OPTION_AVX512DQ         = 1 << 17,
  33.             CPU_OPTION_AVX512IFMA       = 1 << 18,
  34.             CPU_OPTION_AVX512PF         = 1 << 19,
  35.             CPU_OPTION_AVX512ER         = 1 << 20,
  36.             CPU_OPTION_AVX512CD         = 1 << 21,
  37.             CPU_OPTION_AVX512BW         = 1 << 22,
  38.             CPU_OPTION_AVX512VL         = 1 << 23,
  39.             CPU_OPTION_AVX512VBMI       = 1 << 24
  40.         };
  41.  
  42.         enum cpu_vendor_enum
  43.         {
  44.             CPU_VENDOR_UNKNOWN,
  45.             CPU_VENDOR_INTEL,
  46.             CPU_VENDOR_AMD
  47.         };
  48.  
  49.         typedef struct cpu_features_t
  50.         {
  51.             uint32_t            vendor;
  52.             uint32_t            family;
  53.             uint32_t            model;
  54.             uint32_t            features;
  55.         } cpu_features_t;
  56.     }
  57. }
  58.  
Теперь мы готовы написать функцию, идентифицирующую опции процессора. Первым делом следует вызвать CPUID с leaf=0 и subleaf=0, чтобы узнать сигнатуру производителя процессора и номер максимальной поддерживаемой функции идентификации.
Код (C):
  1.  
  2. #include <dsp.h>
  3.  
  4. #include <dsp/x86/features.h>
  5.  
  6. #define DSP_X86_IMPL
  7. namespace dsp
  8. {
  9.     namespace x86
  10.     {
  11.         #include <dsp/x86/cpuid.h>
  12.     }
  13. }
  14. #undef DSP_X86_IMPL
  15.  
  16. namespace dsp
  17. {
  18.     namespace x86
  19.     {
  20.         void detect_options(cpu_features_t *f)
  21.         {
  22.             // Initialize structure
  23.             f->vendor       = CPU_VENDOR_UNKNOWN;
  24.             f->family       = 0;
  25.             f->model        = 0;
  26.             f->features     = 0;
  27.  
  28.             // X86-family code
  29.             if (!cpuid_supported())
  30.                 return;
  31.  
  32.             // Check max CPUID
  33.             cpuid_info_t info;
  34.             cpuid(&info, 0, 0);
  35.  
  36.             // Detect vendor
  37.             if ((info.ebx == X86_CPUID0_INTEL_EBX) &&
  38.                 (info.ecx == X86_CPUID0_INTEL_ECX) &&
  39.                 (info.edx == X86_CPUID0_INTEL_EDX))
  40.                 f->vendor   = CPU_VENDOR_INTEL;
  41.             else if ((info.ebx == X86_CPUID0_AMD_EBX) &&
  42.                      (info.ecx == X86_CPUID0_AMD_ECX) &&
  43.                      (info.edx == X86_CPUID0_AMD_EDX))
  44.                 f->vendor   = CPU_VENDOR_AMD;
  45.  
Функция 0 инструкции CPUID возвращает в регистре EAX максимальный номер функции, поддерживаемый инструкцией, а в регистрах EBX, ECX и EDX – сигнатуру производителя процессора. Для Intel это будет строка «GenuineIntel», а для AMD – «AuthenticAMD», остальные производители нас особо не интересуют. В коде мы сравниваем значения, хранящиеся в регистрах EBX, ECX и EDX с этими сигнатурами, после чего записываем в переменную options информацию о производителе процессора. Сами же сигнатуры объявим следующим образом:
Код (C):
  1.  
  2. // Function 0
  3. #define X86_CPUID0_INTEL_EBX                    0x756e6547
  4. #define X86_CPUID0_INTEL_ECX                    0x6c65746e
  5. #define X86_CPUID0_INTEL_EDX                    0x49656e69
  6.  
  7. #define X86_CPUID0_AMD_EBX                      0x68747541
  8. #define X86_CPUID0_AMD_ECX                      0x444d4163
  9. #define X86_CPUID0_AMD_EDX                      0x69746e65
  10.  
Информация о производителе важна ввиду того, что реализация функций CPUID у этих производителей несколько отличается, и перечень расширений AMD лучше получать, используя расширенные функции с кодами 0x80000000 и более. Далее нам необходимо быть уверенными, что мы можем вызывать функции с большими значениями leaf, что и проделываем в коде:
Код (C):
  1.  
  2.             size_t max_cpuid    = info.eax;
  3.             if (max_cpuid <= 0)
  4.                 return;
  5.  
Теперь мы можем получить информацию о семействе и модели процессора, вызвав функцию 1 инструкции CPUID. Необходимая информация вернётся в регистре EAX:
Код (C):
  1.  
  2.             // Get model and family
  3.             cpuid(&info, 1, 0);
  4.             f->family           = (info.eax >> 8) & 0x0f;
  5.             f->model            = (info.eax >> 4) & 0x0f;
  6.  
  7.             if (f->family == 0x0f)
  8.                 f->family           += (info.eax >> 20) & 0xff;
  9.             if ((f->family == 0x0f) || (f->family == 0x06))
  10.                 f->model            += (info.eax >> 12) & 0xf0;
  11.  
При этом, семейство процессора кодируется достаточно нетривиальным способом, изобразим содержимое регистра EAX:

cpuid-family.png
Рисунок 1. Содержимое регистра EAX, содержащего закодированные семейство и модель.

Официальная документация Intel сообщает следующее:
Если Family равно 0x0f, то семейство процессора дополнительно следует рассчитывать как сумму полей Family и Ext Family. При этом, модель процессора декодируется в зависимости от семейства: если Family принимает значения 0x06 или 0x0f, то модель процессора следует дополнительно рассчитывать по формуле: Ext Model * 16 + Model. По семейству и модели процессора можно определить микроархитектуру и, как следствие, включить какие-то оптимизации или отказаться от их использования. Например, процессоры AMD семейства Bulldozer и Piledriver несмотря на то, что реализуют набор инструкций AVX, тем не менее ввиду специфики реализации не дают серьёзного прироста производительности при их использовании (а, зачастую, даже и наоборот), поэтому достаточно большую часть реализации алгоритмов на AVX можно попросту не включать в конфигурацию модуля DSP для этих процессоров.

Также полезным будет вызов расширенной функции с кодом 0x80000000, которая в регистре EAX вернёт максимальный номер расширенной функции CPUID, после чего уже приступить к детекции расширений:
Код (C):
  1.  
  2.             // Get maximum available extended CPUID
  3.             cpuid(&info, 0x80000000, 0);
  4.             size_t max_ext_cpuid = info.eax;
  5.  
  6.             if (f->vendor == CPU_VENDOR_INTEL)
  7.                 do_intel_cpuid(f, max_cpuid, max_ext_cpuid);
  8.             else if (f->vendor == CPU_VENDOR_AMD)
  9.                 do_amd_cpuid(f, max_cpuid, max_ext_cpuid);
  10.  
Теперь наша задача заключается в реализации функций do_intel_cpuid и do_amd_cpuid. Большую часть расширений можно определить, используя функцию 1 (Intel, AMD) и расширенную функцию 0x80000001 (AMD). Для интересующих нас расширений можно объявить следующие константы:
Код (C):
  1.  
  2. // Function 1
  3. #define X86_CPUID1_INTEL_ECX_SSE3               (1 << 0)
  4. #define X86_CPUID1_INTEL_ECX_SSSE3              (1 << 9)
  5. #define X86_CPUID1_INTEL_ECX_FMA3               (1 << 12)
  6. #define X86_CPUID1_INTEL_ECX_SSE4_1             (1 << 19)
  7. #define X86_CPUID1_INTEL_ECX_SSE4_2             (1 << 20)
  8. #define X86_CPUID1_INTEL_ECX_XSAVE              (1 << 26)
  9. #define X86_CPUID1_INTEL_ECX_OSXSAVE            (1 << 27)
  10. #define X86_CPUID1_INTEL_ECX_AVX                (1 << 28)
  11.  
  12. #define X86_CPUID1_INTEL_EDX_FPU                (1 << 0)
  13. #define X86_CPUID1_INTEL_EDX_CMOV               (1 << 15)
  14. #define X86_CPUID1_INTEL_EDX_MMX                (1 << 23)
  15. #define X86_CPUID1_INTEL_EDX_SSE                (1 << 25)
  16. #define X86_CPUID1_INTEL_EDX_SSE2               (1 << 26)
  17.  
  18. #define X86_CPUID1_AMD_ECX_SSE3                 (1 << 0)
  19. #define X86_CPUID1_AMD_ECX_SSSE3                (1 << 9)
  20. #define X86_CPUID1_AMD_ECX_FMA3                 (1 << 12)
  21. #define X86_CPUID1_AMD_ECX_SSE4_1               (1 << 19)
  22. #define X86_CPUID1_AMD_ECX_SSE4_2               (1 << 20)
  23. #define X86_CPUID1_AMD_ECX_XSAVE                (1 << 26)
  24. #define X86_CPUID1_AMD_ECX_OSXSAVE              (1 << 27)
  25. #define X86_CPUID1_AMD_ECX_AVX                  (1 << 28)
  26.  
  27. #define X86_CPUID1_AMD_EDX_FPU                  (1 << 0)
  28. #define X86_CPUID1_AMD_EDX_CMOV                 (1 << 15)
  29. #define X86_CPUID1_AMD_EDX_MMX                  (1 << 23)
  30. #define X86_CPUID1_AMD_EDX_SSE                  (1 << 25)
  31. #define X86_CPUID1_AMD_EDX_SSE2                 (1 << 26)
  32.  
  33. // Function 80000001
  34. #define X86_XCPUID1_AMD_ECX_FMA4                (1 << 16)
  35. #define X86_XCPUID1_AMD_ECX_SSE4A               (1 << 6)
  36.  
  37. #define X86_XCPUID1_AMD_EDX_FPU                 (1 << 0)
  38. #define X86_XCPUID1_AMD_EDX_CMOV                (1 << 15)
  39. #define X86_XCPUID1_AMD_EDX_MMX                 (1 << 23
  40.  
Для определения поддержки расширений AVX, AVX2, FMA3, FMA4 и AVX512 требуется ряд дополнительных проверок. Прежде всего, нужно убедиться, что процессором поддерживается инструкция сохранения расширенного контекста процессора XSAVE, за что отвечает бит 27 регистра ECX (OSXSAVE) при вызове функции 1 инструкции CPUID. Однако может получиться так, что процессор поддерживает XSAVE, однако целевая операционная система не поддерживает сохранение расширенного контекста процесса при помощи этой инструкции. Для того, чтобы убедиться, что операционная система поддерживает сохранение контекста для AVX и более поздних расширений, необходимо прочитать содержимое специального регистра XCR0, выполнив инструкцию XGETBV, доступную в случае, если процессор поддерживает XSAVE. Флаги регистра XCR0 можно описать следующим набором констант:
Код (C):
  1.  
  2. // XCR0 register flags
  3. #define XCR0_FLAG_FPU                           (1 << 0)
  4. #define XCR0_FLAG_SSE                           (1 << 1)
  5. #define XCR0_FLAG_AVX                           (1 << 2)
  6. #define XCR0_FLAG_BNDREG                        (1 << 3)
  7. #define XCR0_FLAG_BNDCSR                        (1 << 4)
  8. #define XCR0_FLAG_OPMASK                        (1 << 5)
  9. #define XCR0_FLAG_ZMM_HI256                     (1 << 6)
  10. #define XCR0_FLAG_HI16_ZMM                      (1 << 7)
  11. #define XCR0_FLAG_PKRU                          (1 << 8)
  12.  
Если операционная система поддерживает AVX, то в регистре XCR0 должны быть выставлены флаги поддержки сохранения состояния SSE (XCR0_FLAG_SSE) и AVX (XCR0_FLAG_AVX). Если операционная система поддерживает набор расширений AVX512, то она также должна поддерживать сохранение регистров масок K0-K7 (XCR0_FLAG_OPMASK), регистров YMM16-YMM31 (XCR0_FLAG_ZMM_HI256) и старших половинок регистров ZMM (XCR0_FLAG_HI16_ZMM). Таким образом, мы можем объявить две маски XCR0, по которым можно определить поддержку AVX и AVX512:
Код (C):
  1.  
  2. #define XCR_FLAGS_AVX     (XCR0_FLAG_SSE | XCR0_FLAG_AVX)
  3. #define XCR_FLAGS_AVX512  (XCR_FLAGS_AVX | XCR0_FLAG_OPMASK | XCR0_FLAG_ZMM_HI256 | XCR0_FLAG_HI16_ZMM)
  4.  
В заголовочном файле include/dsp/x86/cpuid.h также объявляем прототип функции чтения регистров XCR:
Код (C):
  1.  
  2. uint64_t read_xcr(umword_t xcr_id);
  3.  
Саму же реализацию этой функции поместим в файл include/dsp/x86/avx.h:
Код (C):
  1.  
  2. #ifdef __AVX__
  3.     uint64_t read_xcr(umword_t xcr_id)
  4.     {
  5.         uint64_t xcr;
  6.  
  7.         __asm__ __volatile__
  8.         (
  9.             __ASM_EMIT64("xor       %%rax, %%rax")
  10.             __ASM_EMIT64("xor       %%rdx, %%rdx")
  11.             __ASM_EMIT("xgetbv")
  12.             __ASM_EMIT64("shl       $32, %%rdx")
  13.             __ASM_EMIT64("or        %%rdx, %%rax")
  14.             : __IF_32("=A" (xcr)) __IF_64("=a" (xcr))
  15.             : "c" (xcr_id)
  16.             : __IF_64("%rdx")
  17.         );
  18.         return xcr;
  19.     }
  20. #else
  21.     uint64_t read_xcr(umword_t xcr)
  22.     {
  23.         return 0;
  24.     }
  25. #endif /* __AVX__ */
  26.  
Как видно, инструкция XGETBV принимает в качестве аргумента в регистре EAX номер XCR-регистра, а значение прочитанного XCR-регистра сохраняет в регистровой паре EDX:EAX. Поэтому Код для 32-разрядной и 64-разрядной архитектуры у нас также отличается.

При этом, включение заголовочного файла организуем в src/dsp/x86/avx.cpp, который будет компилироваться с ключём поддержки AVX:
Код (C):
  1.  
  2. #include <dsp.h>
  3.  
  4. #define DSP_X86_AVX_IMPL
  5.  
  6. namespace dsp
  7. {
  8.     namespace x86
  9.     {
  10.         #include <dsp/x86/avx/xcr.h>
  11.     }
  12. }
  13.  
  14. #undef DSP_X86_AVX_IMPL
  15.  
Поддержка большинства расширений AVX проверяется при помощи функции 7 инструкции CPUID, поэтому объявляем константы для интересующих нас расширений:
Код (C):
  1.  
  2. // Function 7
  3. #define X86_CPUID7_INTEL_EBX_AVX2               (1 << 5)
  4. #define X86_CPUID7_INTEL_EBX_AVX512F            (1 << 16)
  5. #define X86_CPUID7_INTEL_EBX_AVX512DQ           (1 << 17)
  6. #define X86_CPUID7_INTEL_EBX_AVX512IFMA         (1 << 21)
  7. #define X86_CPUID7_INTEL_EBX_AVX512PF           (1 << 26)
  8. #define X86_CPUID7_INTEL_EBX_AVX512ER           (1 << 27)
  9. #define X86_CPUID7_INTEL_EBX_AVX512CD           (1 << 28)
  10. #define X86_CPUID7_INTEL_EBX_AVX512BW           (1 << 30)
  11. #define X86_CPUID7_INTEL_EBX_AVX512VL           (1 << 31)
  12.  
  13. #define X86_CPUID7_INTEL_ECX_AVX512VBMI         (1 << 1)
  14.  
  15. #define X86_CPUID7_AMD_EBX_AVX2                 (1 << 5)
  16.  
Теперь, наконец, мы готовы написать реализацию функций do_intel_cpuid и do_amd_cpuid:
Код (C):
  1.  
  2.         void do_intel_cpuid(cpu_features_t *f, size_t max_cpuid, size_t max_ext_cpuid)
  3.         {
  4.             cpuid_info_t info;
  5.             uint64_t xcr0 = 0;
  6.  
  7.             // FUNCTION 1
  8.             if (max_cpuid >= 1)
  9.             {
  10.                 cpuid(&info, 1, 0);
  11.  
  12.                 if (info.edx & X86_CPUID1_INTEL_EDX_FPU)
  13.                     f->features     |= CPU_OPTION_FPU;
  14.                 if (info.edx & X86_CPUID1_INTEL_EDX_CMOV)
  15.                     f->features     |= CPU_OPTION_CMOV;
  16.                 if (info.edx & X86_CPUID1_INTEL_EDX_MMX)
  17.                     f->features     |= CPU_OPTION_MMX;
  18.                 if (info.edx & X86_CPUID1_INTEL_EDX_SSE)
  19.                     f->features     |= CPU_OPTION_SSE;
  20.                 if (info.edx & X86_CPUID1_INTEL_EDX_SSE2)
  21.                     f->features     |= CPU_OPTION_SSE2;
  22.  
  23.                 if (info.ecx & X86_CPUID1_INTEL_ECX_SSE3)
  24.                     f->features     |= CPU_OPTION_SSE3;
  25.                 if (info.ecx & X86_CPUID1_INTEL_ECX_SSSE3)
  26.                     f->features     |= CPU_OPTION_SSSE3;
  27.                 if (info.ecx & X86_CPUID1_INTEL_ECX_SSE4_1)
  28.                     f->features     |= CPU_OPTION_SSE4_1;
  29.                 if (info.ecx & X86_CPUID1_INTEL_ECX_SSE4_2)
  30.                     f->features     |= CPU_OPTION_SSE4_2;
  31.                 if (info.ecx & X86_CPUID1_INTEL_ECX_XSAVE)
  32.                     f->features     |= CPU_OPTION_FXSAVE;
  33.                 if (info.ecx & X86_CPUID1_INTEL_ECX_OSXSAVE)
  34.                 {
  35.                     f->features     |= CPU_OPTION_OSXSAVE;
  36.  
  37.                     xcr0         = read_xcr(0);
  38.  
  39.                     // Additional check for AVX support
  40.                     if ((xcr0 & XCR_FLAGS_AVX) == XCR_FLAGS_AVX)
  41.                     {
  42.                         if (info.ecx & X86_CPUID1_INTEL_ECX_FMA3)
  43.                             f->features     |= CPU_OPTION_FMA3;
  44.                         if (info.ecx & X86_CPUID1_INTEL_ECX_AVX)
  45.                             f->features     |= CPU_OPTION_AVX;
  46.                     }
  47.                 }
  48.             }
  49.  
  50.             // FUNCTION 7
  51.             if (max_cpuid >= 7)
  52.             {
  53.                 cpuid(&info, 7, 0);
  54.  
  55.                 if (f->features & CPU_OPTION_OSXSAVE)
  56.                 {
  57.                     if ((xcr0 & XCR_FLAGS_AVX) == XCR_FLAGS_AVX)
  58.                     {
  59.                         if (info.ebx & X86_CPUID7_INTEL_EBX_AVX2)
  60.                             f->features     |= CPU_OPTION_AVX2;
  61.                     }
  62.  
  63.                     if ((xcr0 & XCR_FLAGS_AVX512) == XCR_FLAGS_AVX512)
  64.                     {
  65.                         if (info.ebx & X86_CPUID7_INTEL_EBX_AVX512F)
  66.                             f->features     |= CPU_OPTION_AVX512F;
  67.                         if (info.ebx & X86_CPUID7_INTEL_EBX_AVX512DQ)
  68.                             f->features     |= CPU_OPTION_AVX512DQ;
  69.                         if (info.ebx & X86_CPUID7_INTEL_EBX_AVX512IFMA)
  70.                             f->features     |= CPU_OPTION_AVX512IFMA;
  71.                         if (info.ebx & X86_CPUID7_INTEL_EBX_AVX512PF)
  72.                             f->features     |= CPU_OPTION_AVX512PF;
  73.                         if (info.ebx & X86_CPUID7_INTEL_EBX_AVX512ER)
  74.                             f->features     |= CPU_OPTION_AVX512ER;
  75.                         if (info.ebx & X86_CPUID7_INTEL_EBX_AVX512CD)
  76.                             f->features     |= CPU_OPTION_AVX512CD;
  77.                         if (info.ebx & X86_CPUID7_INTEL_EBX_AVX512BW)
  78.                             f->features     |= CPU_OPTION_AVX512BW;
  79.                         if (info.ebx & X86_CPUID7_INTEL_EBX_AVX512VL)
  80.                             f->features     |= CPU_OPTION_AVX512VL;
  81.  
  82.                         if (info.ecx & X86_CPUID7_INTEL_ECX_AVX512VBMI)
  83.                             f->features     |= CPU_OPTION_AVX512VBMI;
  84.                     }
  85.                 }
  86.             }
  87.         }
  88.  
  89.         void do_amd_cpuid(cpu_features_t *f, size_t max_cpuid, size_t max_ext_cpuid)
  90.         {
  91.             cpuid_info_t info;
  92.             uint64_t xcr0 = 0;
  93.  
  94.             // FUNCTION 1
  95.             if (max_cpuid >= 1)
  96.             {
  97.                 cpuid(&info, 1, 0);
  98.  
  99.                 if (info.edx & X86_CPUID1_AMD_EDX_FPU)
  100.                     f->features     |= CPU_OPTION_FPU;
  101.                 if (info.edx & X86_CPUID1_AMD_EDX_CMOV)
  102.                     f->features     |= CPU_OPTION_CMOV;
  103.                 if (info.edx & X86_CPUID1_AMD_EDX_MMX)
  104.                     f->features     |= CPU_OPTION_MMX;
  105.                 if (info.edx & X86_CPUID1_AMD_EDX_SSE)
  106.                     f->features     |= CPU_OPTION_SSE;
  107.                 if (info.edx & X86_CPUID1_AMD_EDX_SSE2)
  108.                     f->features     |= CPU_OPTION_SSE2;
  109.  
  110.                 if (info.ecx & X86_CPUID1_AMD_ECX_SSE3)
  111.                     f->features     |= CPU_OPTION_SSE3;
  112.                 if (info.ecx & X86_CPUID1_AMD_ECX_SSSE3)
  113.                     f->features     |= CPU_OPTION_SSSE3;
  114.                 if (info.ecx & X86_CPUID1_AMD_ECX_SSE4_1)
  115.                     f->features     |= CPU_OPTION_SSE4_1;
  116.                 if (info.ecx & X86_CPUID1_AMD_ECX_SSE4_2)
  117.                     f->features     |= CPU_OPTION_SSE4_2;
  118.                 if (info.ecx & X86_CPUID1_AMD_ECX_XSAVE)
  119.                     f->features     |= CPU_OPTION_FXSAVE;
  120.                 if (info.ecx & X86_CPUID1_AMD_ECX_OSXSAVE)
  121.                 {
  122.                     f->features     |= CPU_OPTION_OSXSAVE;
  123.  
  124.                     uint64_t xcr0 = read_xcr(0);
  125.  
  126.                     // Additional check for AVX support
  127.                     if ((xcr0 & XCR_FLAGS_AVX) == XCR_FLAGS_AVX)
  128.                     {
  129.                         if (info.ecx & X86_CPUID1_AMD_ECX_FMA3)
  130.                             f->features     |= CPU_OPTION_FMA3;
  131.                         if (info.ecx & X86_CPUID1_AMD_ECX_AVX)
  132.                             f->features     |= CPU_OPTION_AVX;
  133.                     }
  134.                 }
  135.             }
  136.  
  137.             // FUNCTION 7
  138.             if (max_cpuid >= 7)
  139.             {
  140.                 cpuid(&info, 7, 0);
  141.  
  142.                 if (info.ebx & X86_CPUID7_AMD_EBX_AVX2)
  143.                     f->features     |= CPU_OPTION_AVX2;
  144.             }
  145.  
  146.             // FUNCTION 0x80000001
  147.             if (max_ext_cpuid >= 0x80000001)
  148.             {
  149.                 cpuid(&info, 0x80000001, 0);
  150.  
  151.                 if (info.ecx & X86_XCPUID1_AMD_ECX_SSE4A)
  152.                     f->features     |= CPU_OPTION_SSE4A;
  153.  
  154.                 if (info.edx & X86_XCPUID1_AMD_EDX_FPU)
  155.                     f->features     |= CPU_OPTION_FPU;
  156.                 if (info.edx & X86_XCPUID1_AMD_EDX_CMOV)
  157.                     f->features     |= CPU_OPTION_CMOV;
  158.                 if (info.edx & X86_XCPUID1_AMD_EDX_MMX)
  159.                     f->features     |= CPU_OPTION_MMX;
  160.  
  161.                 if (f->features & CPU_OPTION_OSXSAVE)
  162.                 {
  163.                     if ((xcr0 & XCR_FLAGS_AVX) == XCR_FLAGS_AVX)
  164.                     {
  165.                         if (info.ecx & X86_XCPUID1_AMD_ECX_FMA4)
  166.                             f->features     |= CPU_OPTION_FMA4;
  167.                     }
  168.                 }
  169.             }
  170.         }
  171.  
Обе функции сохраняют в поле features нашей структуры флаги поддержки интересующих нас расширений. При этом, если то или иное расширение не поддерживается операционной системой, соответствующий ему флаг не будет установлен, что позволяет избежать дополнительных проверок впоследствии.

0 2.280
SadKo

SadKo
Владимир Садовников

Регистрация:
4 июн 2007
Публикаций:
8