gpt4 book ai didi

performance - 如何在x86_64上准确基准未对齐的访问速度

转载 作者:行者123 更新时间:2023-12-04 03:11:36 27 4
gpt4 key购买 nike

answer中,我已经说过,未对齐的访问与长时间(在x86/x86_64上)的对齐访问的速度几乎相同。我没有任何数字可以支持此声明,因此我为此创建了一个基准。

在此基准测试中,您是否看到任何缺陷?您是否可以对此进行改进(我的意思是增加GB/秒,以便更好地反射(reflect)真相)?

#include <sys/time.h>
#include <stdio.h>

template <int N>
__attribute__((noinline))
void loop32(const char *v) {
for (int i=0; i<N; i+=160) {
__asm__ ("mov (%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x04(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x08(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x0c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x10(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x14(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x18(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x1c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x20(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x24(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x28(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x2c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x30(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x34(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x38(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x3c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x40(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x44(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x48(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x4c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x50(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x54(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x58(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x5c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x60(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x64(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x68(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x6c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x70(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x74(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x78(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x7c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x80(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x84(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x88(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x8c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x90(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x94(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x98(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x9c(%0), %%eax" : : "r"(v) :"eax");
v += 160;
}
}

template <int N>
__attribute__((noinline))
void loop64(const char *v) {
for (int i=0; i<N; i+=160) {
__asm__ ("mov (%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x08(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x10(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x18(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x20(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x28(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x30(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x38(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x40(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x48(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x50(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x58(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x60(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x68(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x70(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x78(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x80(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x88(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x90(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x98(%0), %%rax" : : "r"(v) :"rax");
v += 160;
}
}

template <int N>
__attribute__((noinline))
void loop128a(const char *v) {
for (int i=0; i<N; i+=160) {
__asm__ ("movaps (%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x10(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x20(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x30(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x40(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x50(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x60(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x70(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x80(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x90(%0), %%xmm0" : : "r"(v) :"xmm0");
v += 160;
}
}

template <int N>
__attribute__((noinline))
void loop128u(const char *v) {
for (int i=0; i<N; i+=160) {
__asm__ ("movups (%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x10(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x20(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x30(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x40(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x50(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x60(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x70(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x80(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x90(%0), %%xmm0" : : "r"(v) :"xmm0");
v += 160;
}
}

long long int t() {
struct timeval tv;
gettimeofday(&tv, 0);
return (long long int)tv.tv_sec*1000000 + tv.tv_usec;
}

int main() {
const int ITER = 10;
const int N = 1600000000;

char *data = reinterpret_cast<char *>(((reinterpret_cast<unsigned long long>(new char[N+32])+15)&~15));
for (int i=0; i<N+16; i++) data[i] = 0;

{
long long int t0 = t();
for (int i=0; i<ITER*100000; i++) {
loop32<N/100000>(data);
}
long long int t1 = t();
for (int i=0; i<ITER*100000; i++) {
loop32<N/100000>(data+1);
}
long long int t2 = t();
for (int i=0; i<ITER; i++) {
loop32<N>(data);
}
long long int t3 = t();
for (int i=0; i<ITER; i++) {
loop32<N>(data+1);
}
long long int t4 = t();

printf(" 32-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%\n", (double)N*ITER/(t1-t0)/1000, (double)N*ITER/(t2-t1)/1000, 100.0*(t2-t1)/(t1-t0)-100.0f);
printf(" 32-bit, mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%\n", (double)N*ITER/(t3-t2)/1000, (double)N*ITER/(t4-t3)/1000, 100.0*(t4-t3)/(t3-t2)-100.0f);
}
{
long long int t0 = t();
for (int i=0; i<ITER*100000; i++) {
loop64<N/100000>(data);
}
long long int t1 = t();
for (int i=0; i<ITER*100000; i++) {
loop64<N/100000>(data+1);
}
long long int t2 = t();
for (int i=0; i<ITER; i++) {
loop64<N>(data);
}
long long int t3 = t();
for (int i=0; i<ITER; i++) {
loop64<N>(data+1);
}
long long int t4 = t();

printf(" 64-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%\n", (double)N*ITER/(t1-t0)/1000, (double)N*ITER/(t2-t1)/1000, 100.0*(t2-t1)/(t1-t0)-100.0f);
printf(" 64-bit, mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%\n", (double)N*ITER/(t3-t2)/1000, (double)N*ITER/(t4-t3)/1000, 100.0*(t4-t3)/(t3-t2)-100.0f);
}
{
long long int t0 = t();
for (int i=0; i<ITER*100000; i++) {
loop128a<N/100000>(data);
}
long long int t1 = t();
for (int i=0; i<ITER*100000; i++) {
loop128u<N/100000>(data+1);
}
long long int t2 = t();
for (int i=0; i<ITER; i++) {
loop128a<N>(data);
}
long long int t3 = t();
for (int i=0; i<ITER; i++) {
loop128u<N>(data+1);
}
long long int t4 = t();

printf("128-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%\n", (double)N*ITER/(t1-t0)/1000, (double)N*ITER/(t2-t1)/1000, 100.0*(t2-t1)/(t1-t0)-100.0f);
printf("128-bit, mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%\n", (double)N*ITER/(t3-t2)/1000, (double)N*ITER/(t4-t3)/1000, 100.0*(t4-t3)/(t3-t2)-100.0f);
}
}

