当前位置: 首页 > 知识库问答 >
问题:

x86-64的缓存填充大小是否应为128字节?

令狐凌
2023-03-14

我在横梁上找到了一条评论。

从Intel的Sandy Bridge开始,spatial prefetcher现在一次提取64字节缓存线对,因此我们必须将其对齐到128字节,而不是64字节。

来源:

  • https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf
  • https://github.com/facebook/folly/blob/1b5288e6eea6df074758f877c849b6e73bbb9fbb/folly/lang/Align.h#L107

我没有在Intel的手册中找到这句话。但是直到最近的提交,folly仍然使用128字节的填充,这让我很有说服力。所以我开始编写代码,看看我是否可以观察到这种行为。这是我的代码。

#include <thread>

int counter[1024]{};

void update(int idx) {
    for (int j = 0; j < 100000000; j++) ++counter[idx];
}

int main() {
    std::thread t1(update, 0);
    std::thread t2(update, 1);
    std::thread t3(update, 2);
    std::thread t4(update, 3);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

编译器资源管理器

我的CPU是Ryzen 3700X。当索引为0、1、2、3时,需要大约1.2秒才能完成。当索引为0、16、32、48时,需要约200ms才能完成。当索引为0、32、64、96时,需要约200ms才能完成,这与之前完全相同。我还在Intel机器上对它们进行了测试,得到了类似的结果。

从这个微型工作台上,我看不出为什么要使用128字节填充而不是64字节填充。我做错什么了吗?

共有1个答案

沃侯林
2023-03-14

英特尔的优化手册确实描述了SnB系列CPU中的L2空间预取器。是的,当第一行被拉入时有备用内存带宽(离核请求跟踪槽)时,它会尝试完成128B对齐的64B行。

您的微基准没有显示64字节与128字节间隔之间的任何显著时间差。在没有任何实际的错误共享(在同一64字节行内)的情况下,经过一些初始的混乱之后,它很快就会达到这样一种状态,即每个核心都对其修改的缓存线拥有独占所有权。这意味着没有进一步的L1d未命中,因此没有对L2的请求会触发L2空间预取器。

例如,与if不同的是,两对线程争夺独立的原子

(L2预取器不会无限期地尝试完成其缓存的每一条有效行的成对行;这会对不同内核重复接触相邻行的情况造成伤害,而不会有任何帮助。)

理解std::hardware\u destructive\u interference\u size和std::hardware\u constructive\u interference\u size包括一个较长基准的答案;我最近没有看过它,但我认为它应该演示64字节的破坏性干扰,而不是128字节。遗憾的是,这里的答案没有提到二级空间预取是可能导致某种破坏性干扰的影响之一(尽管不如缓存外部级别中128字节的行大小,尤其是如果它是包容性缓存的话)。

我们可以用基准测试的性能计数器来衡量更多的初始混乱。在我的i7-6700k(带超线程的四核Skylake;4c8t,运行Linux5.16)上,我改进了源代码,这样我就可以在不破坏内存访问的情况下进行优化编译,并使用CPP宏,这样我就可以从编译器命令行设置步幅(以字节为单位)。请注意,当我们使用相邻行时,大约有500个内存顺序错误猜测管道核武器(machine_clears.memory_ordering)。实际数字变化很大,从200到850,但对总时间的影响仍然可以忽略不计。

$ g++ -DSIZE=64 -pthread -O2 false-share.cpp && perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,machine_clears.memory_ordering -r25 ./a.out 

 Performance counter stats for './a.out' (25 runs):

            560.22 msec task-clock                #    3.958 CPUs utilized            ( +-  0.12% )
                 0      context-switches          #    0.000 /sec                   
                 0      cpu-migrations            #    0.000 /sec                   
               126      page-faults               #  224.752 /sec                     ( +-  0.35% )
     2,180,391,747      cycles                    #    3.889 GHz                      ( +-  0.12% )
     2,003,039,378      instructions              #    0.92  insn per cycle           ( +-  0.00% )
     1,604,118,661      uops_issued.any           #    2.861 G/sec                    ( +-  0.00% )
     2,003,739,959      uops_executed.thread      #    3.574 G/sec                    ( +-  0.00% )
               494      machine_clears.memory_ordering #  881.172 /sec                     ( +-  9.00% )

          0.141534 +- 0.000342 seconds time elapsed  ( +-  0.24% )
$ g++ -DSIZE=128 -pthread -O2 false-share.cpp && perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,machine_clears.memory_ordering -r25 ./a.out 

 Performance counter stats for './a.out' (25 runs):

            560.01 msec task-clock                #    3.957 CPUs utilized            ( +-  0.13% )
                 0      context-switches          #    0.000 /sec                   
                 0      cpu-migrations            #    0.000 /sec                   
               124      page-faults               #  221.203 /sec                     ( +-  0.16% )
     2,180,048,243      cycles                    #    3.889 GHz                      ( +-  0.13% )
     2,003,038,553      instructions              #    0.92  insn per cycle           ( +-  0.00% )
     1,604,084,990      uops_issued.any           #    2.862 G/sec                    ( +-  0.00% )
     2,003,707,895      uops_executed.thread      #    3.574 G/sec                    ( +-  0.00% )
                22      machine_clears.memory_ordering #   39.246 /sec                     ( +-  9.68% )

          0.141506 +- 0.000342 seconds time elapsed  ( +-  0.24% )

在这台4c8t机器上,Linux如何将线程调度到逻辑核心,可能存在某种依赖性。相关:

  • 生产者消费者在超级同级与非超级同级之间共享内存位置的延迟和吞吐量成本是多少对于共享物理核的逻辑核来说,一条线内的实际错误共享要糟糕得多,但对于相邻的线,可能没有影响:每个物理核的L2相同,并且两条线在L1d中都会保持热状态

存储缓冲区(和存储转发)为每个错误的共享机器清除了一系列增量,所以它并不像人们预期的那么糟糕。(如果使用原子RMW,情况会更糟,比如std::atomic

$ g++ -DSIZE=4 -pthread -O2 false-share.cpp && perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,machine_clears.memory_ordering -r25 ./a.out 

 Performance counter stats for './a.out' (25 runs):

            809.98 msec task-clock                #    3.835 CPUs utilized            ( +-  0.42% )
                 0      context-switches          #    0.000 /sec                   
                 0      cpu-migrations            #    0.000 /sec                   
               122      page-faults               #  152.953 /sec                     ( +-  0.22% )
     3,152,973,230      cycles                    #    3.953 GHz                      ( +-  0.42% )
     2,003,038,681      instructions              #    0.65  insn per cycle           ( +-  0.00% )
     2,868,628,070      uops_issued.any           #    3.596 G/sec                    ( +-  0.41% )
     2,934,059,729      uops_executed.thread      #    3.678 G/sec                    ( +-  0.30% )
        10,810,169      machine_clears.memory_ordering #   13.553 M/sec                    ( +-  0.90% )

           0.21123 +- 0.00124 seconds time elapsed  ( +-  0.59% )

我使用了volatile,所以我可以启用优化。我假设您在编译时禁用了优化,因此int j也被存储/重新加载到循环中。

我使用了alignas(128)计数器[],所以我们可以确定数组的开头是两对128字节的行,而不是三行。

#include <thread>

alignas(128) volatile int counter[1024]{};

void update(int idx) {
    for (int j = 0; j < 100000000; j++) ++counter[idx];
}

static const int stride = SIZE/sizeof(counter[0]);
int main() {
    std::thread t1(update, 0*stride);
    std::thread t2(update, 1*stride);
    std::thread t3(update, 2*stride);
    std::thread t4(update, 3*stride);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
}
 类似资料:
  • 问题内容: 在我的应用程序中,有一点我需要对大块连续的内存数据块(100 MB的内存)执行计算。我当时的想法是继续预取程序将来会接触的部分块,以便在我对该部分执行计算时,数据已经在缓存中。 有人可以给我一个简单的示例,说明如何使用gcc实现这一目标吗?我在某处阅读,但不知道如何正确使用它。还要注意,我有一个多核系统,但是每个核都将并行处理不同的内存区域。 问题答案: 使用内置函数作为低级指令的接口

  • 我计划在我的应用程序组件中使用SelfPopulatingCache,因为它支持通读和看似用于刷新的支持缓存。 然而,我对timeToLiveSeconds配置有点困惑。这是我的测试配置: 在我的单元测试中,我执行以下操作: < li >验证我的缓存中有2个条目 < li >睡眠3秒钟 < li >但是,Hibernate后,我的缓存中仍有2个条目。 根据其他在线帖子(而不是文档),当我下次执行读

  • 我反汇编(使用objdump -d)这个操作码(c7 45 fc 05 00 00 00)并得到这个(移动DWORD PTR [rbp-0x4],0x5)。然后我尝试解码自己,我认为应该是(移动DWORD PTR [ebp-0x4],0x5)。为什么它是RBP寄存器而不是EBP寄存器?我错过了什么吗? 在这里,我尝试:首先,我看看C7操作码的mov操作码。 C7 /0 iw |视场角 r/m16,

  • 我有2个xml配置文件,如下所示 app-context.xml: test-cache.xml

  • 问题内容: 我需要将图片调整为固定大小。但它必须将宽度和高度之间的因素。 说我要调整从图片到 我现在要做的是: 现在我考虑最小的因素。 从那以后我一直都有正确的宽度。但是身高还是。 如何在不破坏图片的情况下降低高度? 我需要修剪吗?如果可以,怎么办? 问题答案: 这个解决方案与Can BerkGüder的解决方案基本相同,但是花了一些时间写和发表评论后,我才觉得喜欢发布。 此功能创建的缩略图与您提