另请参阅另一个旋转问题的此答案的早期版本,其中包含有关 asm gcc/clang 为 x86 生成的内容的更多详细信息。
在 C 和 C++ 中表达旋转以避免任何未定义行为的最编译器友好的方式似乎是John Regehr 的 implementation。我已经将它调整为按类型的宽度旋转(使用固定宽度的类型,如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
)。Even由 C++ 的整数uint16_t & uint16_t
提升signed int
规则提升,除非在int
不大于uint16_t
.
在 x86 上,此版本内联到单个rol r32, cl
(or rol r32, imm8
) 编译器,因为编译器知道x86 旋转和移位指令以与 C 源代码相同的方式屏蔽移位计数。
编译器支持 x86 上的这种避免 UB 的习惯用法,uint32_t x
用于unsigned int n
变量计数移位:
- clang:从 clang3.5 开始识别为可变计数轮换,在此之前有多个班次+或 insns。
- gcc:从 gcc4.9 开始识别为变量计数旋转,在此之前有多个班次+或 insns。gcc5 及更高版本也优化了维基百科版本中的分支和掩码,仅使用变量计数的
ror
orrol
指令。
- icc:从 ICC13 或更早版本开始支持可变计数轮换。当 BMI2 不能用于保存 MOV时,常量计数会
shld edi,edi,7
比rol edi,7
某些 CPU(尤其是 AMD,还有一些 Intel)更慢并占用更多字节。rorx eax,edi,25
- MSVC:x86-64 CL19:仅识别为常量计数旋转。(维基百科成语被识别,但分支和 AND 没有被优化掉)。使用x86(包括 x86-64)上的
_rotl
/_rotr
内在函数。<intrin.h>
ARM 的 gcc 使用and r1, r1, #31
for variable-count 旋转,但实际旋转仍然使用一条指令: ror r0, r0, r1
。所以 gcc 没有意识到旋转计数本质上是模块化的。正如 ARM 文档所说,“具有移位长度的 ROR n
,超过 32 与具有移位长度的 ROR 相同n-32
”。我认为 gcc 在这里会感到困惑,因为 ARM 上的左/右移位会使计数饱和,因此移位 32 或更多将清除寄存器。(与 x86 不同,移位掩盖计数与旋转相同)。它可能决定在识别旋转习语之前需要一个 AND 指令,因为非循环移位如何在该目标上起作用。
当前的 x86 编译器仍然使用额外的指令来屏蔽 8 位和 16 位循环的变量计数,这可能与它们在 ARM 上不避免 AND 的原因相同。这是一个错过的优化,因为性能不依赖于任何 x86-64 CPU 上的旋转计数。(出于性能原因,286 引入了计数屏蔽,因为它迭代地处理移位,而不是像现代 CPU 那样具有恒定延迟。)
顺便说一句,对于变量计数旋转,更喜欢右旋转,以避免编译器32-n
在仅提供右旋转的 ARM 和 MIPS 等架构上实现左旋转。(这优化了编译时常数计数。)
有趣的事实:ARM 并没有真正的专用移位/旋转指令,它只是 MOV ,源操作数在 ROR 模式下通过桶形移位器: mov r0, r0, ror r1
。因此,旋转可以折叠成用于 EOR 指令或其他东西的寄存器源操作数。
确保使用无符号类型n
和返回值,否则它不会是 rotate。(x86 目标的 gcc 进行算术右移,移入符号位的副本而不是零,当您OR
将两个移位的值放在一起时会导致问题。负符号整数的右移是 C 中实现定义的行为。)
此外,请确保移位计数是无符号类型,因为(-n)&31
有符号类型可能是一个补码或符号/大小,而不是与您使用无符号或二进制补码获得的模块化 2^n 不同。(参见 Regehr 博客文章的评论)。 unsigned int
在我看过的每个编译器上都做得很好,对于x
. 其他一些类型实际上破坏了一些编译器的习惯用法识别,所以不要只使用与x
.
一些编译器为 rotates 提供了内在函数,如果可移植版本不能在您的目标编译器上生成好的代码,这比 inline-asm 好得多。我所知道的任何编译器都没有跨平台内在函数。这些是一些 x86 选项:
// 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 编译器也具有内在函数,但我们不要扩展这个社区 wiki 答案以包含它们全部。(也许在关于内在函数的现有答案中这样做)。
(这个答案的旧版本建议特定于 MSVC 的内联 asm(仅适用于 32 位 x86 代码),或http://www.devx.com/tips/Tip/14043用于 C 版本。评论正在回复.)
内联汇编破坏了许多优化,尤其是 MSVC 风格,因为它强制存储/重新加载输入。精心编写的 GNU C inline-asm rotate 将允许计数成为编译时常量移位计数的直接操作数,但如果要移位的值也是编译时常量,它仍然无法完全优化内联后。 https://gcc.gnu.org/wiki/DontUseInlineAsm。