最佳答案

计时方法。我可能会进行设置,以便由命令行参数选择测试,因此我可以使用perf stat ./unaligned-test对其进行计时,并获得性能计数器的结果,而不仅仅是每次测试的挂钟时间。这样,由于我可以测量核心时钟周期,所以我不必在意涡轮增压/省电。 (除非禁用turbo和其他频率变化,否则它与gettimeofday/rdtsc引用周期不同。)

您只测试吞吐量,而不是等待时间,因为没有一个负载是相关的。

您的缓存号将比您的内存号更糟,但是您可能不会意识到这是因为您的缓存号可能是由于split-load registers数量的瓶颈所致,该cache-fetch width might be narrower than the line size处理跨越缓存行边界的加载/存储。对于顺序读取,高速缓存的外部级别仍然始终只会看到对整个高速缓存行的请求序列。只有执行单元从L1D获取数据才需要关心对齐。要测试非缓存情况下的未对齐情况,您可以分散负载,因此缓存行拆分将需要将两条缓存行放入L1中。

高速缓存行的宽度为64B1,因此您始终在测试高速缓存行拆分和高速缓存行内访问的混合情况。测试始终拆分的负载将使拆分负载的微体系结构资源面临更大的瓶颈。 (实际上,取决于您的CPU,@BeeOnRope says that AMD CPUs may care about 16B and 32B boundaries。近期的Intel CPU可以从缓存行内部获取所有未对齐的块,但这是因为它们具有特殊的硬件来加快存储速度。其他CPU可能仅在以自然方式获取时才是最快的。对齐的16B块或其他内容。Store-to-Load Forwarding and Memory Disambiguation in x86 Processors。)

您根本没有测试存储->负载转发。对于现有测试,以及一种以可视化的方式显示不同比对结果的好方法,请参阅此stuffedcow.net博客文章:Intel's optimization manual

通过内存传递数据是一个重要的用例,未对齐+高速缓存行拆分可能会干扰某些CPU上的存储转发。要正确测试,请确保测试不同的错位,而不仅仅是1:15(向量)或1:3(整数)。 (您目前仅测试相对于16B对齐的+1偏移)。

我忘记了它是仅用于存储转发还是常规加载,但是当加载跨高速缓存行边界平均分配(8:8向量,也可能是4:4或2:2)时,惩罚可能会更少整数分割)。您应该对此进行测试。 (我可能正在考虑P4 lddqu或Core 2 movqdu)

Why is integer assignment on a naturally aligned variable atomic on x86?具有大的未对齐表与从宽商店到完全包含在其中的狭窄重载的商店转发。在某些CPU上,即使宽存储区是自然对齐的,这也适用于更多情况,即使它没有跨越任何高速缓存行边界也是如此。 (也许在SnB/IvB上,因为它们使用带有16B存储区的存储区L1高速缓存,并且在这些存储区之间拆分会影响商店转发。
我没有重新检查手册,但是如果您真的想通过实验进行测试,那是您应该寻找的东西。)

这使我想起,未对齐的负载更有可能在SnB/IvB上引发缓存库冲突(因为一个负载可以接触两个库)。但是您不会从单个流中看到此负载,因为在一个周期内两次访问同一行中的同一存储库是很好的。它只能以不同的方式访问同一银行,而不会在同一周期中发生。 (例如,两个内存访问的间隔为128B的倍数时。)

您无需尝试测试4k页面拆分。它们比常规的高速缓存行拆分要慢,因为它们还需要两次TLB检查。 (不过,Skylake将其从〜100个周期的惩罚提高到了超过正常负载使用延迟的〜5个周期的惩罚)

