В 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 ассемблер, в который не нужно добавлять новые инструкции вручную.


Encoder в Go ассемблере


Часть ассемблера, ответственная за генерацию машинного кода, находится в стандартном пакете 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 ассемблер добавлялись вручную, по следующей схеме:


  1. Добавление новой константы в aenum.go.
  2. Добавление optab в глобальную таблицу ассемблера x86.
  3. Подбор или добавление нужного ytab списка.
  4. Добавление end2end тестов для новой инструкции.

Если у нас уже есть все нужные A, Y и Z константы, остаётся генерировать сами таблицы encoder'а и тесты.


Этот процесс неплохо автоматизируется, если у нас есть источник, из которого можно читать информацию об инструкциях: их encoding, типы разрешённых операндов и прочее.
К счастью, у нас есть такой источник.


x86avxgen и Intel XED


Для генерации всех инструкций, которые используют 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 ассемблером


Порядок операндов идентичен 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-суффиксами можно найти в документации.


Дизассемблирование AVX-512


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

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


Одним из крупных пользователей 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 будет затронута в отдельной статье.

Комментарии (11)


  1. kovserg
    08.06.2018 14:14
    +1

    А зачем языку go ассемблер от x86, тем более такой экзотический? Неужели так много программистов желают колупаться с ассемблером в вебе?
    Помимо инструкций есть не тривиальная задача их расположения и организации вычислений и данных. Вот один из вариантов решения подобных проблем halide-lang.org


    1. gibson_dev
      08.06.2018 14:24

      go не одним вебом ограничивается, люди вон софт для видео регистратора писали, да мало ли чего еще…


    1. quasilyte Автор
      08.06.2018 14:27

      Неужели так много программистов желают колупаться с ассемблером в вебе?

      Нет, их мало и они не совсем веб-разработчики. То, что они делают, потом используют в том числе веб-разработчики.

      Ещё одним «пользователем» ассемблера является сам тулчейн.
      Можете пробежаться по статье Архитектура ассемблера Go. Разница лишь в том, что нет этапа парсинга.

      Ассемблер в Go — одно из промежуточных представлений компилятора (низкоуровневый IR).
      Если через 10 лет везде будет AVX-512, то, возможно, вместо 16 векторных регистров он будет использовать 32. Либо у нас будет что-то типа вот этого: GOAMD64 architecture flag и получить преимущества EVEX префикса можно будет значительно раньше.


      1. kovserg
        08.06.2018 15:13

        Может еще cuda префикс добавить? Векторизация это конечно здорово, но не всегда уместно.


        1. quasilyte Автор
          08.06.2018 15:20

          Не всегда != никогда.

          У Go тулчейна есть задача быть максимально независимым от других утилит, в частности внешних ассемблеров.

          Тем более если кому-то векторизация не нужна, он о ней не задумывается.
          А в рантайме Go, может быть, даже применение найдётся.
          Я сейчас не обязательно про новые «модные» инструкции и 512-битные регистры, к которым у многих есть своё отношение, а про приятные дополнения к предыдущим AVX. Насколько мне известно, положительные эксперименты как минимум в пакетах крипты уже были.

          Я не пытаюсь вас переубедить. Просто предоставляю контекст, который может быть полезен и, возможно, ускользнул от вас.


          1. kovserg
            09.06.2018 00:51

            У go задача быть языком для ремесленников с низким порогом входа, а не увеличивать скорость вращения сферических коней в вакууме.


            1. quasilyte Автор
              09.06.2018 01:30

              Go в том числе в датацентрах используется, а там есть понятие tail latency.

              Издержки на него можно сократить, если Go работает быстрее.
              Сделать Go быстрее для всех, без надобности учить людей писать более быстрый код можно с помощью ускорения рантайма и улучшения компилятора (например, научить его использовать дополнительные регистры для уменьшения количества spill'ов, правда это немного надуманный пример).

              Хуже от того, что Go будет становиться более быстрым и/или потреблять меньше памяти, никому не будет.


              1. kovserg
                09.06.2018 15:19

                Быстрый и потребление памяти совершенно не связанные вещи. Современное ПО для решения тех же задач требует на порядки больше ресурсов чем, ПО решавшее эти задачи ранее. Напомню что векторные инструкции предполагают жесткое выравнивание данных, что ведёт к увеличению требуемой памяти.


                1. quasilyte Автор
                  09.06.2018 16:13

                  Если у нас больше регистров, нужно меньше spill'ов в память, если в функции можно обойтись без «локальных слотов» внутри фрейма, фрейм у функции будет меньше или вообще нулевой, тогда при вызовах функции стек будет расти медленнее. Меньше стеки — меньше потребление памяти. Вот это имел в виду выше.

                  Мне не известны ваши познания в этой теме, но в своих я не настолько уверен, чтобы про все векторные инструкции тут говорить обобщая. Есть специальные инструкции, которые не требуют выравнивания, причём в случае, если данные выровнены, они работают не хуже, чем формы, требующие выравнивания. В Go даже стек, насколько помню, не выровнен…

                  Можем закрыть эту тему?
                  Я не понимаю ваших мотивов и того, чего вы хотите.
                  Очевидно, мои ответы вам не помогают/не нравятся.


  1. old_bear
    08.06.2018 18:17

    Оба ассемблера при сборке инструкций, где возможно применить и VEX, и EVEX схемы, используют VEX. Иными словами, VADDPD X1, X2, X3 будет иметь VEX префикс.

    А разве смешение VEX и EVEX инструкций не приводит к падению скорости исполнения кода? Что-то там связанное с отделённостью EVEX-конвейера и частотами…


    1. quasilyte Автор
      08.06.2018 18:46

      Конкретно пример с VADDPD привёл для подчёркивания того, что старый код будет собираться по-старому, с VEX префиксом. В редких ситуациях EVEX префикс мог бы дать небольшой выигрыш в размере машинного кода за счёт compressed displacement, но для более детерменированного поведения и в том числе для обратной совместимости, выбрана цитируемая вами схема. Плюс тем, кто «без словаря» не помнит всех нюансов, спокойнее (например мне).

      AVX-512 и EVEX может появляться только в новом коде, когда программист сознательно запросил данное поведение (использовал новые регистры или суффиксы опкода).

      Способа «переопределить» поведение нет. То есть нельзя попросить тот VADDPD кодировать по схеме AVX-512.