C 函数刷新所有包含数组的缓存行 代码审查:API

如何解决C 函数刷新所有包含数组的缓存行 代码审查:API

我试图强制用户应用程序从所有级别的缓存中刷新保存数组(由其自身创建)的所有缓存行。

在阅读这篇文章 (Cflush to invalidate cache line via C function) 并得到@PeterCordes 的大力指导后,我尝试用 C 语言想出一个函数来实现这一点。

#include <x86intrin.h>
#include <stdint.h>

inline void flush_cache_range(uint64_t *ptr,size_t len){
    size_t i;
    // prevent any load or store to be scheduled across 
    // this point due to CPU Out of Order execution.
    _mm_mfence();
    for(i=0; i<len; i++)
        // flush the cache line that contains ptr+i from 
        // all cache levels
        _mm_clflushopt(ptr+i); 
    _mm_mfence();
}

int main(){
    size_t plen = 131072; // or much bigger
    uint64_t *p = calloc(plen,sizeof(uint64_t));
    for(size_t i=0; i<plen; i++){
        p[i] = i;
    }
    flush_cache_range(p,plen);
    // at this point,accessing any element of p should
    // cause a cache miss. As I access them,adjacent
    // elements and perhaps pre-fetched ones will come
    // along.
    (...)
    return 0;
}

我正在运行内核 5.11.14 (Fedora 33) 的 AMD Zen2 处理器中使用 gcc -O3 -march=native source.c -o exec.bin 进行编译。

我不完全理解 mfence/sfence/lfence 之间的区别,或者一个或另一个就足够了,所以我只使用了 mfence 因为我相信它施加了最强的限制(我说得对吗?)。

我的问题是:我在这个实现中遗漏了什么吗?它会做我想象的那样吗? (我的想象是在调用 flush_cache_range 函数后的评论中)

谢谢。


编辑 1:每行冲洗一次,并移除围栏。

在@PeterCordes 的回答之后,我正在做一些调整:

  • 首先,该函数接收一个指向 char 的指针及其大小(以字符为单位),因为它们有 1 个字节长,所以我可以控制从一个刷新跳到下一个刷新的大小。

  • 然后,我需要确认缓存行的大小。我可以使用程序 cpuid:
    获取该信息 cpuid -1 | grep -A12 -e "--- cache [0-9] ---"
    对于 L1i、L1d、L2 和 L3,我得到 line size in bytes = 0x40 (64) 所以这是每次刷新后我必须跳过的字节数。

  • 然后我通过添加 ptr + len - 1 来确定指向最后一个字符的指针。

  • 并循环遍历所有地址,每个缓存行一个,包括最后一个 (ptr_end)。

这是代码的更新版本:

#include <stdio.h>
#include <x86intrin.h>
#include <stdint.h>

inline void flush_cache_range(char *ptr,size_t len);

void flush_cache_range(char *ptr,size_t len){
    const unsigned char cacheline = 64;
    char *ptr_end = ptr + len - 1;
    while(ptr <= ptr_end){
        _mm_clflushopt(ptr);
        ptr += cacheline;
    }
}

int main(){
    size_t i,sum=0,plen = 131072000; // or much bigger
    uint64_t *p = calloc(plen,sizeof(uint64_t));
    for(i=0; i<plen; i++){
        p[i] = i;
    }
    flush_cache_range((char*)p,sizeof(p[0])*plen);
    // there should be many cache misses now
    for(i=0; i<plen; i++){
        sum += p[i];
    }
    printf("sum is:%lu\n",sum);
    return 0;
}

现在当我编译并运行 perf 时: gcc -O3 -march=native src/source.c -o exec.bin && perf stat -e cache-misses,cache-references ./exec.bin

我明白了:

sum is:8589934526464000
 Performance counter stats for './exec.bin':

         1,202,714      cache-misses:u # 1.570 % of all cache refs    
        76,612,476      cache-references:u                                          
       0.377100534 seconds time elapsed
       0.170473000 seconds user
       0.205574000 seconds sys


如果我对调用 flush_cache_range 的行进行注释,我会得到几乎相同的结果:

sum is:8589934526464000

 Performance counter stats for './exec.bin':
         1,211,462      cache-misses:u # 1.590 % of all cache refs    
        76,685      cache-references:u                                          
       0.356544645 seconds time elapsed
       0.160227000 seconds user
       0.195305000 seconds sys

我错过了什么?


