编写一个跨步的x86基准测试(代码片段)

author author     2022-12-29     127

关键词:

我想编写一个加载基准测试,它使用编译时已知的步幅跨越给定的内存区域,在尽可能少的非加载指令的情况下包裹在区域的末尾(2的幂)。

例如,给定一个4099的步幅,以及rdi中的迭代计数和rsi中指向内存区域的指针,“工作”的东西是:

%define STRIDE 4099
%define SIZE   128 * 1024
%define MASK   (SIZE - 1)
xor     ecx, ecx

.top:
mov      al, [rsi + rcx]
add     ecx, STRIDE
and     ecx, MASK
dec rdi
jnz .top

问题是有4个非加载指令只是为了支持单个加载,处理步幅加法,屏蔽和循环终止检查。此外,还有一个通过ecx进行的2周期依赖链。

我们可以将这一点展开以将循环终止检查成本降低到接近零,并打破依赖链(这里展开4x):

.top:

lea     edx, [ecx + STRIDE * 0]
and     edx, MASK
movzx   eax, BYTE [rsi + rdx]

lea     edx, [ecx + STRIDE * 1]
and     edx, MASK
movzx   eax, BYTE [rsi + rdx]

lea     edx, [ecx + STRIDE * 2]
and     edx, MASK
movzx   eax, BYTE [rsi + rdx]

lea     edx, [ecx + STRIDE * 3]
and     edx, MASK
movzx   eax, BYTE [rsi + rdx]

add     ecx, STRIDE * 4

dec rdi
jnz .top

然而,这对addand操作处理包装步幅没有帮助。例如,上面的benmark报告了一个含L1区域的0.875个周期/负载,但我们知道正确的答案是0.5(每个周期两个负载)。 0.875来自15个总uop / 4个uop周期,即,我们受到uop吞吐量的4宽最大宽度的约束,而不是负载吞吐量。

关于如何有效地展开循环以消除步幅计算的大部分成本的任何想法?

答案

因为“绝对最大的疯狂”;您可以要求操作系统在许多虚拟地址映射相同的页面(例如,因此相同的16 MiB RAM出现在虚拟地址0x100000000,0x11000000,0x12000000,0x13000000,...),以避免需要关心包装;并且您可以使用自生成代码来避免其他一切。基本上,生成如下所示的指令的代码:

    movzx eax, BYTE [address1]
    movzx ebx, BYTE [address2]
    movzx ecx, BYTE [address3]
    movzx edx, BYTE [address4]
    movzx esi, BYTE [address5]
    movzx edi, BYTE [address6]
    movzx ebp, BYTE [address7]
    movzx eax, BYTE [address8]
    movzx ebx, BYTE [address9]
    movzx ecx, BYTE [address10]
    movzx edx, BYTE [address11]
    ...
    movzx edx, BYTE [address998]
    movzx esi, BYTE [address999]
    ret

当然,所使用的所有地址都将由生成指令的代码计算。

注意:根据具体的CPU,可能会更快地进行循环而不是完全展开(在指令获取和解码成本与循环开销之间存在折衷)。对于更新的英特尔CPU,有一种称为环路流检测器的设计,旨在避免对小于特定大小的环路进行提取和解码(其大小取决于CPU型号);并且我假设生成一个适合该大小的循环将是最佳的。

另一答案

关于那个数学。证明...在展开循环的开始,如果ecx < STRIDEn = (SIZE div STRIDE),SIZE不能被STRIDE整除,那么(n-1)*STRIDE < SIZE,即n-1次迭代是安全的,没有掩蔽。第n次迭代可能并且可能不需要掩蔽(取决于初始ecx)。如果第n次迭代不需要掩码,则第(n + 1)次将需要掩码。

结果是,你可以设计这样的代码

    xor    ecx, ecx
    jmp    loop_entry
unrolled_loop:
    and    ecx, MASK     ; make ecx < STRIDE again
    jz     terminate_loop
loop_entry:
    movzx  eax, BYTE [rsi+rcx]
    add    ecx, STRIDE
    movzx  eax, BYTE [rsi+rcx]
    add    ecx, STRIDE
    movzx  eax, BYTE [rsi+rcx]
    add    ecx, STRIDE
    ... (SIZE div STRIDE)-1 times
    movzx  eax, BYTE [rsi+rcx]
    add    ecx, STRIDE

    ;after (SIZE div STRIDE)-th add ecx,STRIDE
    cmp    ecx, SIZE
    jae    unrolled_loop
    movzx  eax, BYTE [rsi+rcx]
    add    ecx, STRIDE
    ; assert( ecx >= SIZE )
    jmp    unrolled_loop

