Встроенные RDRAND и RDSEED GCC и Intel C++
Поддерживает ли компилятор Intel C++ и / или GCC следующие встроенные функции, как MSVC с 2012 / 2013?
int _rdrand16_step(uint16_t*);
int _rdrand32_step(uint32_t*);
int _rdrand64_step(uint64_t*);
int _rdseed16_step(uint16_t*);
int _rdseed32_step(uint32_t*);
int _rdseed64_step(uint64_t*);
И если эти встроенные функции поддерживаются, то с какой версии они поддерживаются (с постоянной времени компиляции, пожалуйста)?
3 ответа
И GCC, и компилятор Intel поддерживают их. Поддержка GCC была введена в конце 2010 года. Для них требуется заголовок <immintrin.h>
,
Поддержка GCC присутствует, по крайней мере, с версии 4.6, но не существует какой-либо конкретной константы времени компиляции - вы можете просто проверить __GNUC_MAJOR__ > 4 || (__GNUC_MAJOR__ == 4 && __GNUC_MINOR__ >= 6)
,
Все основные компиляторы поддерживают встроенные функции Intel дляа такжес помощью
<immintrin.h>
.
Для , например, GCC9 (2019) или clang7 (2018), необходимы несколько свежие версии некоторых компиляторов, хотя они уже давно стабильны. Если вы предпочитаете использовать более старый компилятор или не включать параметры расширения ISA, такие как , то хорошим выбором будет функция-оболочка из библиотеки 1 вместо встроенной. (Встроенный ассемблер не нужен, я бы не рекомендовал его, если вы не хотите играть с ним.)
#include <immintrin.h>
#include <stdint.h>
// gcc -march=native or haswell or znver1 or whatever, or manually enable -mrdrnd
uint64_t rdrand64(){
unsigned long long ret; // not uint64_t, GCC/clang wouldn't compile.
do{}while( !_rdrand64_step(&ret) ); // retry until success.
return ret;
}
// and equivalent for _rdseed64_step
// and 32 and 16-bit sizes with unsigned and unsigned short.
Некоторые компиляторы определяют, когда инструкция включена во время компиляции. GCC/clang, так как они вообще поддерживали встроенный, но только гораздо более поздний ICC (19.0). И с ICC не подразумевает и не определяет
__RDRND__
до 2021.1.
ICX основан на LLVM и ведет себя как clang.
MSVC не определяет никаких макросов; его обработка встроенных функций предназначена только для обнаружения функций во время выполнения, в отличие от gcc/clang, где проще всего использовать параметры функций ЦП во время компиляции.
Почему вместо
while(){}
? Оказывается, ICC компилируется в менее тупой цикл с , а не бесполезно очищает первую итерацию. Другие компиляторы не выигрывают от такой поддержки, и для ICC это не проблема корректности.
Почему вместо ? Тип должен согласовываться с типом указателя, ожидаемым встроенным, иначе компиляторы C и особенно C++ будут жаловаться, независимо от того, идентичны ли представления объектов (64-битные беззнаковые). В Linux, например,
uint64_t
есть , но GCC/clang
immintrin.h
определять
int _rdrand64_step(unsigned long long*)
, как и в Windows. Так что вам всегда нужно
unsigned long long ret
с GCC/лязг. MSVC не является проблемой, поскольку он может (насколько мне известно) ориентироваться только на Windows, где
unsigned long long
является единственным 64-битным беззнаковым типом.
Но ICC определяет внутреннее как принятие
unsigned long*
при компиляции для GNU/Linux, согласно моему тестированию на https://godbolt.org/. Итак, чтобы быть переносимым в ICC, вам действительно нужно
#ifdef __INTEL_COMPILER
; даже в С++ я не знаю, как использовать
auto
или другой вывод типа для объявления переменной, которая ему соответствует.
Версии компилятора для поддержки встроенных функций
Протестировано на Godbolt; его самая ранняя версия MSVC — 2015, а ICC — 2013, поэтому я не могу вернуться назад. Поддержка для
_rdrand16_step
/ 32 / 64 были введены одновременно в любом данном компиляторе. 64 требуется 64-битный режим.
Самые ранние версии GCC и clang не распознают только файлы . (GCC 4.9 и clang 3.6 для Ivy Bridge, а не то, что вы специально хотите использовать IvyBridge, если современные ЦП более актуальны. Поэтому используйте недревний компилятор и установите параметр ЦП, соответствующий процессорам, которые вам действительно нужны, или, по крайней мере,
-mtune=
с более новым процессором.)
Все новые компиляторы Intel oneAPI / ICX поддерживают / и основаны на внутренних компонентах LLVM, поэтому они работают аналогично clang для параметров ЦП. (не определяет
__INTEL_COMPILER
, что хорошо, поскольку отличается от ICC.)
GCC и clang позволяют вам использовать встроенные функции только для инструкций, которые вы сообщили компилятору, которые поддерживает цель. Использовать
-march=native
если компилируете для своей машины или используете
-march=skylake
или что-то, чтобы включить все расширения ISA для ЦП, на который вы ориентируетесь. Но если вам нужно, чтобы ваша программа работала на старых ЦП и использовала только RDRAND или RDSEED после обнаружения во время выполнения, нужны только эти функции.
__attribute__((target("rdrnd")))
или и не сможет встраиваться в функции с другими целевыми параметрами. Или было бы проще использовать отдельно скомпилированную библиотеку 1.
- : включено
-march=ivybridge
или илиbdver4
Exavator APU) и позже -
-mrdseed
: включено-march=broadwell
или же-march=znver1
или позже
Обычно, если вы собираетесь включить одну функцию ЦП, имеет смысл включить другие, которые будут иметь ЦП этого поколения, и установить параметры настройки. Но это не то, что компилятор будет использовать сам по себе (в отличие от BMI2
shlx
для более эффективного смещения числа переменных или AVX/SSE для автоматической векторизации, копирования и инициализации массива/структуры). Таким образом, позволяя
-mrdrnd
глобально, вероятно, не приведет к сбою вашей программы на процессорах до Ivy Bridge, если вы проверите функции процессора и на самом деле не запустите код, который использует
_rdrand64_step
на процессорах без этой функции.
Но если вы собираетесь запускать свой код только на каком-то конкретном процессоре или более поздней версии,
gcc -O3 -march=haswell
хороший выбор. (
-march
также подразумевает
-mtune=haswell
, и настройка специально для Ivy Bridge - это не то, что вам нужно для современных процессоров. Ты мог
-march=ivybridge -mtune=skylake
чтобы установить более старый базовый уровень функций ЦП, но все же настроить для более новых ЦП.)
Обертки, которые компилируются везде
Это допустимо для C++ и C. Для C вы, вероятно, захотите
static inline
вместо
inline
поэтому вам не нужно вручную создавать экземпляр
extern inline
версия в
.c
на случай, если отладочная сборка решила не встраиваться. (Или используйте
__attribute__((always_inline))
в GNU C.)
64-битные версии определены только для целей x86-64, потому что инструкции asm могут использовать только 64-битный размер операнда в 64-битном режиме. я не
#ifdef __RDRND__
или же
#if defined(__i386__)||defined(__x86_64__)
, при условии, что вы вообще включите это только для сборок x86(-64), не загромождая ifdefs больше, чем необходимо. Он определяет оболочки только в том случае, если они включены во время компиляции или для MSVC, где нет возможности включить их или обнаружить.
Есть некоторые комментарии
__attribute__((target("rdseed")))
примеры, которые вы можете раскомментировать, если хотите, вместо параметров компилятора.
rdrand16
/
rdseed16
намеренно опущены как обычно бесполезные. работает с одинаковой скоростью для разных размеров операндов и даже извлекает одинаковый объем данных из внутреннего буфера процессора RNG, при желании отбрасывая часть его для вас.
#include <immintrin.h>
#include <stdint.h>
#if defined(__x86_64__) || defined (_M_X64)
// Figure out which 64-bit type the output arg uses
#ifdef __INTEL_COMPILER // Intel declares the output arg type differently from everyone(?) else
// ICC for Linux declares rdrand's output as unsigned long, but must be long long for a Windows ABI
typedef uint64_t intrin_u64;
#else
// GCC/clang headers declare it as unsigned long long even for Linux where long is 64-bit, but uint64_t is unsigned long and not compatible
typedef unsigned long long intrin_u64;
#endif
//#if defined(__RDRND__) || defined(_MSC_VER) // conditional definition if you want
inline
uint64_t rdrand64(){
intrin_u64 ret;
do{}while( !_rdrand64_step(&ret) ); // retry until success.
return ret;
}
//#endif
#if defined(__RDSEED__) || defined(_MSC_VER)
inline
uint64_t rdseed64(){
intrin_u64 ret;
do{}while( !_rdseed64_step(&ret) ); // retry until success.
return ret;
}
#endif // RDSEED
#endif // x86-64
//__attribute__((target("rdrnd")))
inline
uint32_t rdrand32(){
unsigned ret; // Intel documents this as unsigned int, not necessarily uint32_t
do{}while( !_rdrand32_step(&ret) ); // retry until success.
return ret;
}
#if defined(__RDSEED__) || defined(_MSC_VER)
//__attribute__((target("rdseed")))
inline
uint32_t rdseed32(){
unsigned ret; // Intel documents this as unsigned int, not necessarily uint32_t
do{}while( !_rdseed32_step(&ret) ); // retry until success.
return ret;
}
#endif
Тот факт, что внутренний API Intel вообще поддерживается, подразумевает, что это 32-битный тип, независимо от того,
uint32_t
определяется как
unsigned int
или же
unsigned long
если это сделают какие-либо компиляторы.
В обозревателе компиляторов Godbolt мы можем увидеть, как они компилируются. Clang и MSVC делают то, что мы ожидаем, просто цикл из 2 инструкций, пока не останется CF=1.
# clang 7.0 -O3 -march=broadwell MSVC -O2 does the same.
rdrand64():
.LBB0_1: # =>This Inner Loop Header: Depth=1
rdrand rax
jae .LBB0_1 # synonym for jnc - jump if Not Carry
ret
# same for other functions.
К сожалению, GCC не так хорош, даже текущий GCC12.1 делает странный ассемблер:
# gcc 12.1 -O3 -march=broadwell
rdrand64():
mov edx, 1
.L2:
rdrand rax
mov QWORD PTR [rsp-8], rax # store into the red-zone where retval is allocated
cmovc eax, edx # materialize a 0 or 1 from CF. (rdrand zeros EAX when it clears CF=0, otherwise copy the 1)
test eax, eax # then test+branch on it
je .L2 # could have just been jnc after rdrand
mov rax, QWORD PTR [rsp-8] # reload retval
ret
rdseed64():
.L7:
rdseed rax
mov QWORD PTR [rsp-8], rax # dead store into the red-zone
jnc .L7
ret
ICC делает то же самое, пока мы используем
do{}while()
повторить цикл; с
while() {}
еще хуже, сделать rdrand и проверить перед входом в цикл в первый раз.
Сноска 1: / библиотечные обертки
или у Intel есть функции-оболочки с циклами повторных попыток, как я показал, и те, которые заполняют буфер байтов или массив байтов.
uint32_t*
или же . (Последовательно принимаю
uint64_t*
, нет
unsigned long long*
по некоторым целям).
Библиотека также является хорошим выбором, если вы выполняете обнаружение функций ЦП во время выполнения, поэтому вам не нужно возиться с
__attribute__((target))
вещи. Как бы вы это ни делали, это в любом случае ограничивает встраивание функции с использованием встроенных функций, поэтому небольшая статическая библиотека эквивалентна.
libdrng
также обеспечивает
RdRand_isSupported()
а также
RdSeed_isSupported()
, поэтому вам не нужно выполнять собственную проверку CPUID.
Но если вы собираетесь строить с
-march=
в любом случае, что-то более новое, чем Ivy Bridge / Broadwell или Excavator / Zen1, встраивание цикла повтора с двумя инструкциями (например, его компилирует clang) имеет примерно тот же размер кода, что и сайт вызова функции, но не стирает никаких регистров. довольно медленный, так что это, вероятно, не имеет большого значения, но это также означает отсутствие дополнительной зависимости от библиотеки.
Производительность / внутренности /
Дополнительные сведения о внутреннем устройстве аппаратного обеспечения Intel (не версии AMD) см . в документации Intel. Также некоторые ответы SO от инженера, который разработал аппаратное обеспечение и написал
librdrand
, такие как это и это о его характеристиках истощения / производительности на Ivy Bridge, первом поколении, в котором он есть.
Бесконечное количество попыток?
Ассемблерные инструкции устанавливают флаг переноса (CF) = 1 в FLAGS в случае успеха, когда он помещает случайное число в регистр назначения. В противном случае CF=0 и выходной регистр = 0. Вы должны вызвать его в цикле повтора, поэтому (я полагаю), почему во встроенном есть слово
step
во имя; это один шаг генерации одного случайного числа.
Теоретически обновление микрокода может что-то изменить, поэтому оно всегда указывает на сбой, например, если в какой-то модели ЦП обнаружена проблема, которая делает ГСЧ ненадежным (по стандартам производителя ЦП). Аппаратный ГСЧ также имеет некоторую самодиагностику, поэтому теоретически ЦП может решить, что ГСЧ неисправен, и не выдать никаких выходных данных. Я не слышал, чтобы какие-либо процессоры когда-либо делали это, но я не искал. И всегда возможно обновление микрокода в будущем.
Любой из них может привести к бесконечному циклу повторных попыток. Это не очень хорошо, но если вы не хотите писать кучу кода, чтобы сообщить об этой ситуации, это, по крайней мере, наблюдаемое поведение, с которым пользователи могли бы иметь дело в маловероятном случае, когда это когда-либо происходило.
Но случайные временные сбои нормальны и ожидаемы, и с ними нужно справляться. Желательно повторить попытку, не сообщая об этом пользователю.
Если в его буфере не было готового случайного числа, ЦП может сообщить об ошибке вместо того, чтобы задерживать это ядро потенциально еще дольше. Этот выбор дизайна может быть связан с задержкой прерывания или просто с упрощением без необходимости встраивать повторные попытки в микрокод.
По словам дизайнера , Ivy Bridge не может извлекать данные из DRNG быстрее, чем это может поддерживаться , даже при зацикливании всех ядер, но более поздние процессоры могут. Поэтому важно повторить попытку.
У @jww был некоторый опыт развертывания в libcrypto++, и он обнаружил, что при слишком низком количестве повторных попыток появлялись сообщения о случайных ложных сбоях. У него были хорошие результаты от бесконечных повторных попыток, поэтому я выбрал это для этого ответа. (Я подозреваю, что он услышал бы сообщения от пользователей со сломанными процессорами, которые всегда выходят из строя, если бы это было так.)
Функции библиотеки Intel, включающие цикл повторных попыток, учитывают количество повторных попыток. Это, вероятно, справится со случаем постоянного сбоя, который, как я уже сказал, я не думаю, что еще случается с какими-либо реальными процессорами . Без ограниченного количества повторных попыток вы зациклились бы навсегда.
Бесконечное количество повторных попыток позволяет простому API возвращать число по значению без глупых ограничений , таких как функции OpenSSL, которые используют как возврат ошибки: они не могут случайным образом генерировать
0
!
Если вам нужно конечное количество повторных попыток, я бы предложил очень большое. Например, 1 миллион, так что может потребоваться секунда или секунда вращения, чтобы отказаться от сломанного процессора, с незначительной вероятностью того, что один поток будет голодать так долго, если ему неоднократно не везет в борьбе за доступ к внутренней очереди.
https://uops.info/ измерил пропускную способность Skylake: один на 3554 цикла на Skylake, один на 1352 на P-ядрах Alder Lake, 1230 на E-ядрах. Один на 1809 циклов на Zen2. Версия Skylake выполняла тысячи моп, остальные исчислялись низкими двузначными числами. У Ivy Bridge пропускная способность была 110 тактов, а у Haswell она была уже до 2436 тактов, но все равно двузначное число uops.
Эти ужасающие показатели производительности на последних процессорах Intel, вероятно, связаны с обновлениями микрокода для решения проблем, которые не ожидались при разработке аппаратного обеспечения. Agner Fog измерил пропускную способность один раз на 460 циклов для и
rdseed
на Skylake, когда он был новым, каждый стоил 16 мкп. Тысячи мопов, вероятно, являются дополнительной очисткой буфера, подключенной к микрокоду для этих инструкций недавними обновлениями. Агнер измерил Haswell со скоростью 17 мкОм, 320 циклов, когда он был новым. См. Производительность RdRand на уровне ~3% от исходной скорости с подавлением перекрестных помех / SRBDS на Phoronix:
Как объяснялось в предыдущей статье, устранение перекрестных помех включает в себя блокировку всей шины памяти перед обновлением промежуточного буфера и разблокировку ее после очистки содержимого. Эта блокировка и сериализация, которые теперь используются для этих инструкций, очень сильно влияют на производительность, но, к счастью, большинство реальных рабочих нагрузок не должны слишком часто использовать эти инструкции.
Блокировка шины памяти звучит так, как будто это может повредить производительности даже других ядер, если это похоже на разделение строк кэша для
lock
инструкции изд.
(Эти числа циклов являются подсчетами тактов ядра; если DRNG не работает на тех же тактовых частотах, что и ядро, они могут различаться в зависимости от модели ЦП. Интересно, выполняется ли тестирование uops.info.
rdrand
на нескольких ядрах одного и того же оборудования, поскольку Coffee Lake в два раза быстрее, чем Skylake, и в 1,4 раза больше циклов на случайное число. Если только более высокие тактовые частоты не приводят к большему количеству повторных попыток микрокода?)
Компилятор Microsoft не имеет встроенной поддержки команд RDSEED и RDRAND.
Но вы можете реализовать эти инструкции, используя NASM или MASM. Код сборки доступен по адресу:
Для компилятора Intel вы можете использовать заголовок для определения версии. Вы можете использовать следующие макросы для определения версии и дополнительной версии:
__INTEL_COMPILER //Major Version
__INTEL_COMPILER_UPDATE // Minor Update.
Например, если вы используете компилятор ICC15.0 Update 3, он покажет, что у вас есть
__INTEL_COMPILER = 1500
__INTEL_COMPILER_UPDATE = 3
Для получения дополнительной информации о предопределенных макросах вы можете перейти по адресу: https://software.intel.com/en-us/node/524490