凌晨三点,我第六次跑测试用例。segfault。
[疲惫]
V2到V9只用了一周,但这一周我学到了过去三年都没学到的——关于CPU怎么“记仇”、缓存怎么“欺骗”、编译器怎么“撒谎”。
并行调度的三个错误假设
我一开始以为工作窃取队列很简单:每个线程一个双端队列,自己从尾部取,偷别人的从头偷。
三个假设:
- 原子操作是万能的
- 扩容可以原子完成
- 内存序不重要
全错了。
错误案例:我用了memory_order_relaxed,结果在ARM上跑出幽灵数据。解决方案是仔细读了一遍C++内存模型,然后全部改成memory_order_seq_cst——性能跌了15%,但至少对了。
缓存分层的三个伪优化
我以为分层基数排序肯定快,因为“符合缓存局部性”。
三个优化尝试:
- 预取下一块数据——结果预取太早,挤掉了有用数据
- 调整分块大小——手动调了20次,每次结果都不一样
- 用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通宵三天吗?