我有一段时间试图想出一个不违反C / C ++标准的恒定时间旋转。
问题是边缘/边角情况,其中操作在算法中被调出,并且这些算法无法更改。例如,以下内容来自Crypto++并执行GCC ubsan下的测试工具(即g++ fsanitize=undefined
):
$ ./cryptest.exe v | grep runtime
misc.h:637:22: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
misc.h:643:22: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
misc.h:625:22: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
misc.h:637:22: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
misc.h:643:22: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
misc.h:637:22: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
misc.h:637
处的代码:
template <class T> inline T rotlMod(T x, unsigned int y)
{
y %= sizeof(T)*8;
return T((x<<y) | (x>>(sizeof(T)*8-y)));
}
英特尔的ICC特别无情,它删除了整个函数调用y %= sizeof(T)*8
。几年前我们修复了这个问题,但由于缺乏恒定的时间解决方案而将其他勘误表留在了原地。
还有一个痛点。当y = 0
时,我得到32 - y = 32
的条件,并设置未定义的行为。 如果我添加if(y == 0) ...
的检查,则代码无法满足常量时间要求。
我已经研究了许多其他实现,从Linux内核到其他加密库。它们都包含相同的未定义行为,因此它似乎是一个死胡同。
如何用最少的指令在几乎恒定的时间内完成旋转?
编辑 :接近常量时间,我的意思是避免分支,因此始终执行相同的指令。我并不担心CPU微码时序。虽然分支预测在x86 / x64上可能很好,但在嵌入式等其他平台上可能效果不佳。
如果GCC或Clang提供执行rotate in near constant time的内在函数,则不需要这些技巧。我甚至满足于“执行旋转”,因为他们甚至都没有。
答案 0 :(得分:11)
我已经将这个答案与其他几个&#34;旋转&#34;的全部细节联系起来了。问题,包括this community wiki question,应与最佳做法保持同步。
我发现了一篇关于这个问题的博客文章,看起来它终于解决了问题(使用足够新的编译器版本)。
John Regehr at the University of Utah推荐版本&#34; c&#34;他试图制作旋转功能。我用一个按位AND替换了他的断言,发现它仍然编译成一个旋转insn。
typedef uint32_t rotwidth_t; // parameterize for comparing compiler output with various sizes
rotwidth_t rotl (rotwidth_t x, unsigned int n)
{
const unsigned int mask = (CHAR_BIT*sizeof(x)-1); // e.g. 31
assert ( (n<=mask) &&"rotate by type width or more");
n &= mask; // avoid undef behaviour with NDEBUG. 0 overhead for most types / compilers
return (x<<n) | (x>>( (-n)&mask ));
}
rotwidth_t rot_const(rotwidth_t x)
{
return rotl(x, 7);
}
这可以在x的类型上进行模板化,但它可能对于实际使用更有意义,在函数名称中具有宽度(如rotl32
)。通常当你旋转时,你知道你想要的宽度,这比你当前存储该值的大小变量更重要。
另外,请确保仅将其与无符号类型一起使用。有符号类型的右移会进行算术移位,移位符号位。 (它在技术上依赖于实现的行为,但现在一切都使用了2的补充。)
Pabigot在我做之前独立提出了同样的想法,and posted it at gibhub。他的版本有C ++ static_assert检查,使得编译时错误使用该类型范围之外的旋转计数。
I tested mine with gcc.godbolt.org,定义了NDEBUG,用于变量和编译时const旋转计数:
shld $7, %edi, %edi
。没有-march=native
的情况下很好)即使是较新的编译器版本也可以处理来自维基百科的常用代码(包含在godbolt示例中),而不会生成分支或cmov。 John Regehr的版本具有在旋转计数为0时避免未定义行为的优点。
有一些注意事项有8位和16位旋转,但当n
为uint32_t
时,编译器似乎没有32或64。请参阅godbolt link代码中的注释,了解我测试uint*_t
各种宽度的一些注释。希望这个成语能够被所有编译器更好地识别,以便将来更多类型宽度的组合。有时gcc会在旋转计数上无用地发出AND
insn,即使x86 ISA定义旋转insn并使用精确的AND作为第一步。
&#34;最优&#34;意味着有效:
# gcc 4.9.2 rotl(unsigned int, unsigned int):
movl %edi, %eax
movl %esi, %ecx
roll %cl, %eax
ret
# rot_const(unsigned int):
movl %edi, %eax
roll $7, %eax
ret
内联时,编译器应该能够将值排列在正确的寄存器中,从而导致只有一次旋转。
对于较旧的编译器,当旋转计数是编译时常量时,您仍然可以得到理想的代码。 Godbolt让你用ARM作为目标进行测试,它也使用了旋转。对于较旧的编译器具有可变计数,您会得到一些代码膨胀,但没有分支或主要性能问题,因此这个习惯用法通常应该是安全的。
BTW,我修改了John Regehr的原始版本以使用CHAR_BIT * sizeof(x),而gcc / clang / icc也为uint64_t
发出了最佳代码。但是,我确实注意到,当函数返回类型仍为x
时,将uint64_t
更改为uint32_t
会使gcc将其编译为shift /或。因此,如果您希望64b的低32b旋转,请小心将结果转换为单独序列点中的32位。即将结果分配给64位变量,然后转换/返回它。 icc仍然会生成一个旋转insn,但是gcc和clang不是为了
// generates slow code: cast separately.
uint32_t r = (uint32_t)( (x<<n) | (x>>( -n&(CHAR_BIT*sizeof(x)-1) )) );
如果有人可以使用MSVC对此进行测试,那么知道那里会发生什么会很有用。
答案 1 :(得分:6)
你可以添加一个额外的模运算来防止32位的移位,但我不相信这比使用if check和分支预测器更快。
template <class T> inline T rotlMod(T x, unsigned int y)
{
y %= sizeof(T)*8;
return T((x<<y) | (x>>((sizeof(T)*8-y) % (sizeof(T)*8))));
}
答案 2 :(得分:3)
将表达式编写为T((x<<y) | ((x>>(sizeof(T)*CHAR_BITS-y-1)>>1))
应该为位大小以下的y
的所有值产生定义的行为,假设T
是无填充的无符号类型。除非编译器具有良好的优化器,否则生成的代码可能不如原始表达式生成的代码好。不得不忍受笨重难以阅读的代码,这会导致许多编译器执行速度变慢,这是进步代价的一部分,但是,因为给出了一个超现代的编译器
if (y) do_something();
return T((x<<y) | (x>>(sizeof(T)*8-y)));
可以通过调用do_something
无条件来提高代码的“效率”。
PS:我想知道是否有任何真实平台更改shift-right的定义,以便当x >> y
精确等于y
的位大小时x
,将需要产生0或x,但是可以以任意(未指定的)方式进行选择,是否需要平台生成额外的代码或者在非人为场景中排除真正有用的优化?
答案 3 :(得分:2)
额外模数的替代方法是乘以0或1(感谢!!
):
template <class T> T rotlMod(T x, unsigned int y)
{
y %= sizeof(T) * 8;
return T((x << y) | (x >> ((!!y) * (sizeof(T) * 8 - y)));
}