Appearance
SPEC2026 777.zstd
已对数据进行脱敏处理,不影响技术推导。
问题背景
speccpu2026发布后,对该版本的分析马上提上了日程。zstd是新加入的负载,在前期“普查”的数据采集后,发现如下问题:
- 横向对比其它负载,zstd的性能受到L3容量大小的影响。数据层面,包括竞品在内的所有机型裁剪L3后,性能劣化行为一致(减半性能0.x,再减半再0.x),初步判断为负载自身导致。
- A产品多核性能线性度远低于其他产品,即多核测试下,平均到每个核的性能远低于单核测试时的性能。
L3容量对zstd性能的影响
TMA分析
zstd再所有环境中都有明显的mem bound,且发现L1D MPKI(>20)和L1DTLB MPKI(>50)值均不低。继续查看发现三级缓存的miss率都不低,这里已经可以简单解释为什么缩减L3容量会导致性能下降了。
测试用例
zstd有8个测试用例,依次采集特征后发现,L1和L2的miss率稳定,L3的miss率会增加。限制L3容量后,用例的L3 miss率增加曲线的拐点提前。显然,用例设计是和L3容量相关的。
题外话:spec的分数计算方式是根据测试用例的执行时间来计算的,这8个用例的执行时间对总测试时间的贡献基本是一致的,可见benchmark设计的细节之处。
白盒分析
继续黑盒分析对问题解决的帮助有限了。下面需要深入到代码中。
已知zstd的测试命令为:
./zstd -b<compression_level> -e<compression_level up to n> --verbose -i<iterations> input.tar8个测试用例的压缩级别是依次增加的。进一步采集用例的热点后,发现不同压缩级别会选择不同的压缩算法。压缩级别低时,注重压缩速度,使用的是哈希表这样的数据结构;随着压缩级别的增加,注重压缩比,算法会侧重搜索更多的重复项,在高压缩级别下会使用二叉树来检索所有匹配的重复项。
| compression level | hotspot | |
|---|---|---|
| 3 | zstd_decompressBlock_internal.part.0 | 哈希表 |
| 7 | zstd_RowFindBestMatch.constprop.0 | 行搜索 |
| 14 | zstd_DUBT_findBestMatch | 二叉树搜索 |
| 16 | zstd_insertBtAndGetAllMatches | 二叉树搜索,全匹配 |
高级别的压缩算法对内存有更大的需求(需要存储更多的二叉树节点)。压缩级别小于5时,L3 miss率低,汇编热点在数据拷贝上:
// ZSTD_decodeSequence
ZSTD_memcpy(dst, src, size);
stp x0, x1,[x3]高压缩级别下,汇编热点为内存load:
// ZSTD_insertBtAndGetAllMatches
MEM_readST():
ldr x15, [x3, x5]在算法层面,匹配数据时需要在哈希表/树中进行查找,这种无规律的查找模式不利于预取,对应代码片段如下:
cpp
size_t const diff = MEM_readST(pMatch) ^ MEM_readST(pIn);这种难以优化的load行为就造成了MEM bound。
进一步分析源码,zstd内部存在和哈希表和树结构相关的变量hashLog(哈希表记录的条数)和chainLog(链表长度),可以通过这两个变量来推算数据结构占用内存的大小。如:
hashlog=19
chainlog=18
memsize=2^19*4+2^18*4=3M注意chainlog在高压缩级别下使用了二叉树,可能需要乘上2来存储左右叶子节点。且不仅有hashlog和chainlog,还有winlog也会占用一定的空间。
那么白盒来看,高压缩级别确实会占用更多的内存,这和随着压缩级别的增加,L3 miss率增加的现象也是吻合的。
结论
综合黑盒白盒分析,可以得出以下结论:
- zstd性能对L3容量敏感,本质上是cache容量无法覆盖working set导致
- zstd的8个测试用例的压缩级别逐步增加,使用数据结构的体积增加
- zstd整体表现为L3 bound,但仅在压缩级别>x后,理论working set体积超过L3容量,才会出现明显的L3 miss,这类用例对分数贡献约60%
如果设计一个L3容量能完全负载zstd的CPU,zstd在其上的表现应该相当不错。但是从这个负载来看,spec应该是特意设计这样的用例,递增的working set,测试内存子系统的性能。
A产品多核性能/单核性能线性度差
在一众产品中,A产品的多核/单核性能线性度远低于其他产品。其实这里可以初步判断是A产品自身的原因。
问题定位先回归到线性度的计算公式,使用:
线性度=多核分数/单核分数/核数线性度低可能是多核分数低,也可能是单核分数高。普遍来说,单核瓶颈是低于多核瓶颈的。
通过topdown初步定位发现,A产品单核下的mem bound远低于多核下的。
计算mem bound的增长倍率,如下表形式:
| proc | case1 | case2 | ... | case8 |
|---|---|---|---|---|
| A | 多核mem bound/单核mem bound | . | ||
| B | . | . | ||
| ... | ... | ... | ... | |
| G | . | 多核mem bound/单核mem bound |
明显看到A产品的前n个用例的增长倍率远高于其它,而这前n个用例恰恰就是单核下L3 miss低的用例(换句话就是这n个用例单核下L3容量够,但是多核下L3就是瓶颈了)。
这个线索指引我们需要关注A产品的L3 cache了。L3不像L1和L2是核内独占的,而是多核共享的。如果一个负载的working set能被L1+L2覆盖的话,它的线性度应该是1。A产品的L3设计上和其它产品不同,A产品的L3的核存比(core / MB)是最大的,但是L3的容量却不小,这就导致了单核下L3充足而多核下L3紧缺的情况。
设计实验来验证这个假设:在一个numa上只跑x个任务,使任务数/L3核另一台产品一致,再次计算线性度,发现两者的线性度接近。可验证该假设。
结论
zstd仅在产品A上线性度的差的原因是,A产品的L3 cache设计上和其它产品不同,单核下L3充足而多核下L3紧缺。