您无法在对齐的地址上测试movups,因此即使内存在运行时对齐,您也不会检测到movups比Core2上的movaps慢,并且更早。 (我认为即使在Core2中,不对齐的mov加载最多8个字节也是可以的,只要它们不跨越高速缓存行边界即可。IDK您必须查看多大的CPU才能发现非矢量问题在一个缓存行中加载它。它只能是32位CPU,但是您仍然可以使用MMX或SSE甚至x87来测试8B加载。P5Pentium和更高版本保证对齐的8B加载/存储是原子的,但是P6和更高版本只要没有越过缓存行边界,就可以确保缓存的8B加载/存储是原子的。与AMD相比,即使8B边界对于原子性也很重要,即使在可缓存的内存中也可以保证。

查看Agner Fog的内容,以了解更多有关如何降低未对齐负载的速度的信息,并准备测试以解决这些情况。实际上,Agner可能不是最好的资源,因为他的微体系结构指南主要侧重于通过管道获得微指令。只是简要介绍了缓存行拆分的成本,而没有深入讨论吞吐量与延迟的关系。

另请参阅:Dark Shikari的博客(x264首席开发人员)的Cacheline splits, take two,谈论Core2上未对齐的加载策略:检查对齐并为该块使用其他策略是值得的。

脚注:

如今,

  • 64B高速缓存行是一个安全的假设。奔腾3和更早版本具有32B行。 P4有64B行,但它们通常是transferred in 128B-aligned pairs.。我想我记得读过P4在L2或L3中实际上有128B行,但是也许这只是对64B行成对传输的失真。 7-CPU definitely says 64B lines in both levels of cache for a P4 130nm


  • 另请参见uarch-bench结果for Skylake 。显然有人已经写了一个测试器来检查相对于高速缓存行边界的所有可能的未对准情况。

    我在Skylake table 面(i7-6700k)上的测试:

    寻址模式会影响负载使用的延迟,这与英特尔在其优化手册中所描述的完全相同。我用 mov rax, [rax+...]movzx/sx进行了整数测试(在这种情况下,使用加载的值作为索引,因为它太窄而不能用作指针)。
    ;;;  Linux x86-64 NASM/YASM source.  Assemble into a static binary
    ;; public domain, originally written by peter@cordes.ca.
    ;; Share and enjoy. If it breaks, you get to keep both pieces.

    ;;; This kind of grew while I was testing and thinking of things to test
    ;;; I left in some of the comments, but took out most of them and summarized the results outside this code block
    ;;; When I thought of something new to test, I'd edit, save, and up-arrow my assemble-and-run shell command
    ;;; Then edit the result into a comment in the source.

    section .bss

    ALIGN 2 * 1<<20 ; 2MB = 4096*512. Uses hugepages in .bss but not in .data. I checked in /proc/<pid>/smaps
    buf: resb 16 * 1<<20

    section .text
    global _start
    _start:
    mov esi, 128

    ; mov edx, 64*123 + 8
    ; mov edx, 64*123 + 0
    ; mov edx, 64*64 + 0
    xor edx,edx
    ;; RAX points into buf, 16B into the last 4k page of a 2M hugepage

    mov eax, buf + (2<<20)*0 + 4096*511 + 64*0 + 16
    mov ecx, 25000000

    %define ADDR(x) x ; SKL: 4c
    ;%define ADDR(x) x + rdx ; SKL: 5c
    ;%define ADDR(x) 128+60 + x + rdx*2 ; SKL: 11c cache-line split
    ;%define ADDR(x) x-8 ; SKL: 5c
    ;%define ADDR(x) x-7 ; SKL: 12c for 4k-split (even if it's in the middle of a hugepage)
    ; ... many more things and a block of other result-recording comments taken out

    %define dst rax



    mov [ADDR(rax)], dst
    align 32
    .loop:
    mov dst, [ADDR(rax)]
    mov dst, [ADDR(rax)]
    mov dst, [ADDR(rax)]
    mov dst, [ADDR(rax)]
    dec ecx
    jnz .loop

    xor edi,edi
    mov eax,231
    syscall

    然后运行
    asm-link load-use-latency.asm && disas load-use-latency && 
    perf stat -etask-clock,cycles,L1-dcache-loads,instructions,branches -r4 ./load-use-latency

    + yasm -felf64 -Worphan-labels -gdwarf2 load-use-latency.asm
    + ld -o load-use-latency load-use-latency.o
    (disassembly output so my terminal history has the asm with the perf results)

    Performance counter stats for './load-use-latency' (4 runs):

    91.422838 task-clock:u (msec) # 0.990 CPUs utilized ( +- 0.09% )
    400,105,802 cycles:u # 4.376 GHz ( +- 0.00% )
    100,000,013 L1-dcache-loads:u # 1093.819 M/sec ( +- 0.00% )
    150,000,039 instructions:u # 0.37 insn per cycle ( +- 0.00% )
    25,000,031 branches:u # 273.455 M/sec ( +- 0.00% )

    0.092365514 seconds time elapsed ( +- 0.52% )

    在这种情况下,我正在测试自然对齐的 mov rax, [rax],因此周期= 4 * L1-dcache-loads。 4c延迟。我没有禁用涡轮增压之类的功能。由于内核没有任何问题,因此内核时钟周期是最佳的测量方法。
  • [base + 0..2047]:4c负载使用延迟,11c缓存行拆分,11c 4k页拆分(即使在同一大页面内)。有关更多详细信息,请参见Is there a penalty when base+offset is in a different page than the base?:如果base+dispbase不在同一个页面中,则必须重播load uop。
  • 任何其他寻址模式:5c延迟,11c缓存行拆分,12c 4k拆分(即使在大页面内)。这包括[rax - 16]。并非如此,不是disp8 vs. disp32。

  • 因此:大页面无助于避免页面拆分的罚款(至少在TLB中两个页面都很热时)。高速缓存行拆分使寻址模式无关紧要,但是“快速”寻址模式对普通加载和页面拆分加载的延迟降低了1c。

    4k分割处理比以前要好得多,请参阅@harold的数字,其中Haswell的4k分割延迟约为32c。 (而且较旧的CPU可能甚至比这更糟。我认为在SKL之前,这应该会造成约100个周期的损失。)

    吞吐量(与寻址模式无关),通过使用除 rax之外的目标进行测量,因此负载是独立的:
  • 不拆分:0.5c。
  • CL分割:1c。
  • 4k分割:〜3.8至3.9c(比Skylake之前的CPU好得多)

  • 预期的 movzx/movsx的吞吐量/延迟(包括WORD拆分)相同,因为它们是在加载端口中处理的(不同于某些AMD CPU,其中也有ALU uop)。

    从RS(预留站)重放缓存行拆分负载。 uops_dispatched_port.port_2 + port_3 = 2x mov rdi, [rdi]的计数器,在另一个使用基本相同循环的测试中。 (这是一个从属负载情况,不受吞吐量限制。)直到AGU之后,您才能检测到拆分负载。

    大概是当负载uop从第二行发现需要数据时,它会寻找一个拆分寄存器(Intel CPU用于处理拆分负载的缓冲区),并将第一行中所需的数据部分放入该拆分中reg。并且还向RS发出信号,需要重播该信号。 (这是猜测。)

    我认为即使拆分中没有缓存行,拆分加载重播也应在几个周期内发生(也许只要加载端口向RS报告它是拆分的,即在地址生成之后)。因此,拆分两侧的需求负载请求可以立即发送。

    另请参见 Weird performance effects from nearby dependent stores in a pointer-chasing loop on IvyBridge. Adding an extra load speeds it up?以获取有关uop重放的更多信息。 (但请注意,这是针对与负载有关的uops,而不是负载uop本身。在该Q&A中,从属uops大部分也是负载。)

    准备好缓存未命中本身本身并不需要重放它以“接受”传入的数据,而仅依赖于uops 。请参阅有关 Are load ops deallocated from the RS when they dispatch, complete or some other time?的聊天讨论。在i7-6700k上的 https://godbolt.org/z/HJF3BN NASM测试案例显示,无论L1d命中还是L3命中,都分配了相同数量的负载。但是,分派(dispatch)的ALU微指令的数量(不计算循环开销)从每个负载1个增加到每个负载〜8.75个。当负载数据可能从L2缓存中到达时,调度程序会主动调度消耗数据的周期,然后在周期中进行主动调度,而不是等待一个额外的周期来查看它是否完成。

    当还可以在输入已经准备就绪的同一端口上进行其他独立但较年轻的工作时,我们尚未测试过重播的积极性。

    SKL具有两个硬件页面遍历单元,这可能与4k分割性能的大规模改进有关。即使没有TLB遗漏,大概是较旧的CPU也必须考虑可能存在的事实。

    有趣的是4k拆分吞吐量不是整数。我认为我的测量结果具有足够的精度和可重复性。请记住,这是将每个负载分割为4k,并且没有其他工作在进行中(除了在小型dec/jnz循环中)。如果您在真实代码中拥有此功能,那么您所做的事情确实是错误的。

    对于为什么它不是整数,我没有任何可靠的猜测,但是很显然,对于4k分割,微体系结构必须进行很多工作。它仍然是一个高速缓存行拆分,并且必须检查TLB两次。

    关于performance - 如何在x86_64上准确基准未对齐的访问速度,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45128763/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com