“不要猜,去测。” ——每一个严肃的性能工程师
排序是计算机科学里最经典的问题之一,但“哪种排序最快”从来没有一个脱离上下文的答案。数据规模、数据分布、元素类型、缓存行为、标准库实现、编译器版本,都会影响最后结果。
本文只做一件事:把仓库里的排序 benchmark 变成可以直接执行、可以重新出 CSV、可以重新画图的版本,然后在当前环境下重跑一次,用真实结果回答“什么时候该用什么排序”。
先说结论:这篇文章的旧版本并不是当前仓库里可直接复现的实测稿。 当时仓库只有文章中的代码片段和手工绘制的 SVG,没有独立的 benchmark 源码、没有原始 CSV,也没有一条能直接执行的构建命令。所以旧稿里的具体数字,不能算当前仓库内可验证的真实数据。
这次我把可执行代码补到了
examples/sort-bench/,并在当前环境重跑了一遍:x86-64
Linux,12th Gen Intel Core i9-12900K,GCC 15.2.1,单线程
taskset -c 4
绑核。文中的表格和图都来自这次重新生成的 CSV。
代码与原始结果:
bench.cpp|Makefile|README.md|results-current-env.csv|plot.py
一、基准测试方法论
1.1 micro-benchmark 的几个基本陷阱
陷阱一:编译器把你的工作优化掉了
如果排序结果没有被观测到,编译器可能直接把整个调用删掉。常见做法是加汇编栅栏:
inline void do_not_optimize(const void* ptr) {
asm volatile("" : : "g"(ptr) : "memory");
}
inline void clobber_memory() {
asm volatile("" : : : "memory");
}陷阱二:小数组太快,计时噪声大于算法本身
几十个元素的排序可能只有几十到几百纳秒,单次测量几乎没有意义。正确做法是 batch 化,把一批相同规模的排序放到同一个计时窗口里,再除以 batch。
陷阱三:只测一次,拿偶然值当结论
这次套件对每个点都做多次采样,并记录中位数、p5、p95。文章里主要引用中位数,因为它比单次最小值更抗调度抖动。
1.2 这次重跑的测试口径
这次套件覆盖的是:
- 数据类型:
int32_t - 分布:
uniform、normal、sorted、reverse、sawtooth、pipe_organ、high_dup - 规模:
64、1024、65536、1000000 - 算法:
insertion、heap、merge、quick、introsort、pdqsort、TimSort、radix_lsd、radix_msd、ska_sort、std::sort、std::stable_sort
套件使用固定随机种子 42,小数组走 batch
测量,大数组直接单次计时并重复采样。每轮排序结束后都会检查:
- 输出是否有序。
- 输出是否和输入保持同一个 multiset。
二、这次补齐了什么
为了把文章从“描述性 benchmark”变成“可复现 benchmark”,仓库里新增了这些文件:
bench.cpp:真正执行 benchmark 的 C++20 程序。Makefile:构建和一键跑图入口。README.md:说明口径、构建方式和复现命令。plot.py:把 CSV 渲染成文章里的 SVG。results-current-env.csv:这次当前环境的原始结果。
也就是说,现在不是“文章里贴了点代码”,而是仓库里真的有一套可以执行的实验。
三、怎么构建与重跑
真正可执行的命令如下:
cd examples/sort-bench
make正式跑数:
taskset -c 4 ./sort_bench --csv results-current-env.csv用当前 CSV 重画文章里的图:
python3 plot.py \
--csv results-current-env.csv \
--output ../../post/algorithms/06-sort-bench/benchmark-comparison.svg如果想换规模:
./sort_bench --sizes 64,4096,262144 --csv custom.csv四、当前环境与边界
这次重跑环境固定如下:
| 项目 | 本次设置 |
|---|---|
| CPU | 12th Gen Intel Core i9-12900K |
| 架构 | x86-64 Linux |
| 编译器 | GCC 15.2.1 |
| Python | 3.14.3 |
| 绑核方式 | taskset -c 4 单线程 |
| 数据类型 | int32_t |
| 原始结果 | results-current-env.csv |
这里有两个边界必须说清楚:
- 这次图和表是真实数据,但它们比较的是这套仓库内实现。
std::sort/std::stable_sort是标准库实现;pdqsort、TimSort、radix、ska_sort是本文配套的参考实现。所以这些结果应理解为“这套实现的表现”,而不是整个算法家族在所有实现下的绝对排名。 - 当前环境没有
perf,所以旧稿里那些 cache miss、branch miss、IPC 表格全部不保留。没有重测的数据,就不应该继续出现在正文里。
五、当前环境的真实结果
5.1 uniform 随机输入下,不同规模的前三名
| n | 第一名 | 第二名 | 第三名 |
|---|---|---|---|
| 64 | std::sort 159.9 ns |
quick 212.9 ns |
introsort 215.0 ns |
| 1024 | std::sort 4.458 us |
std::stable_sort 5.584 us |
radix_lsd 5.831 us |
| 65536 | radix_lsd 0.327 ms |
radix_msd 0.994 ms |
ska_sort 2.029 ms |
| 1000000 | radix_lsd 6.019 ms |
radix_msd 15.428 ms |
ska_sort 31.125 ms |
这张表足够说明两件事:
- 小规模时,libstdc++ 的
std::sort是最强 baseline。 - 当数据真的是大规模
int32_t且分布接近随机时,基数排序会把比较排序甩开一个量级。
5.2 n=1,000,000 时,各分布前三名
| 分布 | 第一名 | 第二名 | 第三名 |
|---|---|---|---|
| uniform | radix_lsd 6.019 ms |
radix_msd 15.428 ms |
ska_sort 31.125 ms |
| normal | radix_lsd 5.839 ms |
radix_msd 9.878 ms |
ska_sort 18.810 ms |
| sorted | TimSort 0.210 ms |
pdqsort 0.329 ms |
std::sort 4.609 ms |
| reverse | TimSort 0.518 ms |
std::sort 3.416 ms |
pdqsort 5.139 ms |
| sawtooth | TimSort 4.120 ms |
std::stable_sort 5.304 ms |
radix_msd 9.142 ms |
| pipe_organ | TimSort 1.107 ms |
std::stable_sort 6.654 ms |
radix_lsd 9.676 ms |
| high_dup | radix_lsd 4.371 ms |
radix_msd 6.945 ms |
ska_sort 11.568 ms |
这里最值得注意的是:这次真实重跑里,TimSort 对结构化输入非常强,而当前仓库这版 pdqsort 并没有出现“全分布通吃”的表现。
5.3 几个最值得看的对照
下面只挑最常见的几类输入,看几个代表性算法:
| 分布,n=1,000,000 | std::sort |
pdqsort |
TimSort |
radix_lsd |
观察 |
|---|---|---|---|---|---|
| uniform | 47.153 ms | 76.158 ms | 73.883 ms | 6.019 ms | 随机 int32_t 上基数排序碾压比较排序 |
| sorted | 4.609 ms | 0.329 ms | 0.210 ms | 11.772 ms | 有结构输入时自适应排序巨大受益 |
| reverse | 3.416 ms | 5.139 ms | 0.518 ms | 12.138 ms | TimSort 能有效利用 run 和反转结构 |
| high_dup | 26.954 ms | 43.675 ms | 52.429 ms | 4.371 ms | 高重复整数上基数排序仍然最稳 |
另外,heap_sort 在 uniform 1e6 上是 88.122
ms,在 sorted 上是 37.808 ms,在 reverse 上是 42.824
ms,几乎在所有大规模输入里都靠后。这个方向判断和旧稿一致:堆排序更像保底策略,而不是默认首选。
六、这次重跑后,哪些结论还能站住
6.1 还站得住的结论
- 没有一个算法在所有分布上都最快。 这次环境里,random/high-dup 是 radix 家族赢,sorted/reverse/pipe_organ 是 TimSort 赢。
- 对大规模
int32_t,radix_lsd的优势非常实。 在 uniform 1e6 上,它比std::sort快约 \(47.153 / 6.019 \approx 7.8\) 倍。 - TimSort 对有结构的数据极其敏感。 在
sorted 1e6 上,
TimSort0.210 ms,std::sort4.609 ms,差了约 22 倍。 std::sort仍然是最可靠的比较排序 baseline。 在 uniform 的 64 和 1024 上,它都是第一;在 uniform 1e6 上,它也仍然是比较排序里最快的一档。- heapsort 依旧不适合作为默认选择。 它存在的主要工程价值还是给 introsort 兜底最坏情况。
6.2 站不住,或者不能这么写的结论
- “pdqsort 是通用场景最佳选择”
这句话不能继续保留。 这次真实重跑里,当前仓库这版
pdqsort在 uniform / normal / high_dup 上都不占优,只在 sorted 上表现很好。 - 旧稿里的 perf / cache / branch
数字必须删。 当前环境没有
perf,所以这些数字不是这次 bench 产物。 - 旧稿里关于大对象、并行排序、1e8 规模的具体数字也不该保留。 这次并没有按那个口径重跑,就不要伪装成实测结果。
七、怎么在你的机器上复现
最短路径就是这三条命令:
cd examples/sort-bench
make
taskset -c 4 ./sort_bench --csv results-current-env.csv重画文章里的 SVG:
python3 plot.py \
--csv results-current-env.csv \
--output ../../post/algorithms/06-sort-bench/benchmark-comparison.svg如果你要修改规模:
./sort_bench --sizes 64,4096,262144 --csv custom.csv八、当前环境下的选型建议
如果你的问题和这次 bench
的口径接近,也就是单线程、int32_t、内存内排序,那我会给出更保守、也更诚实的一版建议:
- 随机或接近随机的大规模整数:优先看
radix_lsd。 - 明显有序、部分有序、run
很长的输入:优先看
TimSort。 - 只是想要一个稳妥的通用 baseline:先用
std::sort,不要先入为主切到手写pdqsort。 - 必须稳定排序,但输入又不是强结构化:先用
std::stable_sort;如果数据本身就带 run,再测TimSort。 - heapsort 不要直接选。 让 introsort 在需要时退回堆排序就够了。
最后还是那句老话:永远实测。 但这次至少不再是“文章里说能跑”,而是仓库里真的有代码、真的能跑、真的能产出 CSV 和 SVG 了。
参考文献
- Musser, D.R. “Introspective Sorting and Selection Algorithms.” Software: Practice and Experience, 27(8), 1997.
- Peters, O. “Pattern-defeating Quicksort.” arXiv:2106.05123, 2021.
- McIlroy, P.M. “Optimistic Sorting and Information Theoretic Complexity.” SODA, 1993.
- Skarupke, M. “ska_sort: A Radix Sort Implementation.” 2016.
- Peters, T. “TimSort.” CPython source, 2002.
- Drepper, U. “What Every Programmer Should Know About Memory.” 2007.
- Fog, A. “Optimizing Software in C++.” 2023.
- Edelkamp, S., Weiss, A. “BlockQuicksort.” ESA 2016.
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
排序算法专题:从 TimSort 到并行排序
把 TimSort、pdqsort、radix sort、external sort、parallel sort 与 benchmark 串成一条阅读路径。先读哪篇、什么时候选哪种排序,这一页讲清。
TimSort 深度解剖:Python 与 Java 默认排序的精妙设计
为什么 Python 和 Java 不约而同选择了同一个排序算法?TimSort 对真实数据的哪些特征做了针对性优化,使它在工程实践中击败了教科书算法?
pdqsort:击败所有对手的模式自适应排序
Rust 的 sort_unstable 和 C++ Boost.Sort 背后的 pdqsort 如何做到在几乎所有数据分布上都表现优异?模式自适应意味着什么?
基数排序:打破比较下界的正确姿势
比较排序有 O(n log n) 的理论下界,基数排序如何绕过这个限制?它在什么场景下真正有优势,又为什么没有成为通用排序的首选?