高性能编码
通用原则,用于编写快速、资源高效的代码。这些模式适用于所有语言和领域——系统编程、科学计算、数据管道和Web服务。
通用原则
在优化之前,回答一个问题:程序是计算密集型(compute-bound)还是带宽密集型(bandwidth-bound)?
计算密集型:ALU是瓶颈。所有核心都在100%,指令以硬件可以消耗的速度发出。
解决方案:减少指令数量,使用SIMD,改进ILP。
带宽密集型:内存子系统是瓶颈。核心在等待数据时停顿。
解决方案:改进数据布局,减少数据移动,增加缓存重用。
一个有用的思维模型是屋顶线(roofline):每个操作都有算术强度(FLOPs每字节加载)。将其与硬件的峰值计算和峰值带宽进行比较。如果强度低于屋顶线的拐点,则是带宽密集型——优化计算不会有帮助。如果高于拐点,则是计算密集型——改进缓存不会有帮助。
在实践中:如果perf stat显示高IPC(> 2)和低缓存缺失率,则可能是计算密集型。如果IPC低(< 1)且缓存缺失或缓存引用高,则可能是带宽密集型。
在GPU上:高SM利用率 + 低内存吞吐量 → 计算密集型。低SM利用率 → 可能是带宽密集型或启动开销密集型。
Amdahl定律设置了并行性的理论上限。如果程序的S分数是串行的,N个核心的最大加速比是1 / (S + (1-S)/N)。对于S = 10%,无限核心最多提供10倍加速。这就是为什么找到和缩小串行分数比添加核心更重要——以及为什么瓶颈(计算、带宽或串行代码)决定了优化策略。
Little定律将并发性与吞吐量联系起来。并发性 = 吞吐量 × 延迟。如果每个任务需要1秒,并且需要100个任务/秒,则需要 ≥ 100个并发工作者。使用此方法从要求中选择max_workers,而不是猜测。
结合Amdahl:串行分数限制了并发性实际上可以提供多少帮助。这决定了接下来的一切。将内存绑定优化应用于计算密集型程序会增加开销而没有任何好处。将计算优化应用于带宽密集型程序不会带来任何改善。在Amdahl限制之外添加并行性会浪费资源。首先回答瓶颈问题。
在做任何事情之前:检查服务器实际上现在有多少可用资源。cpu_count告诉你总核心数,而不是可用核心数。total_ram告诉你安装的内存,而不是可用内存。其他用户、后台服务和昨天遗忘的进程都会消耗资源。使用htop / free -h / nvidia-smi查看当前状态,而不是理论容量。
然后选择一个策略:
CPU绑定工作负载:从保守开始:几个工作者少于可用核心数,而不是总核心数。监视CPU使用情况,然后逐渐增加。
Python:ProcessPoolExecutor用于CPU工作(GIL在CPU任务上串行化线程),ThreadPoolExecutor用于IO等待。
C++:std::thread::hardware_concurrency()返回可用核心数。std::execution::par_unseq(C++17)或OpenMP #pragma omp parallel for用于循环并行性。
Rust:rayon带有par_iter()自动调整线程池大小。使用par_bridge()进行顺序迭代器。
内存绑定工作负载:检查可用内存(free -h,而不是总内存)。估计每个工作者的内存:一个加载2 GB的工作者意味着您不能在32 GB上运行16个工作者。流式/迭代而不是实例化。生成器、迭代器特征和延迟管道可以保持内存恒定,无论数据集大小如何。
GPU内存分析有其自己的方法论——请参阅§3。
IO绑定工作负载:异步/等待通常是正确的答案。但是要注意隐藏的同步调用,它们可能会阻塞事件循环。连接池设置了真正的上限。一个具有5个DB连接的池不能为50个并发任务提供服务,无论您启动多少协程。使用Semaphore(max_concurrent)门而不是无限制地启动任务。通过观察实际资源压力来调整限制。
GPU绑定工作负载:批大小由VRAM限制。使用梯度累积来模拟更大的批大小:损失/ n_accumulate,然后backward(),然后optimizer.step()每N个微批。
稀疏表示非常重要。N个节点的密集邻接矩阵需要O(N²)内存。在N=50K时,这就是2.5B个条目——甚至零也会消耗内存。使用COO/CSR边列表。
数据传输是异步的。tensor.to(device, non_blocking=True)重叠CPU→GPU复制与内核执行,当数据固定时。
容器绑定工作负载(Docker/批量评估):每个容器都是资源消费者——内存、磁盘、CPU。并行策略:重用容器,而不是重新构建。为每个任务构建Docker镜像是昂贵的。构建一次,然后在运行之间重置状态(docker exec git checkout HEAD && git clean -fd)。一个代理任务在N个模式中共享一个容器比重新构建快N倍。