为什么C++中排序数组处理更快?分支预测的秘密

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

引言:C++世界中最令人意外的问题之一

\n\n

设想一个场景:你编写了一个简单的C++循环,用于对一个大数组中的元素求和,但只对超过特定阈值的元素进行求和。你运行代码,它……正常工作。然后你决定预先对数组进行排序,再次运行。突然你发现代码执行速度提高了2-3倍更快!这其中的奥秘是什么?编译器的魔法?基准测试错误?

\n\n

这个问题在十多年前被发布在Stack Overflow上,获得了数千次浏览,至今仍引发激烈争论。今天,我们在PythonLib上深入剖析它,通过真实的C++示例和对现代处理器架构的深入探讨。你不仅会了解为什么会发生这种情况,还会知道如何利用这种效应来加速你的代码。

\n\n

1. 重现现象:令人惊讶的代码

\n\n

让我们编写一个简单的程序,清晰地展示这种效应。我们需要一个包含10万个整数的数组。我们将用随机值填充它,然后测量对所有大于128的元素求和的时间。

\n\n
#include <iostream>\n#include <vector>\n#include <algorithm>\n#include <chrono>\n#include <random>\n\nint main() {\n    const int SIZE = 100000;\n    const int THRESHOLD = 128;\n    \n    // 创建未排序数组\n    std::vector<int> data(SIZE);\n    std::mt19937 rng(42);\n    std::uniform_int_distribution<int> dist(0, 255);\n    for (auto& val : data) {\n        val = dist(rng);\n    }\n\n    // 对未排序数组求和\n    auto start = std::chrono::high_resolution_clock::now();\n    long long sum_unsorted = 0;\n    for (int i = 0; i < SIZE; ++i) {\n        if (data[i] >= THRESHOLD) {\n            sum_unsorted += data[i];\n        }\n    }\n    auto end = std::chrono::high_resolution_clock::now();\n    auto time_unsorted = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();\n    std::cout << "未排序数组: " << time_unsorted << " 微秒, 总和 = " << sum_unsorted << std::endl;\n\n    // 对数组排序\n    std::sort(data.begin(), data.end());\n\n    // 对排序数组求和\n    start = std::chrono::high_resolution_clock::now();\n    long long sum_sorted = 0;\n    for (int i = 0; i < SIZE; ++i) {\n        if (data[i] >= THRESHOLD) {\n            sum_sorted += data[i];\n        }\n    }\n    end = std::chrono::high_resolution_clock::now();\n    auto time_sorted = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();\n    std::cout << "排序数组: " << time_sorted << " 微秒, 总和 = " << sum_sorted << std::endl;\n\n    std::cout << "加速比: " << (double)time_unsorted / time_sorted << "x" << std::endl;\n    return 0;\n}
\n\n

在不开启优化(-O0标志)编译时,差异很小,但如果使用GCC/Clang开启-O2或-O3,你会得到一个令人印象深刻的结果:排序数组的处理速度快了3-6倍。

\n\n

2. 处理器解剖:差异从何而来?

\n\n

解开谜团的关键是一种称为分支预测的机制。现代处理器是流水线机器。它们不是一条一条地执行指令,而是将指令分解为多个阶段(取指、译码、执行、写回)。为了不让流水线空闲,处理器会尝试猜测接下来将执行哪条代码分支(if或else),并提前开始加载指令。

\n\n

在我们的示例中,条件if (data[i] >= THRESHOLD)是一个分支点。对于排序数组,预测效果很好

博客

书籍推荐