Вопрос довольно широкий, потому что есть несколько способов получить доступ к мощности базового оборудования, поэтому вместо одного конкретного способа приведен список способов, которыми вы можете попытаться использовать все инструкции современных процессоров :
Распознавание идиом
Запишите операцию, не предлагаемую непосредственно в C ++, в «длинной форме» и надеемся, что ваш компилятор распознает ее как идиому для базовой инструкции, которую вы хотите.Например, вы можете написать переменную поворота влево от x
на amount
как (x << amount) | (x >> (32 - amount))
, и все gcc, clang и icc распознают это как вращение и выдадут базовую инструкцию rol
поддерживается x86.
Иногда этот метод ставит вас в неудобное положение: вышеописанная реализация поворота C ++ демонстрирует неопределенное поведение для amount == 0
(а также amount >= 32
), посколькурезультат смещения 32 на uint32_t
не определен, но код на самом деле , создаваемый этими компиляторами, в этом случае вполне подходит.Тем не менее, иметь такое скрытое неопределенное поведение в вашей программе опасно, и, вероятно, оно не будет ясно против убсана и друзей.Альтернативная безопасная версия amount ? (x << amount) | (x >> (32 - amount)) : x;
распознается только icc, но не gcc или clang.
Этот подход имеет тенденцию работать для распространенных идиом, которые отображаются непосредственно на инструкции уровня сборки, которые существовали некоторое время:вращает, проверяет и устанавливает биты, умножения с более широким результатом, чем входные данные (например, умножение двух 32-битных значений для 64-битного результата), условные перемещения и т. д., но с меньшей вероятностью выберут передовые инструкции, которые также могутпредставлять интерес для криптографии.Например, я совершенно уверен, что ни один компилятор в настоящее время не распознает применение расширений набора команд AES *1028*.Он также лучше всего работает на платформах, которые получили много усилий со стороны разработчиков компиляторов, поскольку каждую распознанную идиому нужно добавлять вручную.
Я не думаю, что этот метод будет работать с вашим переносчиком.меньше умножения ( PCLMULQDQ ), но, может быть, за один день (если вы подаете проблему в отношении компиляторов)?Он работает и для других «крипто-интересных» функций, включая rotate.
Встроенные функции
Поскольку компиляторы расширений часто предлагают встроенных функций , которые не являются частьюязык сам по себе, но часто отображается непосредственно на инструкции, предлагаемые большинством аппаратных средств.Хотя это выглядит как вызов функции, компилятор обычно просто выдает единственную инструкцию, необходимую в месте, где вы ее вызываете.
GCC вызывает эти встроенные функции , и вы можете найти список общих функций здесь .Например, вы можете использовать вызов __builtin_popcnt
для выдачи инструкции popcnt
, , если текущая цель поддерживает ее .Man из встроенных в gcc также поддерживается icc и clang, и в этом случае все gcc, clang и icc поддерживают этот вызов и генерируют popcnt
, пока установлена архитектура (-march=Haswell
)в Haswell.В противном случае, clang и icc вставляют заменяющую версию, используя некоторые хитрые трюки SWAR, в то время как gcc вызывает __popcountdi2
, что обеспечивается средой выполнения 1 .
Приведенный выше список встроенных функций является общим и обычноНа любой платформе предлагается поддержка компиляторов.Вы также можете найти специфичные для платформы инстанции, например, этот список от gcc.
Специально для инструкций x86 SIMD Intel предоставляет набор встроенных функций объявленных заголовков, охватывающихих расширения ISA, например, включив #include <x86intrin.h>
.Они имеют более широкую поддержку, чем экземпляры gcc, например, они поддерживаются пакетом компиляторов Microsoft Visual Studio.Новые наборы инструкций обычно добавляются до того, как чипы, поддерживающие их, становятся доступными, поэтому вы можете использовать их для доступа к новым инструкциям сразу после выпуска.
Программирование с помощью встроенных функций SIMD является своего рода промежуточным звеном между C ++ и полной сборкой.Компилятор по-прежнему заботится о таких вещах, как соглашения о вызовах и распределение регистров, и выполняется некоторая оптимизация (особенно для генерации констант и других широковещательных рассылок), но в целом то, что вы пишете, более или менее то, что вы получаете при сборке.level.
Встроенная сборка
Если ваш компилятор предлагает это, вы можете использовать встроенную сборку для вызова любых необходимых вам инструкций 2 .Это имеет много общего с использованием встроенных функций, но с несколько более высоким уровнем сложности и меньшими возможностями для оптимизатора, чтобы помочь вам.Вы должны , вероятно, предпочесть встроенные функции, если у вас нет особых причин для встроенной сборки.Одним из примеров может быть, если оптимизатор действительно плохо справляется со встроенными функциями: вы можете использовать встроенный блок сборки, чтобы получить именно тот код, который вы хотите.
Сборка вне линии
Вы также можете просто написать всю свою функцию ядра в сборке, собрать ее так, как вы хотите, а затем объявить ее extern "C"
и вызвать ее из C ++.Это похоже на параметр встроенной сборки, но работает на компиляторах, которые не поддерживают встроенную сборку (например, 64-разрядная Visual Studio).При желании вы также можете использовать другой ассемблер, что особенно удобно, если вы нацеливаетесь на несколько компиляторов C ++, поскольку затем вы можете использовать один ассемблер для всех них.
Необходимо соблюдать соглашения о вызовахвы и другие беспорядочные вещи, такие как DWARF, информация о раскрутке и Обработка Windows SEH .
Для очень коротких функций этот подход не работает хорошо, так как накладные расходы на вызоввероятно, будет непомерно 3 .
Автоматическая векторизация 4
Если вы хотите сегодня написать быструю криптографию для процессора, вы в значительной степени собираетесьориентироваться в основном на SIMD инструкции.Большинство новых алгоритмов, разработанных с помощью программной реализации, также разработаны с учетом векторизации.
Вы можете встроить функции или ассемблер для написания SIMD-кода, но вы также можете написать обычный скалярный код и полагаться на auto-vectorizer.Они получили дурную славу в первые дни SIMD, и хотя они еще далеки от совершенства, они прошли долгий путь.
Рассмотрим эту простую функцию, которая принимает payload
и key
байтовый массив и xors key
в полезную нагрузку:
void otp_scramble(uint8_t* payload, uint8_t* key, size_t n) {
for (size_t i = 0; i < n; i++) {
payload[i] ^= key[i];
}
}
Это пример софтбола, конечно, но в любом случае gcc, clang и icc все векторизуют это в что-то вроде этого внутреннего цикла 4 :
movdqu xmm0, XMMWORD PTR [rdi+rax]
movdqu xmm1, XMMWORD PTR [rsi+rax]
pxor xmm0, xmm1
movups XMMWORD PTR [rdi+rax], xmm0
Он использует инструкции SSE для загрузки и xor 16 байтов за раз.Тем не менее, разработчик должен только подумать о простом скалярном коде!
Одно из преимуществ этого подхода по сравнению со встроенными функциями или сборкой заключается в том, что вы не выпекаете длину SIMD набора команд на уровне источника.Тот же код C ++, что и выше, скомпилированный с -march=haswell
, приводит к циклу, подобному:
vmovdqu ymm1, YMMWORD PTR [rdi+rax]
vpxor ymm0, ymm1, YMMWORD PTR [rsi+rax]
vmovdqu YMMWORD PTR [rdi+rax], ymm0
Он использует инструкции AVX2, доступные на Haswell, для выполнения 32 байтов за раз.Если вы компилируете с -march=skylake-avx512
, то clang использует 64-байтовые vxorps
инструкции для zmm
регистров (но gcc и icc придерживаются 32-байтовых внутренних циклов).Таким образом, в принципе, вы можете воспользоваться некоторыми преимуществами нового ISA просто с помощью перекомпиляции.
Недостаток auto-vectorizatoin в том, что он довольно хрупок.То, что автоматически векторизуется на одном компиляторе, может не на другом или даже на другой версии того же компилятора.Так что вам нужно проверить, что вы получаете желаемые результаты.Авто-векторизатор часто работает с меньшим количеством информации, чем у вас: он может не знать, что длина ввода кратна некоторой степени или двум, или что указатели ввода выровнены определенным образом.Иногда вы можете передать эту информацию компилятору, но иногда нет.
Иногда компилятор принимает «интересные» решения, когда он векторизуется, например, небольшое не развернутое тело для внутреннего цикла, а затем гигантское «intro» или «outro», обрабатывающее нечетные итерации, как то, что gcc
производит послепервый цикл, показанный выше:
movzx ecx, BYTE PTR [rsi+rax]
xor BYTE PTR [rdi+rax], cl
lea rcx, [rax+1]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+1+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+2]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+2+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+3]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+3+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+4]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+4+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+5]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+5+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+6]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+6+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+7]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+7+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+8]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+8+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+9]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+9+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+10]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+10+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+11]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+11+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+12]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+12+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+13]
cmp rdx, rcx
jbe .L1
movzx r8d, BYTE PTR [rsi+13+rax]
xor BYTE PTR [rdi+rcx], r8b
lea rcx, [rax+14]
cmp rdx, rcx
jbe .L1
movzx eax, BYTE PTR [rsi+14+rax]
xor BYTE PTR [rdi+rcx], al
Возможно, у вас есть лучшие вещи, на которые вы можете потратить свой кэш инструкций (и это далеко не самое худшее, что я видел: легко получить примеры с несколькими сотнями инструкций вчасти intro и outro).
К сожалению, векторизатор, вероятно, не будет выдавать специфичные для криптоинструкции инструкции, такие как умножение без переноса.Вы могли бы рассмотреть сочетание скалярного кода, который становится векторизованным и присущим только инструкциям, которые компилятор не будет генерировать, но это легче предложить, чем фактически сделать успешно.В этот момент вам, вероятно, лучше написать весь свой цикл с помощью встроенных функций.
1 Преимущество подхода gcc заключается в том, что во время выполнения , еслиПлатформа поддерживает popcnt
, этот вызов может разрешить реализацию, которая просто использует инструкцию popcnt
, используя механизм GNU IFUNC .
2 Предполагая базовый ассемблерподдерживает это, но даже если это не так, вы можете просто закодировать необработанные байты инструкции во встроенном блоке сборки.
3 Затраты на вызовы включают в себя не только явные затраты на call
и ret
и передачу аргументов: это также влияет на оптимизатор, который не может оптимизировать кодтакже в вызывающей стороне вокруг вызова функции, так как она имеет неизвестные побочные эффекты.
4 В некоторых случаях автоматическая векторизация может рассматриваться как особый случай распознавания идиом, но этоявляется достаточно важным и имеет достаточно уникальных соображений, чтобы получить здесь свой собственный раздел.
5 С небольшими отличиями: gcc, как показано, clang развернул немного, и icc использовал load-oppxor
, а не отдельная нагрузка.