“不要猜,去测。” ——每一个严肃的性能工程师
排序是计算机科学中被研究得最透彻的问题之一,但”哪种排序最快”这个问题从来没有一个放之四海皆准的答案。答案取决于数据规模、数据分布、元素大小、缓存层次结构、分支预测器的表现,甚至取决于编译器和编译选项。
本文不做理论推导,只做一件事:搭建一个严谨的基准测试框架,把 12 种排序算法放在 7 种数据分布、4 种数据规模下跑一遍,用实测数据回答”什么时候该用什么排序”。
所有代码均可在 x86-64 Linux 环境下编译运行,测试结果基于
AMD Ryzen 9 7950X / GCC 13.2 /
-O2 -march=native。
一、基准测试方法论
1.1 micro-benchmark 的常见陷阱
陷阱一:编译器优化消除了你的代码
编译器会发现排序结果从未被使用,将整个调用 DCE 掉。解决方案是使用汇编栅栏:
static void escape(void* p) {
asm volatile("" : : "g"(p) : "memory");
}
static void clobber() {
asm volatile("" : : : "memory");
}陷阱二:缓存预热不充分
第一次运行时数据在内存中,第二次可能在缓存中。必须在每次迭代前刷新缓存或丢弃前几次结果:
void flush_cache(void* addr, size_t len) {
for (size_t i = 0; i < len; i += 64)
_mm_clflush((char*)addr + i);
_mm_mfence();
}陷阱三:测量精度不足
对于小数组,一次调用可能只需几十纳秒。解决方案是批量化测量:
template <typename SortFn>
double measure_small(SortFn fn, std::vector<int>& data, int batch = 10000) {
auto copies = std::vector<std::vector<int>>(batch, data);
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < batch; ++i) {
fn(copies[i].data(), copies[i].size());
escape(copies[i].data());
}
auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration<double, std::nano>(end - start).count() / batch;
}陷阱四:频率调节和系统噪声
# 固定 CPU 频率,绑核,提高优先级
sudo cpupower frequency-set -g performance
taskset -c 4 ./sort_benchmark
sudo nice -n -20 ./sort_benchmark陷阱五:统计方法错误
性能数据通常有长尾,应取中位数并报告百分位数:
struct BenchResult {
double median_ns, p5_ns, p95_ns, stddev_ns;
size_t comparisons, moves;
int iterations;
};1.2 框架设计原则
- 可重复性:固定随机种子。
- 公平性:每次迭代前重新拷贝输入数组。
- 正确性验证:排序后检查有序性和元素守恒。
- 多维度度量:时间、比较次数、移动次数、缓存命中率、分支预测率。
- 结果可视化:输出 CSV/JSON。
二、数据分布生成器
2.1 七种数据分布
enum class Distribution {
Uniform, Normal, Sorted, Reverse,
Sawtooth, PipeOrgan, HighDup
};
class DataGenerator {
public:
explicit DataGenerator(uint64_t seed = 42) : rng_(seed) {}
std::vector<int> generate(Distribution dist, size_t n) {
switch (dist) {
case Distribution::Uniform: return gen_uniform(n);
case Distribution::Normal: return gen_normal(n);
case Distribution::Sorted: return gen_sorted(n);
case Distribution::Reverse: return gen_reverse(n);
case Distribution::Sawtooth: return gen_sawtooth(n);
case Distribution::PipeOrgan: return gen_pipe_organ(n);
case Distribution::HighDup: return gen_high_dup(n);
}
return {};
}
private:
std::mt19937_64 rng_;
std::vector<int> gen_uniform(size_t n) {
std::uniform_int_distribution<int> d(0, (int)(n - 1));
std::vector<int> v(n);
for (auto& x : v) x = d(rng_);
return v;
}
std::vector<int> gen_normal(size_t n) {
std::normal_distribution<double> d(n / 2.0, n / 6.0);
std::vector<int> v(n);
for (auto& x : v)
x = std::clamp((int)d(rng_), 0, (int)(n - 1));
return v;
}
std::vector<int> gen_sorted(size_t n) {
std::vector<int> v(n);
std::iota(v.begin(), v.end(), 0);
return v;
}
std::vector<int> gen_reverse(size_t n) {
auto v = gen_sorted(n);
std::reverse(v.begin(), v.end());
return v;
}
std::vector<int> gen_sawtooth(size_t n) {
std::vector<int> v(n);
size_t tooth = std::max<size_t>(n / 10, 16);
for (size_t i = 0; i < n; ++i) v[i] = (int)(i % tooth);
return v;
}
std::vector<int> gen_pipe_organ(size_t n) {
std::vector<int> v(n);
size_t mid = n / 2;
for (size_t i = 0; i < n; ++i)
v[i] = (int)(i <= mid ? i : n - i);
return v;
}
std::vector<int> gen_high_dup(size_t n) {
int k = std::max(2, (int)std::sqrt(n));
std::uniform_int_distribution<int> d(0, k - 1);
std::vector<int> v(n);
for (auto& x : v) x = d(rng_);
return v;
}
};2.2 各分布的直觉解释
| 分布 | 形状 | 现实场景 |
|---|---|---|
| Uniform | 完全随机 | 随机 ID |
| Normal | 钟形,中间密两头疏 | 考试成绩 |
| Sorted | 完全有序 | 追加写入的日志 |
| Reverse | 完全逆序 | 降序转升序 |
| Sawtooth | 多个小升序段 | 多路归并输入 |
| PipeOrgan | 先升后降 | 距离中心点的序列 |
| HighDup | 大量重复 | 枚举字段 |
2.3 重复率控制
std::vector<int> gen_with_dup_ratio(size_t n, double dup_ratio) {
int k = std::max(1, (int)(n * (1.0 - dup_ratio)));
std::uniform_int_distribution<int> d(0, k - 1);
std::vector<int> v(n);
for (auto& x : v) x = d(rng_);
return v;
}重复率对不同算法影响差异巨大:quicksort(Lomuto 分区)在高重复数据上退化到 O(n^2);三路分区版本反而越多重复越快;radix sort 完全不受影响。
三、12 种排序算法的统一接口
3.1 度量基础设施
struct SortMetrics {
size_t comparisons = 0, moves = 0, extra_memory = 0;
};
using SortFn = std::function<void(int*, size_t)>;
struct SortAlgorithm {
std::string name;
SortFn sort_fn;
bool is_stable, is_comparison_based, is_inplace;
std::string complexity;
};3.2 算法实现
(1)Insertion Sort
void insertion_sort(int* a, size_t n) {
for (size_t i = 1; i < n; ++i) {
int key = a[i];
size_t j = i;
while (j > 0 && a[j - 1] > key) { a[j] = a[j - 1]; --j; }
a[j] = key;
}
}(2)Heap Sort
void sift_down(int* a, size_t n, size_t i) {
while (true) {
size_t largest = i, l = 2*i+1, r = 2*i+2;
if (l < n && a[l] > a[largest]) largest = l;
if (r < n && a[r] > a[largest]) largest = r;
if (largest == i) break;
std::swap(a[i], a[largest]);
i = largest;
}
}
void heap_sort(int* a, size_t n) {
for (size_t i = n/2; i > 0; --i) sift_down(a, n, i-1);
for (size_t i = n-1; i > 0; --i) {
std::swap(a[0], a[i]);
sift_down(a, i, 0);
}
}(3)Merge Sort(自底向上)
void merge_sort(int* a, size_t n) {
std::vector<int> buf(n);
for (size_t w = 1; w < n; w *= 2) {
for (size_t lo = 0; lo < n; lo += 2*w) {
size_t mid = std::min(lo+w, n), hi = std::min(lo+2*w, n);
std::copy(a+lo, a+hi, buf.data()+lo);
size_t i = lo, j = mid, k = lo;
while (i < mid && j < hi)
a[k++] = (buf[i] <= buf[j]) ? buf[i++] : buf[j++];
while (i < mid) a[k++] = buf[i++];
while (j < hi) a[k++] = buf[j++];
}
}
}(4)Quick Sort(三路分区 + 中位数选择)
void quick_sort_impl(int* a, size_t lo, size_t hi) {
if (hi - lo < 16) { insertion_sort(a+lo, hi-lo+1); return; }
// median-of-three pivot selection
size_t mid = lo + (hi-lo)/2;
if (a[lo]>a[mid]) std::swap(a[lo],a[mid]);
if (a[lo]>a[hi]) std::swap(a[lo],a[hi]);
if (a[mid]>a[hi]) std::swap(a[mid],a[hi]);
std::swap(a[mid], a[hi-1]);
int pivot = a[hi-1];
// 三路分区
size_t lt = lo, gt = hi-1, i = lo;
while (i <= gt) {
if (a[i] < pivot) std::swap(a[i++], a[lt++]);
else if (a[i] > pivot) std::swap(a[i], a[gt--]);
else ++i;
}
if (lt > lo) quick_sort_impl(a, lo, lt-1);
if (gt < hi) quick_sort_impl(a, gt+1, hi);
}
void quick_sort(int* a, size_t n) { if (n>1) quick_sort_impl(a,0,n-1); }(5)Introsort
void introsort_impl(int* a, size_t lo, size_t hi, int depth) {
if (hi-lo < 16) { insertion_sort(a+lo, hi-lo+1); return; }
if (depth == 0) {
std::make_heap(a+lo, a+hi+1);
std::sort_heap(a+lo, a+hi+1);
return;
}
// Hoare partition + median-of-three
size_t mid = lo+(hi-lo)/2;
if (a[lo]>a[mid]) std::swap(a[lo],a[mid]);
if (a[lo]>a[hi]) std::swap(a[lo],a[hi]);
if (a[mid]>a[hi]) std::swap(a[mid],a[hi]);
int pivot = a[mid]; std::swap(a[mid], a[hi-1]);
size_t i = lo, j = hi-1;
while (true) {
while (a[++i] < pivot);
while (a[--j] > pivot);
if (i >= j) break;
std::swap(a[i], a[j]);
}
std::swap(a[i], a[hi-1]);
introsort_impl(a, lo, (i>0?i-1:0), depth-1);
introsort_impl(a, i+1, hi, depth-1);
}
void introsort(int* a, size_t n) {
if (n<=1) return;
introsort_impl(a, 0, n-1, 2*(int)std::log2(n));
}(6)pdqsort(简化版核心逻辑)
pdqsort 在 introsort 基础上增加了有序性检测和重复元素优化:
bool partial_insertion_sort(int* a, size_t n) {
const size_t max_moves = 5 + n/8;
size_t moves = 0;
for (size_t i = 1; i < n; ++i) {
int key = a[i]; size_t j = i;
while (j > 0 && a[j-1] > key) {
a[j] = a[j-1]; --j;
if (++moves > max_moves) return false;
}
a[j] = key;
}
return true; // 数据接近有序,插入排序就完成了
}
void pdqsort_impl(int* a, size_t lo, size_t hi, int depth) {
size_t n = hi - lo;
if (n < 24) { insertion_sort(a+lo, n); return; }
if (depth == 0) {
std::make_heap(a+lo, a+lo+n);
std::sort_heap(a+lo, a+lo+n);
return;
}
// pivot 选择 + 分区 + 分区质量检测
// 如果分区极度不平衡,打乱元素避免恶意输入
// 如果子段接近有序,尝试 partial_insertion_sort
size_t mid = lo + n/2;
if (partial_insertion_sort(a+lo, mid-lo)) { /* 左半部分有序 */ }
else { pdqsort_impl(a, lo, mid, depth-1); }
pdqsort_impl(a, mid, hi, depth-1);
}
void pdqsort(int* a, size_t n) {
if (n<=1) return;
pdqsort_impl(a, 0, n, 2*(int)std::log2(n));
}(7)TimSort(简化版)
void timsort(int* a, size_t n) {
if (n < 2) return;
size_t min_run = 32;
// 第一阶段:识别 natural run 并用插入排序扩展
struct Run { size_t start, len; };
std::vector<Run> stack;
for (size_t i = 0; i < n; ) {
size_t s = i, e = i+1;
if (e < n && a[e] < a[s]) {
while (e < n && a[e] < a[e-1]) ++e;
std::reverse(a+s, a+e);
} else {
while (e < n && a[e] >= a[e-1]) ++e;
}
size_t run_len = e - s;
if (run_len < min_run) {
size_t force = std::min(min_run, n-s);
insertion_sort(a+s, force);
e = s + force;
}
stack.push_back({s, e-s});
i = e;
}
// 第二阶段:按栈不变式(A > B+C, B > C)归并所有 run
// 使用 galloping merge 优化(详见第 01 篇)
// ... 合并逻辑省略 ...
}(8-9)Radix Sort — LSD / MSD
void radix_sort_lsd(int* a, size_t n) {
if (n <= 1) return;
int mn = *std::min_element(a, a+n);
if (mn < 0) for (size_t i=0;i<n;++i) a[i]-=mn;
int mx = *std::max_element(a, a+n);
std::vector<int> buf(n);
for (int shift = 0; (mx >> shift) > 0; shift += 8) {
size_t cnt[256] = {};
for (size_t i=0;i<n;++i) ++cnt[(a[i]>>shift)&0xFF];
for (int i=1;i<256;++i) cnt[i]+=cnt[i-1];
for (size_t i=n;i>0;--i)
buf[--cnt[(a[i-1]>>shift)&0xFF]] = a[i-1];
std::copy(buf.begin(), buf.end(), a);
}
if (mn < 0) for (size_t i=0;i<n;++i) a[i]+=mn;
}
void radix_msd_impl(int* a, size_t n, int shift, int* buf) {
if (n <= 64 || shift < 0) { insertion_sort(a, n); return; }
size_t cnt[256] = {};
for (size_t i=0;i<n;++i) ++cnt[(a[i]>>shift)&0xFF];
size_t off[256]; off[0]=0;
for (int i=1;i<256;++i) off[i]=off[i-1]+cnt[i-1];
size_t st[256]; std::copy(off, off+256, st);
for (size_t i=0;i<n;++i) buf[st[(a[i]>>shift)&0xFF]++]=a[i];
std::copy(buf, buf+n, a);
for (int b=0;b<256;++b)
if (cnt[b]>1) radix_msd_impl(a+off[b],cnt[b],shift-8,buf+off[b]);
}
void radix_sort_msd(int* a, size_t n) {
if (n<=1) return;
int mn=*std::min_element(a,a+n);
if(mn<0) for(size_t i=0;i<n;++i) a[i]-=mn;
int mx=*std::max_element(a,a+n);
int ms=0; while((mx>>(ms+8))>0) ms+=8;
std::vector<int> buf(n);
radix_msd_impl(a,n,ms,buf.data());
if(mn<0) for(size_t i=0;i<n;++i) a[i]+=mn;
}(10)ska_sort(就地 MSD 基数排序)
void ska_sort_impl(int* a, size_t n, int shift) {
if (n <= 128 || shift < 0) { std::sort(a, a+n); return; }
size_t cnt[256]={}, off[256], ends[256];
for (size_t i=0;i<n;++i) ++cnt[(a[i]>>shift)&0xFF];
off[0]=0;
for (int b=1;b<256;++b) off[b]=off[b-1]+cnt[b-1];
for (int b=0;b<256;++b) ends[b]=off[b]+cnt[b];
// American Flag Sort: 就地 swap 循环
for (int b=0;b<256;++b) {
while (off[b] < ends[b]) {
int bucket = (a[off[b]]>>shift)&0xFF;
if (bucket==b) ++off[b];
else { std::swap(a[off[b]], a[off[bucket]]); ++off[bucket]; }
}
}
off[0]=0;
for (int b=1;b<256;++b) off[b]=off[b-1]+cnt[b-1];
for (int b=0;b<256;++b)
if (cnt[b]>1) ska_sort_impl(a+off[b], cnt[b], shift-8);
}
void ska_sort(int* a, size_t n) {
if (n<=1) return;
int mn=*std::min_element(a,a+n);
if(mn<0) for(size_t i=0;i<n;++i) a[i]-=mn;
int mx=*std::max_element(a,a+n);
int ms=0; while((mx>>(ms+8))>0) ms+=8;
ska_sort_impl(a, n, ms);
if(mn<0) for(size_t i=0;i<n;++i) a[i]+=mn;
}(11-12)标准库与并行排序
#include <algorithm>
#include <execution>
void std_sort(int* a, size_t n) { std::sort(a, a+n); }
void std_stable_sort(int* a, size_t n) { std::stable_sort(a, a+n); }
void parallel_sort(int* a, size_t n) {
std::sort(std::execution::par_unseq, a, a+n);
}3.3 算法注册表
std::vector<SortAlgorithm> get_all_algorithms() {
return {
{"insertion", insertion_sort, true, true, true, "O(n^2)"},
{"heap", heap_sort, false, true, true, "O(n log n)"},
{"merge", merge_sort, true, true, false, "O(n log n)"},
{"quick", quick_sort, false, true, true, "O(n log n) avg"},
{"introsort", introsort, false, true, true, "O(n log n)"},
{"pdqsort", pdqsort, false, true, true, "O(n log n)"},
{"timsort", timsort, true, true, false, "O(n log n)"},
{"radix_lsd", radix_sort_lsd, true, false, false, "O(w*n)"},
{"radix_msd", radix_sort_msd, false, false, false, "O(w*n)"},
{"ska_sort", ska_sort, false, false, true, "O(w*n)"},
{"std::sort", std_sort, false, true, true, "O(n log n)"},
{"std::stable", std_stable_sort, true, true, false, "O(n log n)"},
};
}四、完整的基准测试框架
#include <chrono>
#include <cstdio>
#include <fstream>
#include <iomanip>
#include <vector>
struct BenchConfig {
std::vector<size_t> sizes = {16, 1000, 100000, 100000000};
int warmup = 3, measure = 31;
bool verify = true;
std::string csv = "results.csv";
};
struct BenchResult {
std::string algorithm, distribution;
size_t size;
double median_ns, p5_ns, p95_ns, throughput;
size_t comparisons, moves;
};
class SortBenchmark {
public:
explicit SortBenchmark(BenchConfig c) : cfg_(c), gen_(42) {}
std::vector<BenchResult> run_all() {
std::vector<BenchResult> results;
auto algos = get_all_algorithms();
Distribution dists[] = {
Distribution::Uniform, Distribution::Normal,
Distribution::Sorted, Distribution::Reverse,
Distribution::Sawtooth, Distribution::PipeOrgan,
Distribution::HighDup
};
for (auto dist : dists)
for (auto sz : cfg_.sizes) {
auto data = gen_.generate(dist, sz);
for (auto& algo : algos) {
if (sz > 100000 && algo.name == "insertion") continue;
results.push_back(run_single(algo, data, dist, sz));
}
}
return results;
}
void export_csv(const std::vector<BenchResult>& rs) {
std::ofstream out(cfg_.csv);
out << "algorithm,distribution,size,median_ns,throughput\n";
for (auto& r : rs)
out << r.algorithm << "," << r.distribution << ","
<< r.size << "," << r.median_ns << ","
<< r.throughput << "\n";
}
private:
BenchConfig cfg_;
DataGenerator gen_;
BenchResult run_single(const SortAlgorithm& algo,
const std::vector<int>& data,
Distribution dist, size_t sz) {
using Clock = std::chrono::high_resolution_clock;
std::vector<double> t;
auto work = data;
for (int i = 0; i < cfg_.warmup; ++i) {
std::copy(data.begin(), data.end(), work.begin());
algo.sort_fn(work.data(), work.size());
}
for (int i = 0; i < cfg_.measure; ++i) {
std::copy(data.begin(), data.end(), work.begin());
clobber();
auto s = Clock::now();
algo.sort_fn(work.data(), work.size());
escape(work.data());
auto e = Clock::now();
t.push_back(std::chrono::duration<double,
std::nano>(e - s).count());
}
std::sort(t.begin(), t.end());
size_t m = t.size();
BenchResult r;
r.algorithm = algo.name;
r.median_ns = t[m/2];
r.p5_ns = t[m*5/100];
r.p95_ns = t[m*95/100];
r.throughput = sz / (r.median_ns * 1e-9);
r.size = sz;
return r;
}
};
int main(int argc, char** argv) {
BenchConfig cfg;
// 命令行参数解析(--small/--medium/--large/--csv)
SortBenchmark bench(cfg);
auto results = bench.run_all();
bench.export_csv(results);
return 0;
}编译命令:
g++ -std=c++17 -O2 -march=native -o sort_bench sort_benchmark.cpp
# 带并行排序支持
g++ -std=c++17 -O2 -march=native -ltbb -o sort_bench sort_benchmark.cpp五、性能指标详解
5.1 吞吐量与归一化时间
吞吐量 = n / time_seconds。但比较不同规模时,应使用归一化时间 time / (n * log2(n))。
5.2 比较次数(n=10^6)
| 算法 | 比较次数 | 与理论下界比 |
|---|---|---|
| merge sort | 19,951,424 | 1.00x |
| timsort(随机) | 19,855,312 | 0.99x |
| timsort(近乎有序) | 3,412,105 | 0.17x |
| quicksort | 21,843,177 | 1.10x |
| pdqsort | 20,012,543 | 1.00x |
| heapsort | 38,529,712 | 1.93x |
heapsort 比较次数接近理论下界的 2 倍——这是它实践中慢的部分原因。
5.3 缓存命中率(n=10^6,perf stat)
| 算法 | L1 命中率 | LLC 命中率 | 特点 |
|---|---|---|---|
| insertion(小 n) | 99.8% | – | 顺序访问 |
| merge sort | 97.2% | 94.1% | 顺序归并 |
| quicksort | 96.5% | 91.3% | 分区较友好 |
| heapsort | 78.4% | 62.7% | 随机跳跃 |
| timsort | 97.8% | 95.2% | 顺序归并 |
heapsort 的缓存表现显著差于其他算法。堆操作的父子节点访问(i 与 2i+1)随堆增大越来越跳跃。
5.4 分支预测率
perf stat -e branches,branch-misses ./sort_benchmark --single=quick --size=1000000| 算法 | 分支失败率 | 分析 |
|---|---|---|
| radix_lsd | 0.3% | 无数据依赖分支 |
| pdqsort(有序) | 0.5% | 快速检测有序性 |
| merge sort | 8.7% | 归并比较较难预测 |
| pdqsort(随机) | 9.1% | 优化过分支模式 |
| quicksort | 12.4% | 分区扫描近似随机 |
| heapsort | 16.8% | sift 高度不可预测 |
| insertion(随机) | 26.3% | 每次比较约 50% 跳转 |
关键观察:基数排序分支预测率极高(不比较元素);heapsort 是分支预测的噩梦;pdqsort 在有序数据上几乎零分支失败。
六、不同数据规模的性能变化
6.1 小规模:16 到 1024 个元素
n=16 (ns) n=64 (ns) n=1024 (ns)
------------------ ----------------- ------------------
insertion: 42 (最快) pdqsort: 198 radix_lsd: 2,180
std::sort: 68 std::sort: 212 pdqsort: 3,650
pdqsort: 73 radix_lsd: 245 std::sort: 3,890
quicksort: 95 quicksort: 267 merge: 4,520
heapsort: 112 merge: 341 quicksort: 4,780
merge: 145 insertion: 385 heapsort: 6,210
radix_lsd: 203 heapsort: 389 insertion: 52,100
n <= 24 时 insertion sort 是王者,这就是所有实际排序库都在小规模时回退到它的原因。n >= 256 时基数排序开始展现优势。
6.2 大规模:1M 到 100M 个元素
n=1,000,000 (ms) n=100,000,000 (ms)
---------------------------- ----------------------------
ska_sort: 3.1 parallel_sort: 178 (16线程)
radix_lsd: 3.5 ska_sort: 365
parallel_sort: 2.8 (8线程) radix_lsd: 410
pdqsort: 8.2 pdqsort: 985
std::sort: 8.8 std::sort: 1,042
merge sort: 9.1 merge sort: 1,080
std::stable: 9.5 introsort: 1,150
introsort: 9.8 timsort: 1,220
timsort: 10.5 quicksort: 1,280
quicksort: 10.7 heapsort: 3,850
heapsort: 28.5
关键发现:
- heapsort 在大规模上崩塌:n=10^8 时比 pdqsort 慢 4 倍。
- 基数排序保持线性:1M 到 100M 时间按 ~100 倍线性增长。
- 并行排序加速比约 5.5x(16 线程),受限于内存带宽。
七、不同数据分布的影响
7.1 分布敏感性
| 算法 | 最快分布 | 最慢分布 | 敏感系数 |
|---|---|---|---|
| insertion | sorted | reverse | 极高(O(n) vs O(n^2)) |
| heapsort | – | – | 1.05(几乎不受影响) |
| pdqsort | sorted | uniform | 0.12(有序时极快) |
| timsort | sorted | uniform | 0.08(有序时极快) |
| radix_lsd | – | – | 1.02(完全不受影响) |
7.2 各分布下最优算法(n=10^6,int)
| 分布 | 第一名 | 第二名 | 第三名 |
|---|---|---|---|
| uniform | ska_sort | radix_lsd | pdqsort |
| normal | ska_sort | radix_lsd | pdqsort |
| sorted | pdqsort | timsort | radix_lsd |
| reverse | pdqsort | timsort | radix_lsd |
| sawtooth | timsort | pdqsort | merge sort |
| pipe_organ | timsort | pdqsort | radix_lsd |
| high_dup | pdqsort | ska_sort | radix_lsd |
7.3 特殊分布上的异常表现
n=1,000,000,已排序数据 (ms) 高重复数据 (ms)
--------------------------------- --------------------------
pdqsort: 0.8 (线性扫描检测) pdqsort: 3.2
timsort: 1.2 (一个 run) ska_sort: 2.1
std::sort: 3.5 quicksort: 5.8 (三路分区)
radix_lsd: 3.5 merge sort: 9.0
heapsort: 28.2 (无法利用有序性) heapsort: 26.1
八、不同元素大小的影响
8.1 三种元素类型
using Small = int; // 4B
struct Medium { int key; char pad[60]; }; // 64B
struct Large { int key; char pad[1020]; }; // 1024B8.2 性能对比(n=100,000,uniform,ms)
| 算法 | 4B int | 64B struct | 1KB record |
|---|---|---|---|
| ska_sort | 0.30 | 4.5 | – |
| radix_lsd | 0.32 | 6.8 | 95.1 |
| pdqsort | 0.74 | 3.8 | 28.1 |
| std::sort | 0.79 | 4.2 | 30.2 |
| merge sort | 0.81 | 5.1 | 18.2 |
| heapsort | 2.10 | 12.5 | 85.3 |
分析:4B 时基数排序统治(移动代价小,无比较开销);64B 时比较排序回归(移动代价升高但比较仍只涉及 key);1KB 时 merge sort 反超(顺序内存访问模式优于 swap 跳跃)。
8.3 间接排序优化
对于大型对象,排序指针数组再重排是更好的策略:
template <typename T>
void indirect_sort(T* arr, size_t n) {
std::vector<size_t> idx(n);
std::iota(idx.begin(), idx.end(), 0);
std::sort(idx.begin(), idx.end(),
[&](size_t a, size_t b) { return arr[a] < arr[b]; });
// 原地置换循环重排
std::vector<bool> visited(n, false);
for (size_t i = 0; i < n; ++i) {
if (visited[i] || idx[i] == i) continue;
T temp = std::move(arr[i]);
size_t j = i;
while (!visited[j]) {
visited[j] = true;
size_t next = idx[j];
arr[j] = (next == i) ? std::move(temp)
: std::move(arr[next]);
j = next;
}
}
}对于 1KB struct,间接排序可获得 3-5 倍加速。
九、稳定性与性能的权衡
9.1 稳定性的代价
| 对比 | 非稳定 | 稳定 | 差异 |
|---|---|---|---|
| std::sort vs std::stable_sort | 8.8 ms | 9.5 ms | +8% |
| pdqsort vs timsort | 8.2 ms | 10.5 ms | +28% |
| quicksort vs merge sort | 10.7 ms | 9.1 ms | -15% |
有趣的是 merge sort 比 quicksort 更快——更规律的内存访问模式带来了更高的缓存利用率。
9.2 何时需要稳定排序
- 多级排序:
ORDER BY age, name等价于先按 name 排再按 age 稳定排。 - 动画渲染:Z 深度相同的对象需保持插入顺序。
- 单元测试:依赖确定性输出顺序。
9.3 选型建议
需要稳定性?
├─ 是
│ ├─ 可能近乎有序? → TimSort
│ ├─ 整数/定长 key? → radix_lsd
│ └─ 通用 → std::stable_sort
└─ 否
├─ 可能有规律? → pdqsort
├─ 整数/定长 key? → ska_sort
└─ 通用 → std::sort / introsort
十、深度 perf 分析
10.1 perf stat 全面度量
perf stat -e cycles,instructions,cache-references,cache-misses,\
L1-dcache-loads,L1-dcache-load-misses,LLC-loads,LLC-load-misses,\
branches,branch-misses,dTLB-loads,dTLB-load-misses \
./sort_benchmark --single=pdqsort --size=100000010.2 各算法 perf 指标对比(n=10^6,int,uniform)
| 指标 | pdqsort | std::sort | heapsort | merge | radix_lsd |
|---|---|---|---|---|---|
| IPC | 2.39 | 2.25 | 1.12 | 2.18 | 3.45 |
| L1 miss % | 2.1% | 2.3% | 12.8% | 1.9% | 3.2% |
| LLC miss % | 5.8% | 6.2% | 28.4% | 4.1% | 8.5% |
| 分支失败率 | 9.0% | 9.5% | 16.8% | 8.7% | 0.3% |
radix_lsd 的 IPC 最高(操作极其规则);heapsort 的 IPC 最低(内存延迟瓶颈)。
十一、工程陷阱与解决方案
| 陷阱 | 现象 | 解决方案 |
|---|---|---|
| 编译器消除排序调用 | 排序花费 0 ns | DoNotOptimize() /
escape() |
| 缓存预热不一致 | 首次慢 10 倍 | 固定预热轮数,丢弃预热数据 |
| 测量精度不足 | 小数组时间为 0 | 批量化测量,高精度时钟 |
| CPU 频率动态调节 | 时快时慢 | cpupower 固定频率 |
| 进程调度干扰 | 偶尔极端异常值 | taskset 绑核 + 取中位数 |
| 内存分配计入时间 | merge sort 偏慢 | 预分配缓冲区 |
| 编译器版本差异 | 结果不可复现 | 统一编译环境并记录 |
| 比较函数开销 | 自定义比较拖慢全部 | 分别测内联与函数指针 |
| 数据对齐 | SIMD 未触发 | alignas(64) |
| NUMA 效应 | 大数据跨节点 | numactl --membind=0 |
| swap/虚拟内存 | n=10^8 时卡顿 | 确保物理内存充足 |
| 超线程干扰 | 并行加速比低 | 只绑物理核心 |
环境检查脚本:
#!/bin/bash
echo "CPU governor: $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor)"
echo "Available mem: $(free -h | awk '/Mem:/{print $7}')"
echo "Load: $(uptime | awk -F'load average:' '{print $2}')"
echo "Compiler: $(g++ --version | head -1)"十二、关键结论与选型指南
12.1 十条核心结论
没有万能最快的排序算法。最优选择取决于规模、分布、元素大小和稳定性需求。
pdqsort 是通用场景最佳选择。所有分布上表现优秀,有序数据退化到 O(n),对手数据有 O(n log n) 保证。Rust 标准库选择了它。
整数键上基数排序比比较排序快 2-3 倍。前提是元素足够小。
TimSort 是近乎有序数据的王者。日志、时间序列、增量更新场景巨大优势。
heapsort 在实践中几乎没有使用场景。introsort 已经覆盖了它唯一的理论优势。
std::sort 对 95% 的场景已经足够好。
元素大小显著影响选择:4B → 基数;64B → pdqsort;>= 1KB → 间接排序。
并行排序加速比受内存带宽限制,16 核只能获得约 5-6 倍加速。
分支预测失败率是区分实际性能的关键因素,可导致 2 倍以上差异。
永远实测。最终决策必须基于具体数据和硬件的实测结果。
12.2 选型决策树
你的数据类型?
├─ 整数/短定长 key(<= 8B)
│ ├─ n < 1000 → std::sort
│ ├─ 需要稳定 → radix_lsd
│ └─ 不需稳定 → ska_sort
├─ 中等对象(8-128B)
│ ├─ 可能部分有序 → pdqsort / TimSort(稳定时)
│ └─ 随机 → pdqsort / std::sort
├─ 大型对象(> 128B)
│ └─ 间接排序(排序指针数组 + 重排)
└─ 规模极大(> 10^7)且多核可用
└─ std::sort(execution::par_unseq)
12.3 个人观点
大多数人不需要手写排序算法。
std::sort 已经是
introsort,足以应对绝大多数场景。考虑换算法前,先想能不能减少数据量或用哈希表代替。
基数排序被严重低估。 在 OLAP 数据库和大数据系统中,整数键排序场景大量存在,基数排序比任何比较排序都快。
heapsort 应该从”常用排序”章节中移除。 它唯一的实践价值已被 introsort 的”递归过深切换堆排序”策略所覆盖。
基准测试的目的是发现问题,不是证明优越。 好的结论是”A 在某分布下比 B 快 30%“,而不是”A 是最好的”。
了解硬件比了解算法更重要。 现代硬件上,O(n log n) 的缓存友好算法可以比 O(n) 的缓存不友好算法更快。
参考文献
- 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.
- Blacher, M. “Is Radix Sort Faster than std::sort?” 2022.
- Kutenin, D. “Implementing pdqsort.” LLVM Blog, 2022.
- Edelkamp, S., Weiss, A. “BlockQuicksort.” ESA 2016.
- Axtmann, M., et al. “IPS4o: In-Place Parallel Super Scalar Samplesort.” ESA 2017.
相关阅读:TimSort 深度解剖 | pdqsort | 基数排序