【问题标题】:Understanding number of loads and stores retired in a x86 micro-benchmark了解 x86 微基准测试中停用的加载和存储数量
【发布时间】:2014-11-21 00:21:23
【问题描述】:

我最近一直在使用 perf,但得到了一些我无法理解的结果。 具体来说,数量或已停用的加载和存储与我的预期不符。

我确实编写了一个非常简单的微基准测试,以查看结果在一个非常简单的情况下是否有意义:

#include <stdio.h>

#define STREAM_ARRAY_SIZE   10000000

static double   a[STREAM_ARRAY_SIZE],
    b[STREAM_ARRAY_SIZE],
    c[STREAM_ARRAY_SIZE];


int main(){

    ssize_t j;

    for (j=0; j<STREAM_ARRAY_SIZE; j++) {
        a[j] = 1.0;
        b[j] = 2.0;
        c[j] = 0.0;
    }

    return 0;
}

我用 gcc 4.6.3 编译:

gcc -Wall -O benchmark.c -o benchmark

它确实会编译成一个非常简单的程序集(通过 objdump -d 获得)用于主程序:

00000000004004b4 <main>:
  4004b4:   b8 00 00 00 00          mov    $0x0,%eax
  4004b9:   48 be 00 00 00 00 00    movabs $0x3ff0000000000000,%rsi
  4004c0:   00 f0 3f
  4004c3:   48 b9 00 00 00 00 00    movabs $0x4000000000000000,%rcx
  4004ca:   00 00 40
  4004cd:   ba 00 00 00 00          mov    $0x0,%edx
  4004d2:   48 89 34 c5 40 10 60    mov    %rsi,0x601040(,%rax,8)
  4004d9:   00
  4004da:   48 89 0c c5 40 c4 24    mov    %rcx,0x524c440(,%rax,8)
  4004e1:   05
  4004e2:   48 89 14 c5 40 78 e9    mov    %rdx,0x9e97840(,%rax,8)
  4004e9:   09
  4004ea:   48 83 c0 01             add    $0x1,%rax
  4004ee:   48 3d 80 96 98 00       cmp    $0x989680,%rax
  4004f4:   75 dc                   jne    4004d2 <main+0x1e>
  4004f6:   b8 00 00 00 00          mov    $0x0,%eax
  4004fb:   c3                      retq
  4004fc:   90                      nop
  4004fd:   90                      nop
  4004fe:   90                      nop
  4004ff:   90                      nop

这三个 mov 应该对应于存储到内存中的三个不同向量。我希望退休的商店数量非常接近 30M 并且几乎没有负载,因为我只是在初始化三个数组。然而,这是我在 Sandy Bridge 机器上得到的结果:

$ perf stat -e L1-dcache-loads,L1-dcache-stores ./benchmark 

 Performance counter stats for './benchmark':

        46,017,360 L1-dcache-loads                                             
        75,985,205 L1-dcache-stores

这是用于 Nehalem 机器的:

$ perf stat -e L1-dcache-loads,L1-dcache-stores  ./benchmark

 Performance counter stats for './benchmark':                                                                                                                                                                                                

        45,255,731 L1-dcache-loads                                                                                                                                                                                                           
        60,164,676 L1-dcache-stores

对于每个针对内存的 mov 操作,已停用的加载和存储是如何计算的? 即使没有实际从内存中读取数据,为什么会有这么多负载?

【问题讨论】:

  • 您是否尝试过缩放数组大小并检查它对结果的影响?如果它没有改变,它可能只是一个不相关的开销
  • 负载与存储的比率保持不变,所以我肯定错过了如何协调 mov 与负载和存储。
  • 作为一个数据点,在我的 AMD FX8350 上,这些值大约为 10*STREAM_ARRAY_SIZE 负载,并且只是 STREAM_ARRAY_SIZE 存储(即预期的三分之一)。这更奇怪:)
  • 如果我错了,请纠正我,但这些事件不只计算 L1 中的加载/存储吗?你不可能把整个 10M 双打都放在那里,充其量你是在预取它们——L1-dcache-prefetches 说什么?
  • 尝试与优化比较 (-O3)。然后使用_mm_stream_pd 比较使用非临时存储。

标签: c gcc assembly x86 perf


【解决方案1】:

所以我对此有点好奇并做了一些研究。主要是想看看自从我上次使用 perf 框架后它现在有多大用处,它使共享开发机器上的内核崩溃,其他 25 位开发人员对我的实验非常不满意。

首先,让我们验证我是否看到了您所看到的:

$ cc -O -o xx xx.c && perf stat -e L1-dcache-loads,L1-dcache-stores ./xx

 Performance counter stats for './xx':

        58,764,160 L1-dcache-loads
        81,640,635 L1-dcache-stores

是的。甚至更大的数字。那么发生了什么?让我们更好地记录和分析一下:

$ cc -O -o xx xx.c && perf record -e L1-dcache-loads,L1-dcache-stores ./xx
[... blah blah ...]
$ perf report --stdio
[... blah blah ...]
# Samples: 688  of event 'L1-dcache-loads'
# Event count (approx.): 56960661
#
# Overhead  Command      Shared Object    Symbol
# ........  .......  .................  ........
#
    95.80%       xx  [kernel.kallsyms]  [k] 0xffffffff811176ee
     4.20%       xx  xx                 [.] main


# Samples: 656  of event 'L1-dcache-stores'
# Event count (approx.): 80623804
#
# Overhead  Command      Shared Object    Symbol
# ........  .......  .................  ........
#
    61.72%       xx  [kernel.kallsyms]  [k] 0xffffffff811176ee
    38.28%       xx  xx                 [.] main

