См. Также более раннюю версию этого ответа по другому вопросу о ротации с более подробной информацией о том, что asm gcc / clang производит для x86.
Наиболее удобный для компилятора способ выразить поворот в C и C ++, который позволяет избежать любого неопределенного поведения, кажется реализацией Джона Регера . Я адаптировал его для поворота по ширине шрифта (например, используя типы с фиксированной шириной uint32_t
).
#include <stdint.h> // for uint32_t
#include <limits.h> // for CHAR_BIT
#include <assert.h>
static inline uint32_t rotl32 (uint32_t n, unsigned int c)
{
const unsigned int mask = (CHAR_BIT*sizeof(n) - 1);
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);
c &= mask;
return (n>>c) | (n<<( (-c)&mask ));
}
Работает для любого целого типа без знака, а не только uint32_t
, поэтому вы можете создавать версии для других размеров.
См. Также версию шаблона C ++ 11 с множеством проверок безопасности (включая то, static_assert
что ширина типа является степенью двойки) , чего, например, нет в некоторых 24-битных DSP или 36-битных мэйнфреймах.
Я бы рекомендовал использовать шаблон только как серверную часть для оболочек с именами, которые явно включают ширину поворота. Правила целочисленного продвижения означают, rotl_template(u16 & 0x11UL, 7)
что выполняется поворот на 32 или 64 бита, а не на 16 (в зависимости от ширины unsigned long
). Даже uint16_t & uint16_t
повышен до signed int
по правилам целочисленного продвижения С ++, за исключением того, на платформах , где 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 и более поздние версии оптимизируют ветвление и маску в версии wikipedia, используя только инструкцию
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
intrinsics <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, которое вы получаете с беззнаковым или двумя дополнениями. (См. Комментарии к сообщению в блоге Regehr). unsigned int
хорошо работает на всех компиляторах, на которые я смотрел, для любой ширины x
. Некоторые другие типы фактически препятствуют распознаванию идиом для некоторых компиляторов, поэтому не используйте только тот же тип, что и x
.
Некоторые компиляторы предоставляют встроенные функции для вращения , что намного лучше, чем inline-asm, если переносимая версия не генерирует хороший код для компилятора, на который вы нацеливаетесь. Для известных мне компиляторов нет кроссплатформенных встроенных функций. Вот некоторые из вариантов x86:
- Документы Intel, которые
<immintrin.h>
предоставляют _rotl
и _rotl64
внутренние компоненты , и то же самое для сдвига вправо. MSVC требует <intrin.h>
, а gcc требует <x86intrin.h>
. An #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-битные ротации определяются с помощью shift / или (без защиты от UB, потому что код ia32intrin.h
должен работать только на gcc для x86). GNU C, похоже, не имеет каких-либо кроссплатформенных __builtin_rotate
функций, в отличие от них __builtin_popcount
(которые расширяются до любых оптимальных значений на целевой платформе, даже если это не одна инструкция). В большинстве случаев хороший код получается благодаря распознаванию идиом.
#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 _rotl(x, n);
}
#endif
Предположительно, некоторые компиляторы, отличные от x86, также имеют встроенные функции, но давайте не будем расширять этот ответ сообщества, чтобы включить их все. (Возможно, сделайте это в существующем ответе о встроенных функциях ).
(В старой версии этого ответа предлагался встроенный asm для MSVC (который работает только для 32-битного кода x86) или http://www.devx.com/tips/Tip/14043 для версии C. Комментарии отвечают на это .)
Встроенный asm побеждает многие оптимизации , особенно в стиле MSVC, потому что он заставляет вводные данные сохраняться / перезагружаться . Тщательно написанный GNU C inline-asm rotate позволит счетчику быть непосредственным операндом для счетчиков сдвига с постоянной времени компиляции, но он все равно не сможет полностью оптимизировать, если значение, которое должно быть сдвинуто, также является константой времени компиляции после встраивания. https://gcc.gnu.org/wiki/DontUseInlineAsm .