编辑 2:添加 sfence,并修复循环限制

  • 我按照@prl 的建议添加了围栏
  • 将 ptr_end 更改为指向其缓存行的最后一个字节。
void flush_cache_range(char *ptr,size_t len){
    const unsigned char cacheline = 64;
    char *ptr_end = (char*)(((size_t)ptr + len - 1) | (cacheline - 1));

    while(ptr <= ptr_end){
        _mm_clflushopt(ptr);
        ptr += cacheline;
    }
    _mm_sfence();
}

我仍然在性能方面得到了同样的意外结果。

解决方法

是的,看起来正确但效率很低。

您对事后缓存未命中的期望(通过硬件预取减轻)是合理的。您可以使用 perf stat 来检查,如果您编写了一些稍后使用该数组的实际测试代码。


您在每个单独的 uint64_t 上运行 clflushopt,但 x86 缓存行在每个当前支持 clflushopt 的 CPU 上都是 64 字节。因此,您执行的刷新次数是原来的 8 倍,并且在某些 CPU 上重复刷新同一行可能会非常慢。 (比在缓存中刷新更多热行更糟糕。)

请参阅我在 The right way to use function _mm_clflush to flush a large struct 上的回答,了解数组以相对于缓存行的未知对齐开始的一般情况,并且数组大小不是行大小的倍数。在包含任何数组/结构体的每个缓存行上运行 clflush 或 clflushopt 一次。

除性能外,刷新是幂等的,因此您可以以 64 字节的增量循环并刷新数组的最后一个字节,但在该链接的答案中,我想出了一种廉价的方法实现循环逻辑以仅触摸每一行一次。对于数组指针 + 长度,显然使用 sizeof(ptr[0]) * len 而不是 sizeof(struct) 就像使用的链接答案一样。


代码审查:API

冲洗在整条线上起作用。使用 char*void*,然后将其转换为 char* 以按行大小增加它。因为从逻辑上讲,你给 asm 指令一个指针,它只刷新包含该字节的一行。


之前不需要内存屏障

