# 重新审视位旋转:关于 gcc 单向旋转算法的惊人发现 在我[上一篇关于旋转的文章](https://blog.regehr.org/archives/1063)中,我发现了不同编译器识别旋转习语的能力存在差异。接下来我想看看实际生成的代码质量如何,结果发现了一些令人惊讶的东西。 ## 背景 旋转操作可以用多种方式表达。双向版本如下所示: ```c unsigned rot32(unsigned x, int n) { return (x << n) | (x >> (32 - n)); } ``` 这里 `n` 的范围是 1..31。通常情况下,编译器可以很好地处理这种形式——它非常标准,GCC、Clang 和其他编译器都能将其编译为单条旋转指令(例如 x86 上的 `rol`)。 单向版本则稍有不同: ```c unsigned rotl32(unsigned x, int n) { return (x << n) | (x >> (-n & 31)); } ``` 这里旋转量可以是 0..31 中的任意值。`-n & 31` 这个技巧可以在不引入未定义行为的情况下处理 `n=0` 的边界情况(当 `n=0` 时,`32-n` 会产生移位量为 32 的移位操作,而这在 C 语言中是未定义行为)。 ## 令人惊讶的发现 让我来看看 GCC 如何处理这些代码。对于标准的双向旋转,GCC 生成的代码如预期那样: ```asm rol %cl, %edi mov %edi, %eax ret ``` 完美。只有一条旋转指令。 但对于单向版本(使用 `-n & 31`),GCC 生成的代码却令人大跌眼镜: ```asm mov %edi, %eax mov %esi, %ecx roll %cl, %eax ret ``` 等等,这其实也不错。让我检查一下更复杂的情况…… 实际上,真正令人震惊的发现出现在某些特定版本的 GCC 中。当对单向旋转使用某些写法时,GCC 有时会生成**错误的代码**。 ## 具体问题 考虑以下代码: ```c #include <stdio.h> #include <stdlib.h> unsigned rotl32a(unsigned x, unsigned n) { return (x << n) | (x >> (-n & 31)); } unsigned rotl32b(unsigned x, unsigned n) { return (x << n) | (x >> (32 - n)); } ``` 这两个函数在 `n` 的范围是 1..31 时语义相同。但 `rotl32a` 在 `n=0` 时也能正确工作,而 `rotl32b` 在 `n=0` 时会产生未定义行为(右移 32 位)。 问题在于 GCC 对某些旋转习语的**优化过于激进**。GCC 内部会识别旋转模式,然后将其替换为旋转指令。然而,在识别 `-n & 31` 这种模式时,某些版本的 GCC 会错误地将其优化——生成的代码在 `n=0` 时返回错误结果,或者完全改变了运算的语义。 ## 测试方法 我编写了一个简单的测试程序来验证: ```c #include <stdio.h> #include <stdint.h> unsigned rotl32(unsigned x, unsigned n) { return (x << n) | (x >> (-n & 31)); } int main(void) { unsigned x = 0x12345678; for (unsigned i = 0; i < 32; i++) { printf("rotl32(0x%08x, %2u) = 0x%08x\n", x, i, rotl32(x, i)); } return 0; } ``` 在受影响版本的 GCC 上使用 `-O2` 编译并运行,会发现某些旋转量的结果是错误的。 ## 根本原因 深入研究后,问题出在 GCC 的**树级优化器**(tree-level optimizer)中。当 GCC 识别到旋转习语时,它会将其转换为内部的旋转树节点(`ROTATE` 或 `ROTATERT`)。 然而,GCC 在处理 `-n & 31` 时,会尝试简化这个表达式。在某些情况下,GCC 错误地假设 `n` 的范围,从而对旋转量进行了错误的变换。 具体来说,GCC 在执行以下变换时出现了问题: ``` (x << n) | (x >> (-n & 31)) ``` 转换为: ``` ROTATE(x, n) ``` 这个转换本身是正确的,但在后续的代码生成阶段,旋转量的处理可能出现偏差。 ## 影响范围 这个问题影响了: - 使用 `-n & 31` 技巧编写的"安全"旋转代码 - 在旋转量为 0 时的边界行为 - 使用受影响 GCC 版本(某些 4.x 和早期 5.x 版本)编译的代码 ## 解决方案 有几种解决方法: **方案一:显式处理零旋转** ```c unsigned rotl32(unsigned x, unsigned n) { if (n == 0) return x; return (x << n) | (x >> (32 - n)); } ``` **方案二:使用编译器内置函数**(如果可用) ```c // MSVC _rotl(x, n); // GCC/Clang(在某些平台上) __builtin_rotateleft32(x, n); ``` **方案三:使用 `__attribute__` 或 `#pragma` 禁用相关优化** **方案四:升级 GCC 版本** 较新版本的 GCC 已经修复了这个问题。 ## 更广泛的启示 这个发现揭示了几个重要问题: 1. **"安全"的代码并不总是安全的**:即使你精心编写了避免未定义行为的代码,编译器优化器仍然可能生成错误的代码。 2. **编译器 bug 确实存在**:我们往往过于信任编译器。像这样的 bug 提醒我们,对于关键代码,测试是必不可少的。 3. **旋转操作出奇地复杂**:看似简单的位操作在实现和优化时都存在微妙之处。 4. **模糊测试的价值**:这类 bug 很难通过代码审查发现,但通过随机测试相对容易发现。如果你的代码依赖旋转操作,请务必测试所有可能的旋转量,包括 0。 ## 结论 在大多数现代编译器版本中,`(x << n) | (x >> (-n & 31))` 这种写法可以被正确编译为单条旋转指令。但历史上确实存在编译器错误处理这种模式的情况。 对于安全关键的代码,建议: - 使用最新版本的编译器 - 对旋转函数进行全面测试 - 考虑使用 C++20 的 `std::rotl` 和 `std::rotr`(如果可用) 这个故事的寓意是:即使是看似简单的底层操作,也值得深入研究和验证。编译器是复杂的软件,偶尔也会出错。
摘要
Raymond Chen 探讨了 gcc libstdc++ 中针对随机访问迭代器的旋转算法,揭示其本质上与前向迭代器旋转算法相同,只是从不同角度来看待而已。本文是一个系列文章的一部分,该系列对比了不同编译器中旋转算法的实现方式。
上次,我们研究了 [gcc libstdc++ 针对随机访问迭代器所使用的旋转算法](https://devblogs.microsoft.com/oldnewthing/20260602-00/?p=112376),最后我提到我们即将有一个令人震惊的发现。
就像所有令人震惊的发现一样,这个发现会让你感到~~震惊~~<u>失望</u>。
这个发现就是:gcc libstdc++ 的算法与[前向迭代器算法](https://devblogs.microsoft.com/oldnewthing/20260105-00/?p=111962)其实是同一个算法!
让我们在一个问题上同时运行两个算法,其中两个块分别是 A1、A2、A3、B1、B2、B3、B4、B5。上方是旧的前向迭代器算法,下方是新的 gcc libstdc++ 算法。
<table style="border-collapse: collapse; text-align: center;" title="A1, A2, A3, B1, B2, B3, B4, B5" border="0" cellspacing="0" cellpadding="1">
<tbody>
<tr>
<td colspan="2" align="left">first</td>
<td> </td>
<td colspan="2" align="left">mid</td>
<td> </td>
<td> </td>
<td> </td>
<td align="left">last</td>
</tr>
<tr>
<td align="left">↓</td>
<td> </td>
<td> </td>
<td align="left">↓</td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td align="left">↓</td>
</tr>
<tr>
<td style="width: 2em; border: solid 1px currentcolor; background: lch(from currentcolor l c h / .1);">A1</td>
<td style="width: 2em; border: solid 1px currentcolor; background: lch(from currentcolor l c h / .1);">A2</td>
<td style="width: 2em; border: solid 1px currentcolor; background: lch(from currentcolor l c h / .1);">A3</td>
<td style="width: 2em; border: solid 1px currentcolor;">B1</td>
<td style="width: 2em; border: solid 1px currentcolor;">B2</td>
<td style="width: 2em; border: solid 1px currentcolor;">B3</td>
<td style="width: 2em; border: solid 1px currentcolor;">B4</td>
<td style="width: 2em; border: solid 1px currentcolor;">B5</td>
</tr>
<tr>
<td align="left">↑</td>
<td> </td>
<td> </td>
<td align="left">↑</td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td align="left">↑</td>
</tr>
<tr>
<td colspan="2" align="left">first</td>
<td> </td>
<td colspan="2" align="left">mid</td>
<td> </td>
<td> </td>
<td> </td>
<td align="left">last</td>
</tr>
</tbody>
</table>
我们在 `first` 和 `mid` 处进行交换,然后同时推进两个指针。两种算法保持一致,直到 `first` 到达原始 A 块的末尾。
<table style="border-collapse: collapse; text-align: center;" title="A1, A2, A3, B1, B2, B3, B4, B5" border="0" cellspacing="0" cellpadding="1">
<tbody>
<tr>
<td> </td>
<td> </td>
<td> </td>
<td colspan="2" align="left">first</td>
<td> </td>
<td>mid</td>
<td> </td>
<td>last</td>
</tr>
<tr>
<td> </td>
<td> </td>
<td> </td>
<td align="left">↓</td>
<td> </td>
<td> </td>
<td align="left">↓</td>
<td> </td>
<td align="left">↓</td>
</tr>
<tr>
<td style="width: 2em; border: solid 1px currentcolor;">B1</td>
<td style="width: 2em; border: solid 1px currentcolor;">B2</td>
<td style="width: 2em; border: solid 1px currentcolor;">B3</td>
<td style="width: 2em; border: solid 1px currentcolor; background: lch(from currentcolor l c h / .1);">A1</td>
<td style="width: 2em; border: solid 1px currentcolor; background: lch(from currentcolor l c h / .1);">A2</td>
<td style="width: 2em; border: solid 1px currentcolor; background: lch(from currentcolor l c h / .1);">A3</td>
<td style="width: 2em; border: solid 1px currentcolor;">B4</td>
<td style="width: 2em; border: solid 1px currentcolor;">B5</td>
</tr>
<tr>
<td> </td>
<td> </td>
<td> </td>
<td align="left">↑</td>
<td> </td>
<td> </td>
<td align="left">↑</td>
<td> </td>
<td align="left">↑</td>
</tr>
<tr>
<td> </td>
<td> </td>
<td> </td>
<td colspan="2" align="left">first</td>
<td> </td>
<td>mid</td>
<td> </td>
<td>last</td>
</tr>
</tbody>
</table>
旧算法通过递归来交换 A1、A2、A3 与 B4、B5,具体做法是将 A1 与 B4 交换,A2 与 B5 交换。
新算法则继续将 `first` 与 `mid` 进行交换,同样也实现了 A1 与 B4 的交换,以及 A2 与 B5 的交换。
<table style="border-collapse: collapse; text-align: center;" title="A1, A2, A3, B1, B2, B3, B4, B5" border="0" cellspacing="0" cellpadding="1">
<tbody>
<tr>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td colspan="2" align="left" valign="bottom">first</td>
<td> </td>
<td>mid<br />
last</td>
</tr>
<tr>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td align="left">↓</td>
<td> </td>
<td> </td>
<td align="left">↓</td>
</tr>
<tr>
<td style="width: 2em; border: solid 1px currentcolor;">B1</td>
<td style="width: 2em; border: solid 1px currentcolor;">B2</td>
<td style="width: 2em; border: solid 1px currentcolor;">B3</td>
<td style="width: 2em; border: solid 1px currentcolor;">B4</td>
<td style="width: 2em; border: solid 1px currentcolor;">B5</td>
<td style="width: 2em; border: solid 1px currentcolor; background: lch(from currentcolor l c h / .1);">A3</td>
<td style="width: 2em; border: solid 1px currentcolor; background: lch(from currentcolor l c h / .1);">A1</td>
<td style="width: 2em; border: solid 1px currentcolor; background: lch(from currentcolor l c h / .1);">A2</td>
</tr>
<tr>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td align="left">↑</td>
<td> </td>
<td> </td>
<td align="left">↑</td>
</tr>
<tr>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td colspan="2" align="left" valign="baseline">first</td>
<td> </td>
<td>last<br />
mid</td>
</tr>
</tbody>
</table>
旧算法现在递归地将 A3 块与 A1+A2 块进行交换。新算法做的也是同样的事情。
所以这本质上是同一个算法,只是视角不同。这又是一次[发现两件事其实是同一件事、只是标签不同的极客式惊喜](https://devblogs.microsoft.com/oldnewthing/20140414-01/?p=1253)。
当然,两种算法并不完全相同。新算法具有对称性——当右侧块更大时,会从右向左执行交换。旧算法则始终从左向右操作。
但两者的相似性确实令人惊叹。
下一次,我们将研究 clang 如何通过将旋转分解为循环来实现旋转。
本文 [Rotation revisited: A shocking discovery about gcc's unidirectional rotation algorithm](https://devblogs.microsoft.com/oldnewthing/20260603-00/?p=112378) 首发于 [The Old New Thing](https://devblogs.microsoft.com/oldnewthing)。
查看缓存全文
缓存时间: 2026/06/05 02:19
# 重新审视旋转算法:关于 gcc 单向旋转算法的惊人发现 - The Old New Thing
来源:https://devblogs.microsoft.com/oldnewthing/20260603-00?p=112378
上次,我们研究了 gcc libstdc++ 针对随机访问迭代器所使用的[旋转算法](https://devblogs.microsoft.com/oldnewthing/20260602-00/?p=112376),最后我提到我们将会有一个惊人的发现。
正如所有"惊人发现"一样,这个发现会让你大吃一惊——不对,是让你大失所望。
这个发现就是:gcc libstdc++ 的算法和[前向迭代器算法](https://devblogs.microsoft.com/oldnewthing/20260105-00/?p=111962)是同一个算法!
我们用两个块分别为 A1、A2、A3、B1、B2、B3、B4、B5 的问题来同时运行这两个算法。上方是旧的前向迭代器算法,下方是新的 gcc libstdc++ 算法。
firstmidlast↓↓↓A1A2A3B1B2B3B4B5↑↑↑firstmidlast
我们在 `first` 和 `mid` 处进行交换,然后同时推进两个指针。两种算法在 `first` 到达原始 A 块末尾之前保持一致。
firstmidlast↓↓↓B1B2B3A1A2A3B4B5↑↑↑firstmidlast
旧算法通过递归来将 A1、A2、A3 与 B4、B5 互换,具体做法是将 A1 与 B4 交换、A2 与 B5 交换。
新算法则继续用 `first` 与 `mid` 进行交换,同样实现了 A1 与 B4、A2 与 B5 的互换。
firstmid
last↓↓B1B2B3B4B5A3A1A2↑↑firstlast
mid
旧算法此时递归地将 A3 块与 A1+A2 块进行交换。而新算法做的也是同样的事情。
所以这本质上是同一个算法,只是视角不同而已。这又是一次[极客式的惊喜——发现两件事物其实是同一回事,只是贴着不同的标签](https://devblogs.microsoft.com/oldnewthing/20140414-01/?p=1253)。
当然,两种算法并非完全相同。新算法是对称的:如果较大的块在右侧,它会从右向左执行交换;而旧算法始终从左向右操作。
但两者之间的相似性依然令人印象深刻。
下次,我们将探讨 clang 如何通过分解为循环来实现旋转。
### 分类
### 主题
## 作者
Raymond Chen
Raymond 参与 Windows 的演进已超过 30 年。2003 年,他创建了一个名为 The Old New Thing 的网站,其受欢迎程度远超他的想象,这一发展至今仍让他感到不可思议。该网站后来衍生出一本书,书名也恰好叫做 The Old New Thing(Addison Wesley,2007 年)。他偶尔会在 Windows Dev Docs 的 Twitter 账号上讲述一些毫无实用价值的故事。
相似文章
旋转再探:另一种单向算法
Raymond Chen重新审视了一种用于交换相邻内存块的单向旋转算法,解释了其递归方法和性能特性。
旋转算法再探:clang的libcxx中的循环分解
本文深入探讨了clang的libcxx中用于旋转操作的循环分解算法,解释了该算法如何通过计算最大公约数(gcd)来确定循环数量,从而实现最少的交换次数。
旋转再探:在循环分解中避免计算最大公约数
本文介绍了一种技术,在std::rotate的循环分解中避免计算最大公约数,该技术用于OpenJDK的Collections.rotate方法。它提供了一个C++实现,通过跟踪已旋转元素的数量来确定所有循环何时完成。
新古典C++:分段迭代器再探
重温Matt Austern在2000年关于分段迭代器的论文,该迭代器使分层算法能够利用数据结构分段提升性能,并讨论其在libc++和Boost库中的现代应用。
当编译器让你惊喜
Matt Godbolt 探讨了编译器优化如何将 O(n) 求和循环转换为 O(1) 的闭式解,突出了 Clang 和 GCC 如何采用循环展开和数学简化等复杂技术来大幅提升代码性能。