Встроенные 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или или bdver4Exavator 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. Код сборки доступен по адресу:

https://software.intel.com/en-us/articles/intel-digital-random-number-generator-drng-software-implementation-guide

Для компилятора 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

Другие вопросы по тегам