啊哈,所以内核负责大部分的加载和存储。我们得到的计数器计算内核和用户空间所做的缓存访问。

发生的情况是程序的物理页面(包括数据段和bss)在您启动程序时没有映射甚至分配。当您第一次触摸它们时(或者将来如果它们被调出),内核会对其进行故障处理。我们可以看到:

$ cc -O -o foo foo.c && perf stat -e faults ./xx

 Performance counter stats for './xx':

            58,696 faults

实际上,仅在那一次运行期间,我们就执行了 58.7k 页面错误。由于页面大小为 4096 字节,我们得到58696*4096=240418816,这大约是您的数组的 240000000 字节,其余的是运行所需的 libc 和 ld.so 中的程序、堆栈和各种垃圾。

所以现在我们可以算出这些数字了。让我们先看看商店,因为它们应该是最容易弄清楚的。 80623804*0.3828=30862792.1712,这是有道理的。我们预计有 3000 万家商店,我们得到了 30.9 家。由于性能计数器采样并且不完全准确,因此这是意料之中的。内核所做的一些负载溢出并计入程序。在其他运行中,我得到的用户空间计数不到 30M。

以同样的方式,用户空间获得 240 万次负载。我怀疑这些实际上并没有加载到用户空间中,但是由于某种原因,内核在从程序中返回的陷阱中执行了一些访问。或类似的东西。我不确定这些,我不喜欢它们,但让我们看看我们是否可以消除这些噪音并检查它与由页面错误引起的垃圾数据有关的理论。

这是您的测试的更新版本:

#include <stdio.h>

#define STREAM_ARRAY_SIZE   10000000

static double   a[STREAM_ARRAY_SIZE],
    b[STREAM_ARRAY_SIZE],
    c[STREAM_ARRAY_SIZE];

void
setup(void)
{
    memset(a, 0, sizeof a);
    memset(b, 0, sizeof b);
    memset(c, 0, sizeof c);
}

void
bench(void)
{
    ssize_t j;

    for (j = 0; j < STREAM_ARRAY_SIZE; j++) {
        a[j] = 1.0;
        b[j] = 2.0;
        c[j] = 0.0;
    }
}

int
main(int argc, char **argv)
{
    setup();
    bench();
    return 0;
}

我确保在setup 期间得到所有页面错误,然后在bench 期间溢出的任何计数器中的内核噪声都应该很小。

$ cc -O -o xx xx.c && perf record -e faults,L1-dcache-loads,L1-dcache-stores ./xx
[...]
$ perf report --stdio
[...]
# Samples: 468  of event 'faults'
# Event count (approx.): 58768
#
# Overhead  Command      Shared Object             Symbol
# ........  .......  .................  .................
#
    99.20%       xx  libc-2.12.so       [.] __memset_sse2
     0.69%       xx  ld-2.12.so         [.] do_lookup_x
     0.08%       xx  ld-2.12.so         [.] dl_main
     0.02%       xx  ld-2.12.so         [.] _dl_start
     0.01%       xx  ld-2.12.so         [.] _start
     0.01%       xx  [kernel.kallsyms]  [k] 0xffffffff8128f75f


# Samples: 770  of event 'L1-dcache-loads'
# Event count (approx.): 61518838
#
# Overhead  Command      Shared Object             Symbol
# ........  .......  .................  .................
#
    96.14%       xx  [kernel.kallsyms]  [k] 0xffffffff811176ee
     3.86%       xx  libc-2.12.so       [.] __memset_sse2


# Samples: 866  of event 'L1-dcache-stores'
# Event count (approx.): 98243116
#
# Overhead  Command      Shared Object             Symbol
# ........  .......  .................  .................
#
    53.69%       xx  [kernel.kallsyms]  [k] 0xffffffff811176ee
    30.62%       xx  xx                 [.] bench
    15.69%       xx  libc-2.12.so       [.] __memset_sse2

你有它。页面错误发生在调用memset 期间和一些动态链接期间,以前主要的噪音现在发生在memset 期间,bench 本身没有任何负载和大约 3000 万个存储。就像我们预期的那样。这里有一个有趣的说明是memset 知道如何在这台机器上高效,并且与您的测试相比,只做了一半的存储来填充相同数量的内存。 __memset_sse2 中的“sse2”是一个很好的提示。

我刚刚意识到有一件事可能不清楚,我不知道该放在哪里,所以我将它放在这里。性能计数器确实准确地计算了事件,但据我所知,如果您想知道这些事件发生在哪里,CPU 只能在每记录一个 X 事件时生成一个陷阱。所以工具不知道事件发生的确切位置(以这种方式运行会太慢),而是我们等到陷阱出现并将所有 X 事件计入该指令/函数。我想,但我不确定 X 是否至少为 10000。因此,如果函数 bench 仅触及堆栈一次并且恰好生成 L1-dcache-load 溢出陷阱,那么您将对此进行 10000 次读取堆栈的读取。此外,据我所知,bench 函数中的 TLB 未命中(您将获得大约 58593 个)也通过 L1 缓存解决,并将被考虑在内。因此,无论您做什么,都永远不会得到您期望的准确数字。

【讨论】:

  • 要在 Linux 上获得分配的并提交内存,您可以使用MAP_POPULATE 来mmap() 数组。这应该允许使用 -O3 编译基准测试,而不会冒编译器优化所有内容的风险。
猜你喜欢
  • 2021-03-15
  • 1970-01-01
  • 2015-04-23
  • 1970-01-01
  • 2018-06-06
  • 2011-12-20
  • 1970-01-01
  • 1970-01-01
  • 2013-10-20
相关资源
最近更新 更多