См. Также более раннюю версию этого ответа на другой вопрос поворота с некоторыми подробностями о том, что asm gcc / clang производит для x86.
Наиболее удобным для компилятора способом выражения поворота в C и C ++, позволяющим избежать любого неопределенного поведения, является реализация Джона Регера . Я адаптировал его для поворота по ширине шрифта (используя типы фиксированной ширины, такие как uint32_t
).
#include <stdint.h> // for uint32_t
#include <limits.h> // for CHAR_BIT
// #define NDEBUG
#include <assert.h>
static inline uint32_t rotl32 (uint32_t n, unsigned int c)
{
const unsigned int mask = (CHAR_BIT*sizeof(n) - 1); // assumes width is a power of 2.
// assert ( (c<=mask) &&"rotate by type width or more");
c &= mask;
return (n<<c) | (n>>( (-c)&mask ));
}
static inline uint32_t rotr32 (uint32_t n, unsigned int c)
{
const unsigned int mask = (CHAR_BIT*sizeof(n) - 1);
// assert ( (c<=mask) &&"rotate by type width or more");
c &= mask;
return (n>>c) | (n<<( (-c)&mask ));
}
Работает для любого целого типа без знака, а не только uint32_t
, поэтому вы можете создавать версии для других размеров.
См. также шаблонную версию C ++ 11 с множеством проверок безопасности (включая static_assert
, что ширина типа равна степени 2) , что не например, для некоторых 24-битных DSP или 36-битных мэйнфреймов.
Я бы порекомендовал использовать шаблон только в качестве серверной части для упаковщиков с именами, в которых явно указана ширина поворота. Правила целочисленного продвижения означают, что rotl_template(u16 & 0x11UL, 7)
будет выполнять 32- или 64-битное вращение, а не 16 (в зависимости от ширины unsigned long
). Даже uint16_t & uint16_t
повышается до signed int
по правилам целочисленного продвижения C ++, за исключением платформ, где int
не шире, чем uint16_t
.
На x86 эта версия встроена в один rol r32, cl
(или rol r32, imm8
) с компиляторами, которые его проглатывают, потому что компилятор знает, что x86 вращается и инструкции сдвига маскируют счет сдвига так же, как это делает источник C.
Поддержка компилятора для этой идиомы избегания UB на x86, для uint32_t x
и unsigned int n
для сдвигов с переменным числом:
- clang: распознается для поворотов с переменным числом, начиная с clang3.5, с несколькими сменами + или insns до этого.
- gcc: распознается для поворотов с переменным числом, начиная с gcc4.9 , с несколькими сменами + или insns до этого. gcc5 и более поздние версии также оптимизируют ветку и маску в версии википедии, используя только инструкцию
ror
или rol
для подсчета переменных.
- icc: поддерживается для вращений с переменным числом, начиная с ICC13 или ранее . Для поворота с постоянным счетом используется
shld edi,edi,7
, который медленнее и занимает больше байтов, чем rol edi,7
на некоторых процессорах (особенно AMD, но также и на некоторых Intel), когда BMI2 недоступен для rorx eax,edi,25
для сохранения MOV.
- MSVC: x86-64 CL19: распознается только для поворотов с постоянным счетом. (Идиома википедии распознается, но ветвь и AND не оптимизированы). Используйте свойства
_rotl
/ _rotr
из <intrin.h>
на x86 (включая x86-64).
gcc для ARM использует and r1, r1, #31
для вращений с переменным счетом, но все же выполняет фактическое вращение с одной инструкцией : ror r0, r0, r1
. Таким образом, gcc не понимает, что число поворотов является модульным. Как сказано в документации ARM, "ROR с длиной смещения, n
, более 32 - это то же самое, что ROR с длиной смещения n-32
" . Я думаю, что gcc здесь запутался, потому что сдвиги влево / вправо на ARM насыщают счет, поэтому сдвиг на 32 или больше очистит регистр. (В отличие от x86, где сдвиги маскируют счет так же, как и вращение). Вероятно, он решает, что ему нужна инструкция AND перед распознаванием идиомы поворота, из-за того, как некруглые сдвиги работают на этой цели.
Текущие компиляторы x86 по-прежнему используют дополнительную инструкцию для маскировки счетчика переменных для 8- и 16-битных поворотов, вероятно, по той же причине, по которой они не избегают AND в ARM. Это пропущенная оптимизация, поскольку производительность не зависит от числа оборотов на любом процессоре x86-64. (Маскирование счетчиков было введено с 286 по соображениям производительности, потому что оно обрабатывало сдвиги итеративно, а не с постоянной задержкой, как современные процессоры.)
Кстати, для поворотов с переменным числом предпочтение отдается повороту вправо, чтобы компилятор не делал 32-n
для реализации поворота влево на архитектурах, таких как ARM и MIPS, которые обеспечивают только поворот вправо. (Это оптимизирует счет с постоянными времени компиляции.)
Интересный факт: ARM на самом деле не имеет специальных команд сдвига / поворота, это просто MOV с операндом источника , проходящим через баррель в режиме ROR : mov r0, r0, ror r1
. Таким образом, вращение может сложиться в операнд источника-регистра для инструкции EOR или чего-то еще.
Убедитесь, что вы используете беззнаковые типы для n
и возвращаемого значения, иначе это не будет поворот . (gcc для целей x86 выполняет арифметическое смещение вправо, смещение копий знака-знака, а не нуля, что приводит к проблеме, когда вы OR
объединяете два сдвинутых значения. Сдвиг вправо отрицательных целых чисел со знаком является определяемым реализацией C).
Кроме того, убедитесь, что счетчик сдвигов относится к типу без знака , потому что (-n)&31
со знаком со знаком может быть дополнением или знаком / величиной, а не модульным 2 ^ n, который вы получаете. с неподписанным или двумя дополнениями. (См. Комментарии к сообщению в блоге Регера). unsigned int
хорошо работает на каждом компиляторе, на который я смотрел, при любой ширине x
. Некоторые другие типы фактически игнорируют распознавание идиомы для некоторых компиляторов, поэтому не просто используйте тот же тип, что и x
.
Некоторые компиляторы предоставляют встроенные функции для поворотов , что намного лучше, чем inline-asm, если переносимая версия не генерирует хороший код на выбранном вами компиляторе. Не существует кроссплатформенных встроенных функций для каких-либо известных мне компиляторов. Вот некоторые из вариантов x86:
- Intel документирует, что
<immintrin.h>
обеспечивает _rotl
и _rotl64
встроенные , и то же самое для сдвига вправо. MSVC требует <intrin.h>
, в то время как gcc требует <x86intrin.h>
. #ifdef
заботится о gcc против icc, но, похоже, clang их нигде не предоставляет, за исключением режима совместимости MSVC с -fms-extensions -fms-compatibility -fms-compatibility-version=17.00
. И asm, который он испускает для них, отстой (дополнительная маскировка и CMOV).
- MSVC:
_rotr8
и _rotr16
.
- gcc и icc (не clang):
<x86intrin.h>
также обеспечивает __rolb
/ __rorb
для 8-битного поворота влево / вправо, __rolw
/ __rorw
(16-бит), __rold
/ __rord
(32-битный), __rolq
/ __rorq
(64-битный, только для 64-битных целей). Для узких поворотов в реализации используются __builtin_ia32_rolhi
или ...qi
, но 32- и 64-разрядные повороты определяются с помощью сдвига / или (без защиты от UB, поскольку код только в ia32intrin.h
должен работать на gcc для x86). Похоже, что GNU C не имеет каких-либо кроссплатформенных функций __builtin_rotate
, как для __builtin_popcount
(которые расширяются до того, что является оптимальным на целевой платформе, даже если это не одна инструкция). Большую часть времени вы получаете хороший код от распознавания идиом.
// For real use, probably use a rotate intrinsic for MSVC, or this idiom for other compilers. This pattern of #ifdefs may be helpful
#if defined(__x86_64__) || defined(__i386__)
#ifdef _MSC_VER
#include <intrin.h>
#else
#include <x86intrin.h> // Not just <immintrin.h> for compilers other than icc
#endif
uint32_t rotl32_x86_intrinsic(rotwidth_t x, unsigned n) {
//return __builtin_ia32_rorhi(x, 7); // 16-bit rotate, GNU C
return _rotl(x, n); // gcc, icc, msvc. Intel-defined.
//return __rold(x, n); // gcc, icc.
// can't find anything for clang
}
#endif
Предположительно, некоторые компиляторы, отличные от x86, также имеют встроенные функции, но давайте не будем расширять этот ответ вики-сообщества, чтобы включить их все. (Возможно, сделайте это в существующем ответе о внутренностях ).
(В старой версии этого ответа предлагался встроенный asm для MSVC (который работает только для 32-битного кода x86), или http://www.devx.com/tips/Tip/14043 для версии на C. На это отвечают комментарии.)
Встроенный asm побеждает многие оптимизации , , особенно в стиле MSVC, поскольку он заставляет входные данные быть сохраненными / перезагруженными . Тщательно написанное вращение in-asm в GNU C позволило бы счету быть непосредственным операндом для счетчиков смещения во время компиляции, но он все равно не мог оптимизировать полностью, если значение, которое должно быть смещено, также является константой времени компиляции после встраивания. https://gcc.gnu.org/wiki/DontUseInlineAsm.