2026-04-10 · algorithms
排序算法专题:从 TimSort 到并行排序
把 TimSort、pdqsort、radix sort、external sort、parallel sort 与 benchmark 串成一条阅读路径。先读哪篇、什么时候选哪种排序,这一页讲清。


























“不要猜,去测。” ——每一个严肃的性能工程师
排序是计算机科学里最经典的问题之一,但“哪种排序最快”从来没有一个脱离上下文的答案。数据规模、数据分布、元素类型、缓存行为、标准库实现、编译器版本,都会影响最后结果。
本文只做一件事:把仓库里的排序 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
陷阱一:编译器把你的工作优化掉了
如果排序结果没有被观测到,编译器可能直接把整个调用删掉。常见做法是加汇编栅栏:
inline void do_not_optimize(const void* ptr) {
asm volatile("" : : "g"(ptr) : "memory");
}
inline void clobber_memory() {
asm volatile("" : : : "memory");
}陷阱二:小数组太快,计时噪声大于算法本身
几十个元素的排序可能只有几十到几百纳秒,单次测量几乎没有意义。正确做法是 batch 化,把一批相同规模的排序放到同一个计时窗口里,再除以 batch。
陷阱三:只测一次,拿偶然值当结论
这次套件对每个点都做多次采样,并记录中位数、p5、p95。文章里主要引用中位数,因为它比单次最小值更抗调度抖动。
这次套件覆盖的是:
int32_tuniform、normal、sorted、reverse、sawtooth、pipe_organ、high_dup64、1024、65536、1000000insertion、heap、merge、quick、introsort、pdqsort、TimSort、radix_lsd、radix_msd、ska_sort、std::sort、std::stable_sort套件使用固定随机种子 42,小数组走 batch
测量,大数组直接单次计时并重复采样。每轮排序结束后都会检查:
为了把文章从“描述性 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
表格全部不保留。没有重测的数据,就不应该继续出现在正文里。| 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 |
这张表足够说明两件事:
std::sort 是最强
baseline。int32_t
且分布接近随机时,基数排序会把比较排序甩开一个量级。| 分布 | 第一名 | 第二名 | 第三名 |
|---|---|---|---|
| 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 并没有出现“全分布通吃”的表现。
下面只挑最常见的几类输入,看几个代表性算法:
| 分布,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,几乎在所有大规模输入里都靠后。这个方向判断和旧稿一致:堆排序更像保底策略,而不是默认首选。
int32_t,radix_lsd
的优势非常实。 在 uniform 1e6 上,它比
std::sort 快约 \(47.153 / 6.019 \approx 7.8\)
倍。TimSort 0.210
ms,std::sort 4.609 ms,差了约 22 倍。std::sort 仍然是最可靠的比较排序
baseline。 在 uniform 的 64 和 1024
上,它都是第一;在 uniform 1e6
上,它也仍然是比较排序里最快的一档。pdqsort 在 uniform / normal / high_dup
上都不占优,只在 sorted 上表现很好。perf,所以这些数字不是这次 bench 产物。最短路径就是这三条命令:
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。TimSort。std::sort,不要先入为主切到手写
pdqsort。std::stable_sort;如果数据本身就带 run,再测
TimSort。最后还是那句老话:永远实测。 但这次至少不再是“文章里说能跑”,而是仓库里真的有代码、真的能跑、真的能产出 CSV 和 SVG 了。
把当前热点继续串成多页阅读,而不是停在单篇消费。
2026-04-10 · algorithms
把 TimSort、pdqsort、radix sort、external sort、parallel sort 与 benchmark 串成一条阅读路径。先读哪篇、什么时候选哪种排序,这一页讲清。
2025-07-15 · algorithms
为什么 Python 和 Java 不约而同选择了同一个排序算法?TimSort 对真实数据的哪些特征做了针对性优化,使它在工程实践中击败了教科书算法?
2025-07-15 · algorithms
Rust 的 sort_unstable 和 C++ Boost.Sort 背后的 pdqsort 如何做到在几乎所有数据分布上都表现优异?模式自适应意味着什么?
2025-07-15 · algorithms
比较排序有 O(n log n) 的理论下界,基数排序如何绕过这个限制?它在什么场景下真正有优势,又为什么没有成为通用排序的首选?
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。