一周7个版本:排序库重构中的并发bug、缓存玄学与SIMD暗坑

1 阅读2分钟

凌晨三点,我第六次跑测试用例。segfault。

[疲惫]

V2到V9只用了一周,但这一周我学到了过去三年都没学到的——关于CPU怎么“记仇”、缓存怎么“欺骗”、编译器怎么“撒谎”。

并行调度的三个错误假设

我一开始以为工作窃取队列很简单:每个线程一个双端队列,自己从尾部取,偷别人的从头偷。

三个假设:

  1. 原子操作是万能的
  2. 扩容可以原子完成
  3. 内存序不重要

全错了。

错误案例:我用了memory_order_relaxed,结果在ARM上跑出幽灵数据。解决方案是仔细读了一遍C++内存模型,然后全部改成memory_order_seq_cst——性能跌了15%,但至少对了。

缓存分层的三个伪优化

我以为分层基数排序肯定快,因为“符合缓存局部性”。

三个优化尝试:

  1. 预取下一块数据——结果预取太早,挤掉了有用数据
  2. 调整分块大小——手动调了20次,每次结果都不一样
  3. 用non-temporal存储——适合大数组,但小数组反而慢

最后发现最简单的方案最好:8K以下原地排序,8K-100K用64KB缓冲区,更大的用递归MSD减少TLB抖动。

SIMD的三个编译器bug

AVX-512掩码预计算表:

  • GCC 12: 正常工作
  • Clang 15: 静态初始化顺序随机
  • MSVC 2022: 对齐错误,静默崩溃

我用了std::call_once解决Clang问题,加__declspec(align(64))解决MSVC问题。但GCC又出了新问题:它把我的常量表优化掉了。

解决方案是在变量前加volatile。但volatile影响性能,所以最后用了asm volatile("" : : "r"(table) : "memory")内存屏障。

真实失败案例:American Flag排序

我实现了American Flag原地基数排序,理论上零额外内存。

测试时发现它比普通基数排序慢50%。我以为是算法问题,但检查了三天代码都没问题。

最后用perf top发现,它在不停地刷新缓存行——因为原地交换导致缓存一致性协议爆炸式通信。

解决方案?加了个__builtin_prefetch提示CPU预取。就这一行,性能提升40%。

关于开源

你可以fork、可以改、可以发issue骂我代码烂。但不能商用,除非你付钱。

为什么?因为我这一周喝了37杯咖啡,掉了好多头发。如果你用这个赚钱,至少够我买咖啡吧?

代码:``

你试过为了一个bug通宵三天吗?