土法炼钢兴趣小组的算法知识备份

排序基准测试:用数据说话

目录

“不要猜,去测。” ——每一个严肃的性能工程师

排序是计算机科学中被研究得最透彻的问题之一,但”哪种排序最快”这个问题从来没有一个放之四海皆准的答案。答案取决于数据规模、数据分布、元素大小、缓存层次结构、分支预测器的表现,甚至取决于编译器和编译选项。

本文不做理论推导,只做一件事:搭建一个严谨的基准测试框架,把 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 框架设计原则

  1. 可重复性:固定随机种子。
  2. 公平性:每次迭代前重新拷贝输入数组。
  3. 正确性验证:排序后检查有序性和元素守恒。
  4. 多维度度量:时间、比较次数、移动次数、缓存命中率、分支预测率。
  5. 结果可视化:输出 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

关键发现:

  1. heapsort 在大规模上崩塌:n=10^8 时比 pdqsort 慢 4 倍。
  2. 基数排序保持线性:1M 到 100M 时间按 ~100 倍线性增长。
  3. 并行排序加速比约 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]; };   // 1024B

8.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 何时需要稳定排序

  1. 多级排序ORDER BY age, name 等价于先按 name 排再按 age 稳定排。
  2. 动画渲染:Z 深度相同的对象需保持插入顺序。
  3. 单元测试:依赖确定性输出顺序。

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=1000000

10.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 十条核心结论

  1. 没有万能最快的排序算法。最优选择取决于规模、分布、元素大小和稳定性需求。

  2. pdqsort 是通用场景最佳选择。所有分布上表现优秀,有序数据退化到 O(n),对手数据有 O(n log n) 保证。Rust 标准库选择了它。

  3. 整数键上基数排序比比较排序快 2-3 倍。前提是元素足够小。

  4. TimSort 是近乎有序数据的王者。日志、时间序列、增量更新场景巨大优势。

  5. heapsort 在实践中几乎没有使用场景。introsort 已经覆盖了它唯一的理论优势。

  6. std::sort 对 95% 的场景已经足够好

  7. 元素大小显著影响选择:4B → 基数;64B → pdqsort;>= 1KB → 间接排序。

  8. 并行排序加速比受内存带宽限制,16 核只能获得约 5-6 倍加速。

  9. 分支预测失败率是区分实际性能的关键因素,可导致 2 倍以上差异。

  10. 永远实测。最终决策必须基于具体数据和硬件的实测结果。

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) 的缓存不友好算法更快。

参考文献

  1. Musser, D.R. “Introspective Sorting and Selection Algorithms.” Software: Practice and Experience, 27(8), 1997.
  2. Peters, O. “Pattern-defeating Quicksort.” arXiv:2106.05123, 2021.
  3. McIlroy, P.M. “Optimistic Sorting and Information Theoretic Complexity.” SODA, 1993.
  4. Skarupke, M. “ska_sort: A Radix Sort Implementation.” 2016.
  5. Peters, T. “TimSort.” CPython source, 2002.
  6. Drepper, U. “What Every Programmer Should Know About Memory.” 2007.
  7. Fog, A. “Optimizing Software in C++.” 2023.
  8. Blacher, M. “Is Radix Sort Faster than std::sort?” 2022.
  9. Kutenin, D. “Implementing pdqsort.” LLVM Blog, 2022.
  10. Edelkamp, S., Weiss, A. “BlockQuicksort.” ESA 2016.
  11. Axtmann, M., et al. “IPS4o: In-Place Parallel Super Scalar Samplesort.” ESA 2017.

算法系列导航上一篇:并行排序 | 下一篇:哈希表内部

相关阅读TimSort 深度解剖 | pdqsort | 基数排序


By .