terminate_loop:

需要在add之前发生的and的数量不规则,它将是nn+1,因此展开的循环的结束已加倍,以ecx < STRIDE值开始每个展开的循环。

我不善于使用nasm宏来决定是否可以通过某种宏魔法展开它。

还有一个问题是,这是否可以宏编辑到不同的寄存器,如

    xor    ecx, ecx

    ...
loop_entry:
    lea    rdx,[rcx + STRIDE*4]  
    movzx  eax, BYTE [rsi + rcx]
    movzx  eax, BYTE [rsi + rcx + STRIDE]
    movzx  eax, BYTE [rsi + rcx + STRIDE*2]
    movzx  eax, BYTE [rsi + rcx + STRIDE*3]
    add    ecx, STRIDE*8
    movzx  eax, BYTE [rsi + rdx]
    movzx  eax, BYTE [rsi + rdx + STRIDE]
    movzx  eax, BYTE [rsi + rdx + STRIDE*2]
    movzx  eax, BYTE [rsi + rdx + STRIDE*3]
    add    edx, STRIDE*8
    ...

    then the final part can be filled with simple
    movzx  eax, BYTE [rsi + rcx]
    add    ecx, STRIDE
    ... until the n-th ADD state is reached, then jae loop final

    ;after (SIZE div STRIDE)-th add ecx,STRIDE
    cmp    ecx, SIZE
    jae    unrolled_loop
    movzx  eax, BYTE [rsi + rcx]
    add    ecx, STRIDE
    ; assert( ecx >= SIZE )
    jmp    unrolled_loop

内部的“安全”部分也可以循环一些,如果你的例子中SIZE div STRIDE = 31.97657965357404,那么内部8次movzx可以循环3次... 3 * 8 = 24,然后7次非 - 简单的线条达到31x add,然后双重循环出口跟随最终到达第32个add根据需要。

虽然在31.9的情况下它看起来毫无意义,但在类似数百+ = SIZE div STRIDE的情况下循环中间部分是有意义的。

另一答案

如果使用AVX2聚集生成加载uops,则可以使用SIMD进行add + AND。但是,在尝试测量非聚集负载时,这可能不是您想要的!

如果您的区域大小是2 ^ 16..19,您可以使用add ax, dx(使用DX = stride以避免LCP停顿)以2 ^ 16免费包装。使用eax作为缩放索引。使用lea di, [eax + STRIDE * n]等在一个展开的循环中,这可以节省足够的uops,让你每个时钟运行2个负载,而不会在前端出现瓶颈。但是partial-register merging dependencies (on Skylake)会创建多个循环传输的dep链,如果你需要避免重用它们,你就会在32位模式下用完寄存器。

您甚至可以考虑映射低64k的虚拟内存(在Linux上设置vm.mmap_min_addr=0)和在32位代码中使用16位寻址模式。只读16位寄存器避免了只需写16位的复杂性;最好在上层16结束垃圾。


要在没有16位寻址模式的情况下做得更好,您需要创建条件,让您知道包装不会发生。这允许展开[reg + STRIDE * n]寻址模式。

您可以编写一个正常的展开循环,当接近环绕点时(即ecx + STRIDE*n > bufsize时)会爆发,但如果在Skylake上bufsize / STRIDE大于22,则无法预测。

您可以在每次迭代时只进行一次AND屏蔽,并放宽工作集正好为2 ^ n个字节的约束。也就是说,如果你为你的负载预留了足够的空间来超越STRIDE * n - 1,并且你对缓存行为没有问题,那就去做吧。

如果您仔细选择展开因子,您可以控制每次环绕发生的位置。但是,凭借主要的步幅和2缓冲的力量,我认为你需要展开lcm(stride, bufsize/stride) = stride * bufsize/stride = bufsize重复模式。对于不适合L1的缓冲区大小,此展开因子太大而不适合uop缓存,甚至不适合L1I。我看了几个小的测试用例,比如n*7 % 16,它在16次迭代后重复,就像n*5 % 16n*3 % 16一样。并且n*7 % 32在32次迭代后重复。即,当乘数和模数相对为素数时,线性同余生成器探索小于模数的每个值。

这些选项都不是理想选择,但这是我现在建议的最佳选择。

一文带你了解单元测试和基准测试干货(代码片段)

