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 ↓ A+64 ↓ A+128 ↓ A+192CPU很容易提前加载。
但是:
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造成碎片化。
四、增加软件Prefetch
Hash完成以后。
立即Prefetch Session。
提前加载热点数据。
让CPU在处理当前Packet时。
后台完成下一次Cache Fill。
十五、优化结果
重新压测百万连接。
持续12小时。
结果如下:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| PPS | 131 Mpps | 159 Mpps |
| P99 Latency | 7.9 μs | 5.8 μs |
| IPC | 1.58 | 1.92 |
| Backend 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阶段,应优先优化数据组织方式,而不是继续优化算法。