旋转再探:另一种单向算法
摘要
Raymond Chen重新审视了一种用于交换相邻内存块的单向旋转算法,解释了其递归方法和性能特性。
<p>不久前,我们研究了<a title="在大型内存块内交换两个内存块,使用恒定内存" href="https://devblogs.microsoft.com/oldnewthing/20260101-00/?p=111955">在大型内存块内交换两个内存块</a>的问题,并在此过程中了解了<code>std::<wbr />rotate</code>,它用于交换两个<i>相邻</i>的内存块(大小不必相同)。</p>
<p>我在后记中提到,<a href="https://github.com/llvm/llvm-project/blob/682c8e22e61f50ce2d9a0c42475a3aa6d578a1ad/libcxx/include/__algorithm/rotate.h">clang的libcxx</a>和<a href="https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/stl_algo.h">gcc的libstdc++</a>包含了针对随机访问迭代器的<code>std::<wbr />rotate</code>特化实现,这些实现将操作视为排列,并将排列分解为循环。</p>
<p>我弄错了。</p>
<p>gcc的libstdc++中的实现包含针对单元素旋转的特殊情况,但在一般情况下,它使用了不同的算法。</p>
<p>我们将要交换的内存块称为A和B,其中A由元素A1、A2、A3等组成;而B块包含元素B1、B2、B3等。不失一般性,假设A块较小(如果不是,我们可以镜像算法)。具体来说,假设元素顺序为A1、A2、A3、B1、B2、B3、B4、B5。</p>
<table style="border-collapse: collapse; text-align: center;" title="见上文" border="0" cellspacing="0" cellpadding="1">
<tbody>
<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>↑</td>
<td> </td>
<td> </td>
<td>↑</td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td>↑</td>
</tr>
<tr>
<td>first</td>
<td> </td>
<td> </td>
<td>mid</td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td>last</td>
</tr>
</tbody>
</table>
<p>交换<code>first</code>和<code>mid</code>处的元素,然后将两个迭代器向前移动。第一步之后,得到如下结果:</p>
<table style="border-collapse: collapse; text-align: center;" title="B1 A2 A3 A1 B2 B3 B4 B5, first指向A2, mid指向B2" border="0" cellspacing="0" cellpadding="1">
<tbody>
<tr>
<td style="width: 2em; border: solid 1px currentcolor;">B1</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; background: lch(from currentcolor l c h / .1);">A1</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> </td>
<td>↑</td>
<td> </td>
<td> </td>
<td>↑</td>
<td> </td>
<td> </td>
<td> </td>
<td>↑</td>
</tr>
<tr>
<td> </td>
<td>first</td>
<td> </td>
<td> </td>
<td>mid</td>
<td> </td>
<td> </td>
<td> </td>
<td>last</td>
</tr>
</tbody>
</table>
<p>经过三步后,所有A元素被移出,并被相同数量的B元素替换。</p>
<table style="border-collapse: collapse; text-align: center;" title="B1 B2 B3 A1 A2 A3 B4 B5, first指向A1, mid指向B4" border="0" cellspacing="0" cellpadding="1">
<tbody>
<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>↑</td>
<td> </td>
<td> </td>
<td>↑</td>
<td> </td>
<td>↑</td>
</tr>
<tr>
<td> </td>
<td> </td>
<td> </td>
<td>first</td>
<td> </td>
<td> </td>
<td>mid</td>
<td> </td>
<td>last</td>
</tr>
</tbody>
</table>
<p>但不要停下来。继续直到<code>mid</code>到达<code>last</code>。</p>
<table style="border-collapse: collapse; text-align: center;" title="B1 B2 B3 B4 B5 A3 A1 A2, first指向A1, mid指向最后一个元素之后" border="0" cellspacing="0" cellpadding="1">
<tbody>
<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>↑</td>
<td> </td>
<td> </td>
<td>↑</td>
</tr>
<tr>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td>first</td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td>mid<br />
last</td>
</tr>
</tbody>
</table>
<p>所有B元素都交换到了最终位置,但A元素变得混乱。</p>
<p>但你可以预测混乱的确切性质。A块分为两块。设<var>n</var>为总元素数|A| + |B|,<var>a</var>为A中元素的数量,则第一块包含最后的<var>n</var> % <var>a</var>个元素,第二块包含最初的<var>a</var> − (<var>n</var> % <var>a</var>)个元素。</p>
<p>因此,我们可以递归旋转A块的两部分来完成工作。将<code>mid</code>移动到<code>first</code> + (<var>n</var> % <var>a</var>)处,并重新启动算法。</p>
<p>该算法执行<var>n</var> − 1次交换。可以通过归纳法计算:执行|B|次交换,然后递归旋转|A|。或者直接计算:每次交换将一个元素移动到最终位置,除了最后一次交换将两个元素移动到最终位置。</p>
<p>该算法的局部性相当好。<code>first</code>迭代器稳步向前移动,<code>mid</code>迭代器大多数时间向前移动,最多有<var>O</var>(log (min(|A|, |B|))次向后重置。</p>
<p>下次,我们将对这个算法有一个惊人的发现。</p>
<p>本文最初发布于<a href="https://devblogs.microsoft.com/oldnewthing/20260602-00/?p=112376">Rotation revisited: Another unidirectional algorithm</a>。</p>
查看缓存全文
缓存时间: 2026/06/03 03:35
# 旋转再探:另一种单向算法 - 旧事新说
来源:https://devblogs.microsoft.com/oldnewthing/20260602-00?p=112376
不久前,我们探讨了在固定内存中交换位于更大内存块内的两个内存块的问题(https://devblogs.microsoft.com/oldnewthing/20260101-00/?p=111955),并在此过程中了解了`std::rotate`,它用于交换两个*相邻*的内存块(大小不必相同)。
我在后记中提到,clang 的 libcxx([链接](https://github.com/llvm/llvm-project/blob/682c8e22e61f50ce2d9a0c42475a3aa6d578a1ad/libcxx/include/__algorithm/rotate.h))和 gcc 的 libstdc++([链接](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/stl_algo.h))包含了针对随机访问迭代器的`std::rotate`特化版本,这些版本将操作视为一种置换,并将该置换分解为循环。
我搞错了。
gcc 的 libstdc++ 中的实现对于单元素旋转有特例,但在一般情况下,它使用的是另一种算法。
我们将要交换的两个内存块称为 A 和 B,其中 A 由元素 A1、A2、A3 等组成,而 B 块包含元素 B1、B2、B3 等。不失一般性,假设 A 块较小(如果不是,我们只需镜像算法即可)。为了具体说明,我们假设元素为 A1、A2、A3、B1、B2、B3、B4、B5。
```
A1 A2 A3 B1 B2 B3 B4 B5
↑ ↑ ↑
first mid last
```
交换`first`和`mid`位置的元素,然后将两个迭代器都向前移动。经过第一步后,得到如下结果:
```
B1 A2 A3 A1 B2 B3 B4 B5
↑ ↑ ↑
first mid last
```
经过三步后,我们已经将所有 A 移出,并用相等数量的 B 替换了它们。
```
B1 B2 B3 A1 A2 A3 B4 B5
↑ ↑ ↑
first mid last
```
但不要停止。继续操作,直到`mid`到达`last`。
```
B1 B2 B3 B4 B5 A3 A1 A2
↑ ↑ ↑
first mid last
```
所有 B 都已交换到它们的最终位置,但 A 的顺序被打乱了。
但你可以预测打乱的具体模式。A 块分成两段。如果令 n 为元素总数 |A| + |B|,a 为 A 中元素的数量,那么第一段包含最终 n % a 个元素,而第二段包含初始的 a − (n % a) 个元素。
因此,我们可以递归地旋转 A 块的两段来完成任务。将`mid`移动到`first` + (n % a),然后重新启动算法。
该算法执行 n − 1 次交换。你可以通过归纳法来证明:首先执行 |B| 次交换,然后递归旋转 |A|。或者直接计算:每次交换将一个元素移动到最终位置,除了最后一次交换将两个元素移动到最终位置。
该算法的局部性相当不错。`first`迭代器稳步向前移动,而`mid`迭代器大部分时间也向前移动,最多只有 O(log(min(|A|, |B|))) 次向后复位。
下一次,我们将对这个算法有一个令人震惊的发现。
### 分类
### 主题
## 作者
Raymond Chen
Raymond 参与 Windows 的发展已有 30 多年。2003 年,他创建了一个名为“旧事新说”的网站,其受欢迎程度远超他最疯狂的想象,这一发展至今仍让他心有余悸。该网站催生了一本书,巧合的是,书名也叫《旧事新说》(Addison Wesley 2007 年出版)。他偶尔会出现在 Windows Dev Docs Twitter 账户上,讲述一些毫无有用信息的故事。
相似文章
# 重新审视位旋转:关于 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++ 中针对随机访问迭代器的旋转算法,揭示其本质上与前向迭代器旋转算法相同,只是从不同角度来看待而已。本文是一个系列文章的一部分,该系列对比了不同编译器中旋转算法的实现方式。
旋转算法再探:clang的libcxx中的循环分解
本文深入探讨了clang的libcxx中用于旋转操作的循环分解算法,解释了该算法如何通过计算最大公约数(gcd)来确定循环数量,从而实现最少的交换次数。
旋转再探:在循环分解中避免计算最大公约数
本文介绍了一种技术,在std::rotate的循环分解中避免计算最大公约数,该技术用于OpenJDK的Collections.rotate方法。它提供了一个C++实现,通过跟踪已旋转元素的数量来确定所有循环何时完成。
愚蠢的RCU技巧:边界案例RCU实现
Paul McKenney 讨论了非常规和边界情况的 RCU(读-拷贝-更新)实现,包括早期 Unix 系统中使用的定时等待 RCU 方法,以及与内存隔离相关的固定缓冲区 RCU 概念,展示了内核开发中具有创造性但潜在危险的同步技术。
@che_shr_cat: 1/ 多年来我们一直通过头部共享(GQA/MQA)来优化KV缓存,但我们忽略了一个基本假设:为什么……
这条推文挑战了关于Transformer需要独立的Q、K和V投影的基本假设,提出合并它们可以为KV缓存带来巨大的内存节省。