...战部分你可以看到,在写每个功能的时候,都会编写测试代码。那是因为TDD(Test-DrivenDevelopment,测试驱动开发)中提倡先编写测试代码,然后再编写功能代码,每做一个修改后,都要执行一次单元... 查看详情

go语言测试(代码片段)

...对低级的。它依赖一个gotest测试命令和一组按照约定方式编写的测试函数,测试命令可以运行这些测试函数。编写相对轻量级的纯测试代码是有效的,而且它很容易延伸到基准测试和示例文档。gotest编写测试代码和编写普通的Go... 查看详情

刷新缓存以防止基准测试波动(代码片段)

...有办法刷新缓存,或以某种方式防止这些波动?答案没有一个“适用于所有事物,无处不在”。大多数处理器都有特殊指令来刷新缓存,但它们通常是特权指令,因此必须从OS内核内部完成,而不是用户模式代码。当然,对于每... 查看详情

python一个为hugo基准测试生成5000个测试帖的脚本(代码片段)

查看详情

gotest(代码片段)

...试和基准测试。gotest工具Go语言中的测试依赖gotest命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。gotest命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_t... 查看详情

如何编写c++代码简单测试一下x86和arm的cpu性能(代码片段)

x86:Intel(R)Core(TM)i5-8250UCPUarm:Qualcomm®snapdragon ™821(MSM8996-AC)一千万次nop循环c代码如下:intmain()inti;for(i=0;i<10000000;i++)return(0);编译和执行,如下&# 查看详情

golang基准测试详解(代码片段)

...测试前的准备生成以_test后缀的go文件(例:xxx_test.go)后,编写基准测试用例,以Benchmark开头的。以测试冒泡排序为例,代码如下:funcBenchmarkSort(b*testing.B)arr:=make([]int,100000)fori:=100000;i>0;i--arr=append(arr,i)b.ResetTimer()fori:=0;i<b.N;i++bubbl... 查看详情

几款优秀的linux基准测试工具(代码片段)

...不同组件的性能.基准测试有两类:复合和应用复合基准对一个 查看详情

go语言之基准测试

...才是最优值呢,这就需要配合基准测试不断调优了。如何编写基准测试基准测试代码的编写和单元测试非常相似,它也有一定的规则, 查看详情

黄金法则:mysql基准测试最佳实践(代码片段)

MySQL基准测试在数据库性能优化中是一个非常重要的分支。本文目的在于让读者对关系型数据库系统有一个基本的了解,掌握MySQL以及如何管理使用Linux。文中讨论了MySQL性能因素以及如何测试CPU性能,同时使用具体的例子... 查看详情

sysbench工具和mysql的基准测试(代码片段)

一、基准测试  1、什么是基准测试    数据库的基准测试是对数据库的性能指标进行定量的、可复现的、可对比的测试。    基准测试与压力测试    基准测试可以理解为针对系统的一种压力测试。但基准测试... 查看详情

myql基准测试工具sysbench(代码片段)

一、Sysbench介绍SysBench是一个模块化的、跨平台、多线程基准测试工具,主要用于评估测试各种不同系统参数下的数据库负载情况。它主要包括以下几种方式的测试:1、cpu性能2、磁盘io性能3、调度程序性能4、内存分配及传输速... 查看详情

如何在 Java 中编写正确的微基准测试?

】如何在Java中编写正确的微基准测试?【英文标题】:HowdoIwriteacorrectmicro-benchmarkinJava?【发布时间】:2021-12-1102:42:34【问题描述】:您如何用Java编写(和运行)正确的微基准测试?我正在寻找一些代码示例和cmets来说明需要考虑... 查看详情

如何在 Java 中编写正确的微基准测试?

】如何在Java中编写正确的微基准测试?【英文标题】:HowdoIwriteacorrectmicro-benchmarkinJava?【发布时间】:2009-02-0217:39:41【问题描述】:您如何用Java编写(和运行)正确的微基准测试?我正在寻找一些代码示例和cmets来说明需要考虑... 查看详情

mysql基准测试与sysbench工具(代码片段)

一、基准测试简介 1、什么是基准测试数据库的基准测试是对数据库的性能指标进行定量的、可复现的、可对比的测试。基准测试与压力测试基准测试可以理解为针对系统的一种压力测试。但基准测试不关心业务逻辑,更加简... 查看详情

phpphp的基准测试功能(代码片段)

查看详情

phpphp简单的基准测试(代码片段)

查看详情

pythonpython中的系统基准测试。(代码片段)

查看详情