在刷新之前屏蔽是没有意义的; clflushopt 是订购wrt。存储到相同的缓存行,因此将 clflushopt 存储到同一行(在 asm 中的顺序)将按该顺序发生,刷新新存储的数据。手册记录了这一点(https://www.felixcloutier.com/x86/clflushopt 来自英特尔,我假设 AMD 的手册在其 CPU 上记录了相同的语义。)

我认为/希望 C 编译器将 _mm_clflushopt(p) 视为至少对包含 p 的整行的可变访问,因此不会在编译时将存储重新排序到任何 C 对象*p 可以别名。 (并且可能也不会加载。)如果没有,您最多需要 asm("":::"memory"),一个仅编译时屏障。 (像 atomic_signal_fence(memory_order_seq_cst),而不是 atomic_thread_fence)。


我认为如果您的循环不是微小的,并且您只关心该线程是否会缓存未命中,那么在此之后进行围栏也是不必要的。使用 sfence 肯定没用,它根本不排序加载,不像 mfencelfence

sfence 之后使用 clflushopt 的正常原因是为了保证较早的存储在任何后续存储之前已进入持久存储,例如使崩溃后恢复一致性成为可能。 (在具有傲腾 DC PM 或其他类型的真正内存映射非易失性 RAM 的系统中)。参见 this Q&A 示例,了解 clflushopt 的排序以及有时需要围栏的原因。

这不会迫使以后的负载丢失,它们不是按顺序订购的。 sfence,因此可以提前执行,在 sfence 之前和 clflushopt 之前。 mfence 会阻止这种情况发生。

lfence(或大约 ROB 大小的 uops 数量,例如 Skylake 上的 224)可能,但要等待 clflushopt 从无序返回中退出 -end 并不意味着它已经完成驱逐线。它可能更像一个存储,并且必须通过存储缓冲区。

我在我的 CPU 和 i7-6700k Intel Skylake 上测试了这个:

default rel
%ifdef __YASM_VER__
    CPU Conroe AMD
    CPU Skylake AMD
%else
%use smartalign
alignmode p6,64
%endif

global _start
_start:

%if 1
    lea        rdi,[buf]
    lea        rsi,[bufsrc]
%endif

    mov     ebp,10000000

  mov [rdi],eax
  mov [rdi+4096],edx   ; dirty a couple BSS pages
align 64
.loop:
    mov  eax,[rdi]
;    mov  eax,[rdi+8]
    clflushopt [rdi]          ; clflush vs. clushopt doesn't make a different here except in uops issued/executed
sfence            ; actually speeds things up
;mfence           ; the load after this definitely misses.
    mov  eax,[rdi+16]
;    mov  eax,[rdi+24]
    add rdi,64        ; next cache line
    and rdi,-(1<<14)  ; wrap to 4 pages
    dec ebp
    jnz .loop
.end:

    xor edi,edi
    mov eax,231   ; __NR_exit_group  from /usr/include/asm/unistd_64.h
    syscall       ; sys_exit_group(0)


section .bss
align 4096
buf:    resb 4096*4096

bufsrc:  resb 4096
resb 100
t=testloop; asm-link -dn "$t".asm && taskset -c 3 perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_executed.thread,mem_load_retired.l1_hit,mem_load_retired.l1_miss -r4 ./"$t"
+ nasm -felf64 -Worphan-labels testloop.asm
+ ld -o testloop testloop.o

...
0000000000401040 <_start.loop>:
  401040:       8b 07                   mov    eax,DWORD PTR [rdi]
  401042:       66 0f ae 3f             clflushopt BYTE PTR [rdi]
  401046:       0f ae f8                sfence 
  401049:       8b 47 10                mov    eax,DWORD PTR [rdi+0x10]
  40104c:       48 83 c7 40             add    rdi,0x40
  401050:       48 81 e7 00 c0 ff ff    and    rdi,0xffffffffffffc000
  401057:       ff cd                   dec    ebp
  401059:       75 e5                   jne    401040 <_start.loop>
...

 Performance counter stats for './testloop' (4 runs):

            334.27 msec task-clock                #    0.999 CPUs utilized            ( +-  7.62% )
                 0      context-switches          #    0.000 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
                 3      page-faults               #    0.009 K/sec                  
     1,385,639,019      cycles                    #    4.145 GHz                      ( +-  7.68% )
        80,000,116      instructions              #    0.06  insn per cycle           ( +-  0.00% )
       100,271,634      uops_issued.any           #  299.968 M/sec                    ( +-  0.04% )
       100,257,154      uops_executed.thread      #  299.924 M/sec                    ( +-  0.04% )
            16,894      mem_load_retired.l1_hit   #    0.051 M/sec                    ( +- 17.24% )
         2,347,561      mem_load_retired.l1_miss  #    7.023 M/sec                    ( +- 14.76% )

            0.3346 +- 0.0255 seconds time elapsed  ( +-  7.62% )

那是 sfence,而且是最快的。平均运行时间变化很大。使用 clflush 而不是 clflushopt 不会改变太多时间,但更多 uops:150,185,359 uops_issued.any(融合域)和 110,219,059 uops_executed.thread(未融合域)。

使用 mfence 是最慢的,每次 clflush 都会导致我们两次缓存未命中(一次是在加载后的本次迭代,另一次是在我们返回时进行的下一次迭代。)

## With MFENCE
     3,765,471,466      cycles                    #    4.129 GHz                      ( +-  1.26% )
        80,292      instructions              #    0.02  insn per cycle           ( +-  0.00% )
       160,386,634      uops_issued.any           #  175.881 M/sec                    ( +-  0.03% )
       100,533,848      uops_executed.thread      #  110.246 M/sec                    ( +-  0.06% )
             7,005      mem_load_retired.l1_hit   #    0.008 M/sec                    ( +- 21.58% )
         9,966,476      mem_load_retired.l1_miss  #   10.929 M/sec                    ( +-  0.05% )

没有围栏,仍然比sfence慢。我不知道为什么。或许 sfence 会阻止 clflush 操作的执行速度如此之快,从而让后面的迭代中的负载有机会先于它们并在 clflushopt 驱逐它之前读取缓存行?

     2,047,314,028      cycles                    #    4.125 GHz                      ( +-  2.58% )
        70,166      instructions              #    0.03  insn per cycle           ( +-  0.00% )
        80,619,482      uops_issued.any           #  162.427 M/sec                    ( +-  0.05% )
        80,584,719      uops_executed.thread      #  162.357 M/sec                    ( +-  0.04% )
            66,198      mem_load_retired.l1_hit   #    0.133 M/sec                    ( +-  6.61% )
         4,814,405      mem_load_retired.l1_miss  #    9.700 M/sec                    ( +-  4.59% )

这些实验结果来自 Intel Skylake,而非 AMD

(并且较旧或较新的英特尔在允许使用 clflushopt 重新排序负载的方式方面可能有所不同。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res