Go 1.11: AVX-512 со вкусом Go
В Go 1.11 значительно обновлён ассемблер под платформу x86.
У программистов появится возможность использовать AVX-512 — новейшие инструкции, доступные в процессорах Intel.
Под катом:
- Самые значительные обновления в
cmd/asm
(go tool asm
) - Как был внедрён новый набор инструкций в Go ассемблер
- Использование новых инструкций и специальных возможностей EVEX префикса
- Уровень интеграции в тулчейн (рецепты обхождения текущих ограничений)
Из видимого для программиста:
- AVX-512
— Новые векторные регистры:X16-X31
,Y16-Y31
иZ0-Z31
— Добавлены регистры масок:K0-K7
— Особые возможности EVEX префикса (см. ниже: rounding, zeroing, …).
— Сотни новых инструкций (379 новых опкодов + AVX{1,2} инструкции с EVEX префиксом). - Добавлено 110 недостающих legacy инструкций (CL97235).
- Почти на 25% более быстрое ассемблирование (CL108895). Ускоряет сборку примерно на 1.5%.
Была также проведена предварительная работа по улучшению сообщений об ошибках (CL108515), но в релиз go1.11 это не попадёт.
Кроме самого факта добавления новых расширений, важно то, что в новом ассемблере все VEX и EVEX таблицы сгенерированы автоматически.
Теперь в Go такой x86 ассемблер, в который не нужно добавлять новые инструкции вручную.
Часть ассемблера, ответственная за генерацию машинного кода, находится в стандартном пакете cmd/internal/obj/x86.
Большая часть кода в нём — это транслированные из C исходники x86 ассемблера из plan9.
Таблицы ассемблера концептуально состоят из 3-х измерений: X, Y и Z.
Конкретная инструкция генерируется как encode(X, Y, Z)
.
Альтернативной ментальной моделью может являться table[X][Y][Z]
, но она менее близка к деталям реализации.
Из пространства опкодов (измерение X) выбирается соответствующий ассемблируемой инструкции объект optab
. Затем перебирается список доступных комбинаций операндов (измерение Y) и выбирается соответствующий аргументам инструкции объект ytab
. Последним шагом является выбор схемы кодогенерации: Z-case.
В коде легко найти константы, которые имеют Y и Z префиксы, но ничего с префиксом X там нет.
Есть гипотеза, что изначально это были A, B и C префиксы, затем B и C переименовали в Y и Z, а опкоды так и остались с префиксом A.
Что также забавно, тип A-констант — obj.As
, что может быть сокращением от asm
(assembler opcode), либо просто означать множественное число A
.
Ранее инструкции в Go x86 ассемблер добавлялись вручную, по следующей схеме:
- Добавление новой константы в aenum.go.
- Добавление
optab
в глобальную таблицу ассемблера x86. - Подбор или добавление нужного
ytab
списка. - Добавление end2end тестов для новой инструкции.
Если у нас уже есть все нужные A, Y и Z константы, остаётся генерировать сами таблицы encoder’а и тесты.
Этот процесс неплохо автоматизируется, если у нас есть источник, из которого можно читать информацию об инструкциях: их encoding, типы разрешённых операндов и прочее.
К счастью, у нас есть такой источник.
Для генерации всех инструкций, которые используют VEX и EVEX префиксы, была написана утилита x86avxgen. Данная программа генерирует те самые optab
и ytab
объекты для ассемблера.
Входными данными для программы являются XED datafiles, работать с которыми из Go можно с помощью пакета xeddata.
Преимущество кодогенерации в том, что для реализации новых инструкций из серии AVX будет достаточно перезапустить x86avxgen
и добавить тестов.
Генерация тестов также автоматизирована с помощью Intel XED encoder’а (XED — это в первую очередь библиотека).
EVEX имеет большое свободное пространство для опкодов и потенциал для расширений, так что новые инструкции точно появятся.
В ближайшее будущее можно подглядывать с помощью документа ISA-extensions.
Кроме самих таблиц кодогенератора был обновлён парсер.
Теперь для x86
можно использовать списки регистров и суффиксы опкодов.
Списки регистров используются для multi-source инструкций, таких как VP4DPWSSD
.
В мануале используется нотация +n
:
VP4DPWSSD zmm1{k1}{z}, zmm2+3, m128
В данном случае +3
означает, что второй zmm
операнд описывает диапазон регистров из 4 элементов (в мануале эти диапазоны именуются «register block»).
Диапазон для Z0+3
в Go ассемблере будет выглядеть следующим образом:
VP4DPWSSD Z25, [Z0-Z3], (AX)
Использование диапазонов типа [Z0-Z1]
, [Z3-Z0]
, [AX-DX]
является ошибкой
этапа ассемблирования.
Суффиксы используются для активации особых возможностей AVX-512.
Например, возьмём одну из новых форм инструкции VADDPD
:
VADDPD zmm1 {k1}{z}, zmm2, zmm3/m512/m64bcst{er}
Сейчас мы разберём, что означает вся эта магия из {k1}
, {z}
, m64bcst
и {er}
.
Обратите внимание: порядок операндов полностью обратен Intel синтаксису.
Точно так же, как и в GNU ассемблере (AT&T синтаксис).
// Без суффиксов, "обычный" VADDPD.
VADDPD (AX), Z30, Z10
// {k1} - merging с использованием маски из K аргумента.
VADDPD (AX), Z30, K5, Z10
// Для всех инструкций ниже можно использовать формы с явным K регистром,
// что будет активировать селективный merging или zeroing.
// {z} - возможен zeroing-mask (без этого суффикса будет merging-mask).
VADDPD.Z (AX), Z30, Z10
// m64bcst - возможность использовать embedded broadcasting.
// Обозначение "bcst" позаимствовано у Microsoft ассемблера (MASM).
VADDPD.BCST (AX), Z30, Z10
// {er} - доступен embedded rounding. Не совместим с memory операндами.
// Округление всегда включает SAE (см. ниже), поэтому суффикс выглядит как составной.
VADDPD.RU_SAE Z0, Z30, Z10 // Округление к +Inf
VADDPD.RD_SAE Z0, Z30, Z10 // Округление к -Inf
VADDPD.RZ_SAE Z0, Z30, Z10 // Округление к нулю
VADDPD.RN_SAE Z0, Z30, Z10 // Округление в "ближайшую сторону"
Что более интересно, суффикс Z
, если инструкция его поддерживает, может использоваться совместно с другими суффиксами:
// SAE - доступен "surpress all exceptions".
// В мануале обозначается меткой {sae}.
VMAXPD.SAE.Z Z3, Z2, Z1
На вопросы вроде «А почему именно так?» может ответить go#22779: AVX512 design.
Рекомендуется также пройтись по приводимой там ссылке на golang-dev.
Порядок операндов идентичен GNU ассемблеру.
Тех, кто застал «странный» порядок операндов в CMP
инструкциях, ждёт новость:
для AVX инструкций эти особые правила не действуют (хорошо это или плохо — решайте сами).
Feature | GNU ассемблер | Go ассемблер |
---|---|---|
Masking | VPORD %ZMM0, %ZMM1, %ZMM2{%K2} {k} всегда у dst операнда |
VPODR Z0, Z1, K2, Z2 {k} всегда перед dst операндом |
Broadcasting | VPORD (%RDX){1to16}, %ZMM1, %ZMM2 1toN у аргумента-памяти |
VPORD.BCST (DX), Z1, Z2 BCST суффикс |
Zeroing | VPORD %ZMM0, %ZMM1, %ZMM2{z} {z} аргумент у dst операнда |
VPORD.Z Z0, Z1, Z2 Z суффикс |
Rounding | VSQRTPD {ru-sae}, %ZMM0, %ZMM1 Особый первый аргумент |
VSQRTPD.RU_SAE Z0, Z1 Суффикс |
SAE | VUCOMISD {sae}, %XMM0, %XMM1 Аналогично rounding |
VUCOMISD.SAE X0, X1 Аналогично rounding |
Multi-source | V4FMADDPS (%RCX), %ZMM4, %ZMM1 Указывается первый регистр |
V4FMADDPS (CX), [Z4-Z7], Z1 Явное указание диапазона |
Оба ассемблера при сборке инструкций, где возможно применить и VEX, и EVEX схемы, используют VEX. Иными словами, VADDPD X1, X2, X3
будет иметь VEX префикс.
В случаях, когда есть неоднозначность размерности операнда, в Go ассемблере опкоды получают дополнительные size-суффиксы:
VCVTSS2USIL (AX), DX // VCVTSS2USI (%RAX), %EDX
VCVTSS2USIQ (AX), DX // VCVTSS2USI (%RAX), %RDX
Там, где в Intel синтаксисе можно указать ширину memory операнда, в GNU и Go ассемблерах используются X
и Y
size-суффиксы:
VCVTTPD2DQX (AX), X0 // VCVTTPD2DQ XMM0, XMMWORD PTR [RAX]
VCVTTPD2DQY (AX), X0 // VCVTTPD2DQ XMM0, YMMWORD PTR [RAX]
Полный список инструкций с size-суффиксами можно найти в документации.
CL113315 добавляет поддержку AVX-512 в go tool asm
, в основном затрагивая парсер и кодогенератор obj/x86
, но что произойдёт, если вы соберёте .s
файл и попробуете исследовать его с помощью go tool objdump
?
// Файл avx.s
TEXT avxCheck(SB), 0, $0
VPOR X0, X1, X2 // AVX1
VPOR Y0, Y1, Y2 // AVX2
VPORD.BCST (DX), Z1, K2, Z2 // AVX-512
RET
Вы увидите не то, что ожидаете:
$ go tool asm avx.s
$ go tool objdump avx.o
TEXT avxCheck(SB) gofile..$GOROOT/avx.s
avx.s:2 0xb7 c5f1ebd0 JMP 0x8b
avx.s:3 0xbb c5f5ebd0 JMP 0x8f
avx.s:4 0xbf 62 ?
avx.s:4 0xc0 f1 ICEBP
avx.s:4 0xc1 755a JNE 0x11d
avx.s:4 0xc3 eb12 JMP 0xd7
avx.s:5 0xc5 c3 RET
Использовать objdump
на объектных файлах Go не получится:
$ objdump -D avx.o
objdump: avx.o: File format not recognized
Но его можно использовать на исполняемых файлах.
Если ассемблерный код включён в main пакет, системный objdump
справится с задачей.
Более простым способом получить машинный код является передача -S
аргумента:
$ go tool asm -S avx.s
avxCheck STEXT nosplit size=15 args=0xffffffff80000000 locals=0x0
0x0000 00000 (avx.s:1) TEXT avxCheck(SB), NOSPLIT, $0
0x0000 00000 (avx.s:2) VPOR X0, X1, X2
0x0004 00004 (avx.s:3) VPOR Y0, Y1, Y2
0x0008 00008 (avx.s:4) VPORD.BCST (DX), Z1, K2, Z2
0x000e 00014 (avx.s:5) RET
0x0000 c5 f1 eb d0 c5 f5 eb d0 62 f1 75 5a eb 12 c3 ........b.uZ...
go.info.avxCheck SDWARFINFO size=34
0x0000 02 61 76 78 43 68 65 63 6b 00 00 00 00 00 00 00 .avxCheck.......
0x0010 00 00 00 00 00 00 00 00 00 00 01 9c 00 00 00 00 ................
0x0020 01 00
Интересующие нас октеты: c5 f1 eb d0 c5 f5 eb d0 62 f1 75 5a eb 12 c3
.
Скопируем их и будем делать реверс через системный objdump
:
# 1. Перегнать текстовое представление в бинарное с помощью xxd.
# 2. Запустить objdump в binary режиме.
# Примечание: для Intel синтаксиса можно вместо "i386" использовать "i386:intel".
$ echo 'c5 f1 eb d0 c5 f5 eb d0 62 f1 75 5a eb 12 c3' |
xxd -r -p > shellcode.bin &&
objdump -b binary -m i386 -D shellcode.bin
Disassembly of section .data:
00000000 <.data>:
0: c5 f1 eb d0 vpor %xmm0,%xmm1,%xmm2
4: c5 f5 eb d0 vpor %ymm0,%ymm1,%ymm2
8: 62 f1 75 5a eb 12 vpord (%edx){1to16},%zmm1,%zmm2{%k2}
e: c3 ret
XED также предоставляет несколько полезных утилит, одна из которых позволяет
использовать encoder/decoder через командную строку.
$ echo 'c5 f1 eb d0 c5 f5 eb d0 62 f1 75 5a eb 12 c3' > data.txt &&
xed -64 -A -ih data.txt && rm data.txt
00 LOGICAL AVX C5F1EBD0 vpor %xmm0, %xmm1, %xmm2
04 LOGICAL AVX2 C5F5EBD0 vpor %ymm0, %ymm1, %ymm2
08 LOGICAL AVX512EVEX 62F1755AEB12 vpordl (%rdx){1to16}, %zmm1, %zmm2{%k2}
0e RET BASE C3 retq
Флаг -A
выбирает AT&T синтаксис, -64
выбирает 64-битный режим.
Утилита xed-ex4
показывает детальную информацию об инструкции:
$ xed-ex4 -64 C5 F1 EB D0
PARSING BYTES: c5 f1 eb d0
VPOR VPOR_XMMdq_XMMdq_XMMdq
EASZ:3,
EOSZ:2,
HAS_MODRM:1,
LZCNT,
MAP:1,
MAX_BYTES:4,
MOD:3,
MODE:2,
MODRM_BYTE:208,
NOMINAL_OPCODE:235,
OUTREG:XMM0,
P4,
POS_MODRM:3,
POS_NOMINAL_OPCODE:2,
REG:2,
REG0:XMM2,
REG1:XMM1,
REG2:XMM0,
SMODE:2,
TZCNT,
VEXDEST210:6,
VEXDEST3,
VEXVALID:1,
VEX_PREFIX:1
0 REG0/W/DQ/EXPLICIT/NT_LOOKUP_FN/XMM_R
1 REG1/R/DQ/EXPLICIT/NT_LOOKUP_FN/XMM_N
2 REG2/R/DQ/EXPLICIT/NT_LOOKUP_FN/XMM_B
YDIS: vpor xmm2, xmm1, xmm0
ATT syntax: vpor %xmm0, %xmm1, %xmm2
INTEL syntax: vpor xmm2, xmm1, xmm0
go tool objdump
основан на x86.csv, который не содержит многих новых инструкций и имеет неточности.
Сам csv файл создан утилитой x86spec на основе конвертации из Intel мануала (PDF).
Следующим шагом может быть создание x86.csv
из таблиц XED’а, что позволит повторно сгенерировать таблицы для decoder’а.
Одним из крупных пользователей AVX-512 в мире Go является minio.
До 1.11 им приходилось использовать утилиту asm2plan9s.
Вот, например, их результаты для sha256:
Processor SIMD Speed (MB/s)
3.0 GHz Intel Xeon Platinum 8124M AVX512 3498
1.2 GHz ARM Cortex-A53 ARM64 638
3.0 GHz Intel Xeon Platinum 8124M AVX2 449
3.1 GHz Intel Core i7 AVX 362
3.1 GHz Intel Core i7 SSE 299
Для того, чтобы начать знакомиться с новым расширением, вы можете попробовать использовать уже знакомые вам по AVX1 и AVX2 инструкции (без Z
регистров). Таким образом вы сможете эксперементировать с новыми возможностями, типа merging/zeroing масок, без риска попасть в полностью новое пространство «особенностей».
Самое главное — измеряйте, перед тем, как делать окончательные выводы. При этом проверяйте как производительность самой функции, так и приложения в целом.
Рекомендую также ознакомиться с golang.org/wiki/AVX-512-support-in-Go-assembler.
Более детально тема эффективного использования AVX-512 будет затронута в отдельной статье.