100G交换机吞吐下降20%——一次DPDK Hash Cache Locality优化实战(下)

100G交换机吞吐下降20%——一次DPDK Hash Cache Locality优化实战(下)
接上文我们已经定位到Hash算法本身没有退化真正增加的是CPU Backend Stall。那么为什么一次成功的Hash查找仍然会消耗如此多的CPU周期八、真正的瓶颈不是Hash而是Pointer Chasing先来看一次典型的数据访问路径session rte_hash_lookup_data(...); action session-action; stats session-stats; qos session-qos; policer session-policer;很多开发者看到这里只会想到只是读取几个指针而已。实际上。CPU看到的却完全不同。真正发生的是Hash Bucket ↓ Session ↓ Action ↓ QoS ↓ Stats ↓ Policer每一次指针解引用Pointer Dereference都意味着CPU必须重新寻找下一块内存。如果这些对象分散在不同HugePage。或者分散在不同Cache Line。CPU流水线便会不断停止等待。核心知识点四CPU最害怕的不是计算。而是不知道下一块数据在哪里。算术运算通常只需要几个Cycle。而一次LLC Miss可能需要上百Cycle。如果最终访问落到DDR。延迟甚至达到数百Cycle。九、为什么Hardware Prefetch几乎帮不上忙很多人认为现代CPU不是有Hardware Prefetch吗为什么还会等待原因就在于Hardware Prefetch只能预测连续访问。例如A ↓ A64 ↓ A128 ↓ A192CPU很容易提前加载。但是Hash查找以后真正访问的是0x81a0... ↓ 0x4f92... ↓ 0xc817... ↓ 0x1258...完全随机。CPU无法预测下一次访问哪里。于是Prefetch彻底失效。核心知识点五Hardware Prefetch擅长连续访问。Hash Lookup属于随机访问。因此Hash性能最终受限于Memory Latency。而不是CPU主频。十、DPDK为什么大量使用rte_prefetch0()阅读DPDK源码。会发现大量地方都有rte_prefetch0(pkt);或者rte_prefetch0(next_mbuf);很多人误认为Prefetch就是提高Cache命中。实际上真正目的是隐藏Memory Latency。例如错误写法session lookup(pkt); process(session);CPU必须等待Lookup结束才能继续执行。更好的方式next pkts[i 1]; rte_prefetch0(next); process(current);CPU处理当前Packet。同时下一Packet已经进入L1 Cache。这样Memory Latency与业务计算发生重叠。核心知识点六Prefetch不能减少Memory Latency。它真正做的是隐藏Latency。这是两件完全不同的事情。十一、Session布局为什么比Hash算法更重要继续分析旧版本Session。struct session { action * qos * stats * policer * ... };真正热点数据分散四处。CPU每处理一个Packet。都需要不断跳转。后来重新设计Session。struct session { uint32_t action; uint32_t qos; uint64_t counter; uint8_t flags; void *rule; };真正热点全部放入一个Cache Line。只有少量冷数据采用指针。这样绝大多数Packet无需继续Pointer Chasing。十二、DPDK Hash为什么采用Bucket连续布局很多开发者第一次阅读librte_hash都会疑惑为什么Bucket里面首先保存Signature。而不是直接保存Key。原因就是Cache。Bucket通常连续存放。CPU一次Cache Fill即可获得多个Signature。只有Signature匹配以后。才需要继续访问真正Key。这样避免大量随机访问。因此真正优秀的Hash优化目标并不是减少Hash计算。而是减少Cache Miss。核心知识点七高性能Hash。真正优化的是Memory Access Pattern。不是Hash Function。十三、如何验证自己的系统存在Pointer Chasing除了普通perf stat建议增加如下PMU事件perf stat \ -e LLC-load-misses,\ LLC-loads,\ stalled-cycles-backend,\ l1-dcache-load-misses如果观察到Backend Stall持续升高LLC Miss明显增加IPC下降Instructions基本不变那么大概率已经进入Memory Bound。此时继续优化算法。意义已经不大。应该首先优化数据布局。十四、工程优化方案最终进行了以下调整。一、重新设计Session热点字段全部放在前64Bytes。冷数据采用二级对象。二、减少Pointer数量能够直接存储就不要额外malloc。减少随机访问。三、对象连续分配Session统一来自Mempool。保证空间局部性。避免系统malloc造成碎片化。四、增加软件PrefetchHash完成以后。立即Prefetch Session。提前加载热点数据。让CPU在处理当前Packet时。后台完成下一次Cache Fill。十五、优化结果重新压测百万连接。持续12小时。结果如下指标优化前优化后PPS131 Mpps159 MppsP99 Latency7.9 μs5.8 μsIPC1.581.92Backend Stall高显著下降LLC Miss高明显下降整个优化过程中没有修改Hash算法。没有增加CPU。甚至没有改变Hash表大小。只是重新设计数据布局。系统便恢复性能。十六、全文总结很多DPDK开发者会把关注点放在Hash算法、CRC计算、SIMD优化、流水线调度。实际上对于百万连接以上的数据平面。真正限制性能的往往已经不是计算。而是内存访问。CPU可以在极短时间内完成Hash计算却可能因为一次随机指针访问等待上百个Cycle。随着Session对象越来越复杂、指针层级越来越多Pointer Chasing逐渐成为真正的性能瓶颈。因此高性能DPDK系统优化的重点应该从优化算法逐渐转向优化数据布局。良好的Cache Locality、连续内存布局、合理的软件Prefetch以及热点数据聚集往往比更复杂的Hash算法带来更大的收益。全文核心知识点Hash命中率100%并不代表Hash查找效率高。Hash计算通常不是瓶颈Pointer Chasing才是。Hardware Prefetch无法预测随机指针访问。rte_prefetch0()的作用是隐藏延迟而不是消除延迟。Session内存布局比Hash函数优化更重要。连续内存布局能够显著提升Cache Locality。当系统进入Memory Bound阶段应优先优化数据组织方式而不是继续优化算法。