哪种对齐方式会导致这种性能差异

Which alignment causes this performance difference

有什么问题

我正在对以下代码进行基准测试 for (T& x : v) x = x + x;,其中 T 是 int。 使用 mavx2 编译时,性能会根据某些条件波动 2 次。 这不会在 sse4.2

上重现

我想了解发生了什么。

基准如何工作

我正在使用 Google 基准测试。它旋转循环直到确定时间。

主要基准测试代码:

using T = int;
constexpr std::size_t size = 10'000 / sizeof(T);

NOINLINE std::vector<T> const& data()
{
    static std::vector<T> res(size, T{2});
    return res;
}

INLINE void double_elements_bench(benchmark::State& state)
{
   auto v = data();

   for (auto _ : state) {
       for (T& x : v) x = x + x;
       benchmark::DoNotOptimize(v.data());
   }
}

然后我从基准驱动程序的多个实例调用 double_elements_bench

机器、编译器、选项

我把所有函数对齐到128试了一下,没有效果。

结果

当复制 2 次时,我得到:

------------------------------------------------------------
Benchmark                  Time             CPU   Iterations
------------------------------------------------------------
double_elements_0        105 ns          105 ns      6617708
double_elements_1        105 ns          105 ns      6664185

Vs 重复了 3 次:

------------------------------------------------------------
Benchmark                  Time             CPU   Iterations
------------------------------------------------------------
double_elements_0       64.6 ns         64.6 ns     10867663
double_elements_1       64.5 ns         64.5 ns     10855206
double_elements_2       64.5 ns         64.5 ns     10868602

这也会在更大的数据大小上重现。

性能统计数据

我查找了我知道可能与代码对齐相关的计数器

LSD 缓存(由于几年前的一些安全问题在我的机器上关闭)、DSB 缓存和分支预测器:

LSD.UOPS,idq.dsb_uops,UOPS_ISSUED.ANY,branches,branch-misses

慢case

------------------------------------------------------------
Benchmark                  Time             CPU   Iterations
------------------------------------------------------------
double_elements_0        105 ns          105 ns      6663885
double_elements_1        105 ns          105 ns      6632218

 Performance counter stats for './transform_alignment_issue':

                 0      LSD.UOPS                                                    
    13,830,353,682      idq.dsb_uops                                                
    16,273,127,618      UOPS_ISSUED.ANY                                             
       761,742,872      branches                                                    
            34,107      branch-misses             #    0.00% of all branches        

       1.652348280 seconds time elapsed

       1.633691000 seconds user
       0.000000000 seconds sys 

快速案例

------------------------------------------------------------
Benchmark                  Time             CPU   Iterations
------------------------------------------------------------
double_elements_0       64.5 ns         64.5 ns     10861602
double_elements_1       64.5 ns         64.5 ns     10855668
double_elements_2       64.4 ns         64.4 ns     10867987

 Performance counter stats for './transform_alignment_issue':

                 0      LSD.UOPS                                                    
    32,007,061,910      idq.dsb_uops                                                
    37,653,791,549      UOPS_ISSUED.ANY                                             
     1,761,491,679      branches                                                    
            37,165      branch-misses             #    0.00% of all branches        

       2.335982395 seconds time elapsed

       2.317019000 seconds user
       0.000000000 seconds sys

在我看来两者差不多。

代码:https://github.com/DenisYaroshevskiy/small_benchmarks/blob/ade1ed42fc2113f5ad0a4313dafff5a81f9a0d20/transform_alignment_issue.cc#L1

UPD

我认为这可能是从 malloc

返回的数据的对齐方式

0x4f2720 在快速情况下和 0x8e9310 慢

因此 - 由于 clang 不对齐 - 我们未对齐 reads/writes。 我测试了对齐的转换 - 似乎没有这种变化。

有办法确认吗?

是的,数据未对齐可以解释适合 L1d 的小型阵列速度降低 2 倍的原因。您希望每个其他 load/store 都是 cache-line 拆分,它可能只会减慢 1.5 倍,而不是 2 倍,如果拆分加载或存储花费 2 次访问 L1d 而不是1.

但是它有额外的效果,比如取决于加载结果的 uops 的重播,这显然可以解释其余的问题,要么使 out-of-order exec 不太能够重叠工作和隐藏延迟,要么直接 运行 进入诸如“拆分寄存器”之类的瓶颈。

ld_blocks.no_sr 计数次数 cache-line 拆分加载被临时阻止,因为用于处理拆分访问的所有资源都在使用中。

当加载执行单元检测到加载跨高速缓存行拆分时,它必须将第一部分保存在某处(显然在“拆分寄存器”中),然后访问第二个高速缓存行。在像您这样的英特尔 SnB-family CPU 上,第二次访问不需要 RS 再次将负载 uop 分派到端口;加载执行单元只是在几个周期后执行它。 (但大概不能在与第二次访问相同的周期内接受另一个负载。)

拆分加载的额外延迟,以及等待这些加载结果的 uops 的潜在重放,是另一个因素,但这些也是未对齐加载的相当直接的后果。 ld_blocks.no_sr 的大量计数告诉您 CPU 实际上 运行 超出了拆分寄存器,否则可能会做更多的工作,但由于未对齐的负载本身而不得不停止,而不仅仅是其他效果。

您还可以查找 front-end 由于 ROB 或 RS 已满而导致的停顿,如果您想调查详细信息,但无法执行拆分加载会使这种情况发生得更多。所以可能所有 back-end 停顿都是未对齐加载的结果(如果从存储缓冲区提交到 L1d 也是一个瓶颈,则可能存储。)


On a 100KB I reproduce the issue: 1075ns vs 1412ns. On 1 MB I don't think I see it.

对于大型数组(512 位向量除外),数据对齐通常不会产生太大差异。随着高速缓存行(2x YMM 向量)到达频率降低,back-end 有时间处理未对齐加载/存储的额外开销,并且仍然跟上。硬件预取做得很好,它仍然可以最大化 per-core L3 带宽。对于适合 L2 但不适合 L1d(如 100kiB)的大小,预计会看到较小的效果。

当然,大多数类型的执行瓶颈都会表现出类似的效果,甚至像 un-optimized 代码这样简单的事情,它为数组数据的每个向量做一些额外的 store/reloads。因此,仅此一点并不能证明它是错位导致 do 适合 L1d 的小尺寸(例如 10 KiB)的减速。但这显然是最明智的结论。


代码对齐或其他 front-end 瓶颈似乎不是问题;根据 idq.dsb_uops,您的大部分 uops 来自 DSB。 (相当多的人不是,但慢与快之间的百分比差异不大。)

How can I mitigate the impact of the Intel jcc erratum on gcc? 在像您这样的 Skylake-derived 微架构上可能很重要;甚至有可能这就是为什么你的 idq.dsb_uops 不接近你的 uops_issued.any.