如何在x86_64上准确地对准未对齐的访问速度

时间:2017-07-16 12:44:08

标签: performance x86 x86-64 benchmarking inline-assembly

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

你看到这个基准测试有什么缺陷吗?你能改进吗(我的意思是,增加GB /秒,这样可以更好地反映真相)?

#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);
    }
}

3 个答案:

答案 0 :(得分:15)

计时方法。我可能会设置它以便测试由命令行arg选择,所以我可以用perf stat ./unaligned-test计时,并获得perf计数器结果,而不是每次测试的挂钟时间。这样,我不必关心涡轮/节能,因为我可以测量核心时钟周期。 (与gettimeofday / rdtsc参考周期不同,除非您禁用turbo和其他频率变化。)

您只测试吞吐量,而不是延迟,因为没有任何负载是依赖的。

您的缓存数量会比您的内存数字更糟糕,但您可能不会意识到这是因为您的缓存数量可能是因为处理负载的split-load registers数量存在瓶颈问题/跨越缓存行边界的商店。对于顺序读取,高速缓存的外部级别仍然只是看到整个高速缓存行的一系列请求。它只是从L1D获取数据的执行单元必须关心对齐。要测试非缓存情况的错位,可以进行分散加载,因此缓存行拆分需要将两个缓存行放入L1中。

高速缓存行是64B宽 1 ,因此您始终要测试高速缓存行拆分和高速缓存行访问的混合。测试总是分裂的负载会对分裂负载的微架构资源造成更大的困难。 (实际上,取决于您的CPU,cache-fetch width might be narrower than the line size。最近的Intel CPU可以从缓存行中获取任何未对齐的块,但这是因为它们具有特殊的硬件来实现这一点。其他CPU可能只是在自然对齐的16B块或其他内容中获取最快速度。@BeeOnRope says that AMD CPUs may care about 16B and 32B boundaries。)

您根本没有测试 store-&gt;加载转发。对于现有测试,以及一种可视化不同路线结果的好方法,请参阅这篇stuffedcow.net博客文章:Store-to-Load Forwarding and Memory Disambiguation in x86 Processors

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

我忘记了它是仅用于存储转发还是常规加载,但是当负载在缓存线边界上均匀分割时(8:8向量,也可能也是4:4或2:2整数分裂)。你应该测试一下。 (我可能会想到P4 lddqu或Core 2 movqdu

Intel's optimization manual有一个很大的错位表,而不是从广泛的商店转发到完全包含在其中的狭窄重新加载。在某些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加载.P5 Pentium及以后保证对齐的8B加载/存储是原子的,但P6和更新版本保证缓存的8B加载/存储是原子的,只要没有超过缓存行边界。与AMD不同,其中8B边界对于原子性保证很重要,即使在可缓存的内存中也是如此。Why is integer assignment on a naturally aligned variable atomic on x86?

查看Agner Fog的内容,了解更多关于未对齐的负载如何变慢的信息​​,以及烹饪测试以执行这些情况。实际上,Agner可能不是最好的资源,因为他的微观指南主要集中在通过管道获取uops。只是简要提一下缓存行拆分的成本,没有深入了解吞吐量与延迟的关系。

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

脚注:

  1. 这些天,64B缓存行是一个安全的假设。 Pentium 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
  2. 另请参阅uarch-bench结果for Skylake 。显然有人已经编写了一个测试程序,可以检查相对于缓存行边界的每个可能的错位。

    我在Skylake桌面上测试(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],自然对齐,所以cycles = 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+disp的页面与base不同,则必须重播加载uop。
    • 任何其他寻址模式:5c延迟,11c缓存线分割,12c 4k分割(甚至在巨大的页面内)。这包括[rax - 16]。它并不是disp8与disp32之间的差异。

    所以:largepages不帮助避免页面拆分惩罚(至少在TLB中两个页面都很热时都没有)。缓存行分割使寻址模式无关紧要,但是快速&#34;寻址模式对正常和页面分割负载的延迟降低1c。

    4k-split处理比以前更好,请参阅@ harold的数字,其中Haswell的4k-split有~32c延迟。 (而较旧的CPU可能会比那更糟。我认为SKL之前应该是~100周期惩罚。)

    吞吐量(无论寻址模式如何),使用rax以外的目的地来衡量,因此负载是独立的:

    • no split:0.5c。
    • CL-split:1c。
    • 4k-split:~3.8到3.9c(很多比前Skylake CPU更好)

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

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

    当负载uop发现它需要来自第二行的数据时,它会查找一个拆分寄存器(Intel CPU用来处理拆分负载的缓冲区),并从第一行输入所需的数据部分进入那个分裂注册。并且还向RS发回需要重播的信号。 (这是猜测。)

    有关uop重播的更多信息,另请参阅Weird performance effects from nearby dependent stores in a pointer-chasing loop on IvyBridge. Adding an extra load speeds it up?。 (但请注意,uops 依赖于加载,而不是加载uop本身。我认为缓存未命中加载会设置在数据到达时使用数据的所有内容,而不必是重播本身。问题是调度程序主动安排消耗数据的uop在负载数据可能从L2高速缓存到达的周期中调度,而不是等待一个额外的周期来查看它是否有。在那个Q&amp; A中,依赖的uops也主要是负载。)

    所以我认为即使两个缓存行都不存在,分割负载重放也应该在几个周期内完成,因此分割两边的需求加载请求可以立即进行。

    SKL有两个硬件页面漫游单元,这可能与4k分割性能的大幅提升有关。即使没有TLB未命中,可能是较旧的CPU必须考虑到可能存在的事实。

    有趣的是,4k-split吞吐量是非整数的。我认为我的测量结果具有足够的精度和可重复性。请记住,这是每个加载是4k-split,并且没有其他工作正在进行(除了在一个小的dec / jnz循环中)。如果您在实际代码中使用过此代码,那么您确实做错了。

    我对于为什么它可能是非整数没有任何可靠的猜测,但显然有很多必须在微架构上发生4k-split。它仍然是一个缓存行拆分,它必须检查TLB两次。

答案 1 :(得分:4)

测试各种偏移的64位负载(下面的代码),我在Haswell上的原始结果是:

aligned L: 4.01115 T: 0.500003
ofs1 L: 4.00919 T: 0.500003
ofs2 L: 4.01494 T: 0.500003
ofs3 L: 4.01403 T: 0.500003
ofs7 L: 4.01073 T: 0.500003
ofs15 L: 4.01937 T: 0.500003
ofs31 L: 4.02107 T: 0.500002
ofs60 L: 9.01482 T: 1
ofs62 L: 9.03644 T: 1
ofs4092 L: 32.3014 T: 31.1967

根据你的需要应用舍入,其中大部分显然应该向下舍入,但.3和.2(从页面边界跨越)可能太重要而不是噪音。这只测试了具有简单地址的负载,只测试了“纯负载”,没有转发。

我得出结论,高速缓存行中的对齐与标量载荷无关,只是跨越高速缓存行边界并且(特别是,并且出于显而易见的原因)跨越页面边界很重要。在这种情况下,在中间或其他地方完全跨越缓存行边界似乎没有区别。

AMD偶尔会有一些16字节边界的有趣效果,但我无法测试。

以下是原始(!)xmm矢量结果,其中包含pextrq的效果,因此减去2个延迟周期:

aligned L: 8.05247 T: 0.500003
ofs1 L: 8.03223 T: 0.500003
ofs2 L: 8.02899 T: 0.500003
ofs3 L: 8.05598 T: 0.500003
ofs7 L: 8.03579 T: 0.500002
ofs15 L: 8.02787 T: 0.500003
ofs31 L: 8.05002 T: 0.500003
ofs58 L: 13.0404 T: 1
ofs60 L: 13.0825 T: 1
ofs62 L: 13.0935 T: 1
ofs4092 L: 36.345 T: 31.2357

测试代码是

global test_unaligned_l
proc_frame test_unaligned_l
    alloc_stack 8
[endprolog]
    mov r9, rcx
    rdtscp
    mov r8d, eax

    mov ecx, -10000000
    mov rdx, r9
.loop:
    mov rdx, [rdx]
    mov rdx, [rdx]
    add ecx, 1
    jnc .loop

    rdtscp
    sub eax, r8d

    add rsp, 8
    ret
endproc_frame

global test_unaligned_tp
proc_frame test_unaligned_tp
    alloc_stack 8
[endprolog]
    mov r9, rcx
    rdtscp
    mov r8d, eax

    mov ecx, -10000000
    mov rdx, r9
.loop:
    mov rax, [rdx]
    mov rax, [rdx]
    add ecx, 1
    jnc .loop

    rdtscp
    sub eax, r8d

    add rsp, 8
    ret
endproc_frame

对于大致相似但在延迟测试中具有pextrq的向量。

在各种偏移量下准备一些数据,例如:

align 64
%rep 31
db 0
%endrep
unaligned31: dq unaligned31
align 4096
%rep 60
db 0
%endrep
unaligned60: dq unaligned60
align 4096
%rep 4092
db 0
%endrep
unaligned4092: dq unaligned4092

为了更多地关注新标题,我将描述这是尝试做什么以及为什么。

首先,有延迟测试。从不在eax中的某个指针(如问题中的代码所示)中将一百万个内容加载到eax中,测试吞吐量,这只是图片的一半。对于平凡的标量载荷,对于向量载荷,我使用了对:

movdqu xmm0, [rdx]
pextrq rdx, xmm0, 0

pextrq的延迟为2,这就是为什么矢量负载的延迟数据都太高了,如上所述。

为了便于进行此延迟测试,数据是一个自引用指针。这是一个相当不典型的情况,但它不应该影响负载的时序特性。

吞吐量测试每个循环有两个负载而不是一个负载,以避免由循环开销造成瓶颈。可以使用更多的负载,但这对Haswell来说是不必要的(或者我能想到的任何东西,但理论上可能存在分支吞吐量较低或负载吞吐量较高的μarch)。

我对TSC读取中的防护或补偿其开销(或其他开销)并不十分谨慎。我也没有禁用Turbo,我只是让它以turbo频率运行并除以TSC速率和turbo频率之间的比率,这可能会影响时序。与大约1E7的基准相比,所有这些效果都很小,无论如何结果都可以舍入。

所有时间都是30的最佳值,平均值和方差等因素在这些微观基准测试中毫无意义,因为基本事实不是一个随机过程,我们想要估算参数,但有些固定整数 [1] (或分数的整数倍,用于吞吐量)。几乎所有的噪声都是正的,除了在第一次TSC读取前基准“泄漏”的(相对理论上的)指令情况(如果有必要,甚至可以避免),所以采取最小值是合适的。

注1:除了明显越过4k边界外,那里发生了一些奇怪的事情。

答案 2 :(得分:1)

我在这里提出了一些改进的基准。仍然仅测量吞吐量(并且仅测量未对齐的偏移1)。基于其他答案,我添加了测量64和4096字节的分割。

对于4k分裂,有一个巨大的差异!但是如果数据没有超过64字节边界,那么根本就没有速度损失(至少对于我测试的这两个处理器而言)。

查看这些数字(以及其他答案的数字),我的结论是未对齐访问平均速度很快(吞吐量和延迟),但有时可能会慢很多。但这并不意味着不鼓励使用它们。

我的基准测试产生的原始数字应该用一粒盐(很可能是正确编写的asm代码优于它),但这些结果大多与哈罗德对Haswell(差异列)的答案一致。

Haswell:

Full:
 32-bit, cache: aligned:  33.2901 GB/sec unaligned:  29.5063 GB/sec, difference: 1.128x
 32-bit,   mem: aligned:  12.1597 GB/sec unaligned:  12.0659 GB/sec, difference: 1.008x
 64-bit, cache: aligned:  66.0368 GB/sec unaligned:  52.8914 GB/sec, difference: 1.249x
 64-bit,   mem: aligned:  16.1317 GB/sec unaligned:  16.0568 GB/sec, difference: 1.005x
128-bit, cache: aligned: 129.8730 GB/sec unaligned:  87.9791 GB/sec, difference: 1.476x
128-bit,   mem: aligned:  16.8150 GB/sec unaligned:  16.8151 GB/sec, difference: 1.000x

JustBoundary64:
 32-bit, cache: aligned:  32.5555 GB/sec unaligned:  16.0175 GB/sec, difference: 2.032x
 32-bit,   mem: aligned:   1.0044 GB/sec unaligned:   1.0001 GB/sec, difference: 1.004x
 64-bit, cache: aligned:  65.2707 GB/sec unaligned:  32.0431 GB/sec, difference: 2.037x
 64-bit,   mem: aligned:   2.0093 GB/sec unaligned:   2.0007 GB/sec, difference: 1.004x
128-bit, cache: aligned: 130.6789 GB/sec unaligned:  64.0851 GB/sec, difference: 2.039x
128-bit,   mem: aligned:   4.0180 GB/sec unaligned:   3.9994 GB/sec, difference: 1.005x

WithoutBoundary64:
 32-bit, cache: aligned:  33.2911 GB/sec unaligned:  33.2916 GB/sec, difference: 1.000x
 32-bit,   mem: aligned:  11.6156 GB/sec unaligned:  11.6223 GB/sec, difference: 0.999x
 64-bit, cache: aligned:  65.9117 GB/sec unaligned:  65.9548 GB/sec, difference: 0.999x
 64-bit,   mem: aligned:  14.3200 GB/sec unaligned:  14.3027 GB/sec, difference: 1.001x
128-bit, cache: aligned: 128.2605 GB/sec unaligned: 128.3342 GB/sec, difference: 0.999x
128-bit,   mem: aligned:  12.6352 GB/sec unaligned:  12.6218 GB/sec, difference: 1.001x

JustBoundary4096:
 32-bit, cache: aligned:  33.5500 GB/sec unaligned:   0.5415 GB/sec, difference: 61.953x
 32-bit,   mem: aligned:   0.4527 GB/sec unaligned:   0.0431 GB/sec, difference: 10.515x
 64-bit, cache: aligned:  67.1141 GB/sec unaligned:   1.0836 GB/sec, difference: 61.937x
 64-bit,   mem: aligned:   0.9112 GB/sec unaligned:   0.0861 GB/sec, difference: 10.582x
128-bit, cache: aligned: 134.2000 GB/sec unaligned:   2.1668 GB/sec, difference: 61.936x
128-bit,   mem: aligned:   1.8165 GB/sec unaligned:   0.1700 GB/sec, difference: 10.687x

Sandy Bridge (processor from 2011)

Full:
 32-bit, cache: aligned:  30.0302 GB/sec unaligned:  26.2587 GB/sec, difference: 1.144x
 32-bit,   mem: aligned:  11.0317 GB/sec unaligned:  10.9358 GB/sec, difference: 1.009x
 64-bit, cache: aligned:  59.2220 GB/sec unaligned:  41.5515 GB/sec, difference: 1.425x
 64-bit,   mem: aligned:  14.5985 GB/sec unaligned:  14.3760 GB/sec, difference: 1.015x
128-bit, cache: aligned: 115.7643 GB/sec unaligned:  45.0905 GB/sec, difference: 2.567x
128-bit,   mem: aligned:  14.8561 GB/sec unaligned:  14.8220 GB/sec, difference: 1.002x

JustBoundary64:
 32-bit, cache: aligned:  15.2127 GB/sec unaligned:   3.1037 GB/sec, difference: 4.902x
 32-bit,   mem: aligned:   0.9870 GB/sec unaligned:   0.6110 GB/sec, difference: 1.615x
 64-bit, cache: aligned:  30.2074 GB/sec unaligned:   6.2258 GB/sec, difference: 4.852x
 64-bit,   mem: aligned:   1.9739 GB/sec unaligned:   1.2194 GB/sec, difference: 1.619x
128-bit, cache: aligned:  60.7265 GB/sec unaligned:  12.4007 GB/sec, difference: 4.897x
128-bit,   mem: aligned:   3.9443 GB/sec unaligned:   2.4460 GB/sec, difference: 1.613x

WithoutBoundary64:
 32-bit, cache: aligned:  30.0348 GB/sec unaligned:  29.9801 GB/sec, difference: 1.002x
 32-bit,   mem: aligned:  10.7067 GB/sec unaligned:  10.6755 GB/sec, difference: 1.003x
 64-bit, cache: aligned:  59.1895 GB/sec unaligned:  59.1925 GB/sec, difference: 1.000x
 64-bit,   mem: aligned:  12.9404 GB/sec unaligned:  12.9307 GB/sec, difference: 1.001x
128-bit, cache: aligned: 116.4629 GB/sec unaligned: 116.0778 GB/sec, difference: 1.003x
128-bit,   mem: aligned:  11.2963 GB/sec unaligned:  11.3533 GB/sec, difference: 0.995x

JustBoundary4096:
 32-bit, cache: aligned:  30.2457 GB/sec unaligned:   0.5626 GB/sec, difference: 53.760x
 32-bit,   mem: aligned:   0.4055 GB/sec unaligned:   0.0275 GB/sec, difference: 14.726x
 64-bit, cache: aligned:  60.6175 GB/sec unaligned:   1.1257 GB/sec, difference: 53.851x
 64-bit,   mem: aligned:   0.8150 GB/sec unaligned:   0.0551 GB/sec, difference: 14.798x
128-bit, cache: aligned: 121.2121 GB/sec unaligned:   2.2455 GB/sec, difference: 53.979x
128-bit,   mem: aligned:   1.6255 GB/sec unaligned:   0.1103 GB/sec, difference: 14.744x

以下是代码:

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

__attribute__((always_inline))
void load32(const char *v) {
    __asm__ ("mov     %0, %%eax" : : "m"(*v) :"eax");
}

__attribute__((always_inline))
void load64(const char *v) {
    __asm__ ("mov     %0, %%rax" : : "m"(*v) :"rax");
}

__attribute__((always_inline))
void load128a(const char *v) {
    __asm__ ("movaps     %0, %%xmm0" : : "m"(*v) :"xmm0");
}

__attribute__((always_inline))
void load128u(const char *v) {
    __asm__ ("movups     %0, %%xmm0" : : "m"(*v) :"xmm0");
}

struct Full {
    template <int S>
    static float factor() {
        return 1.0f;
    }
    template <void (*LOAD)(const char *), int S, int N>
    static void loop(const char *v) {
        for (int i=0; i<N; i+=S*16) {
            LOAD(v+S* 0);
            LOAD(v+S* 1);
            LOAD(v+S* 2);
            LOAD(v+S* 3);
            LOAD(v+S* 4);
            LOAD(v+S* 5);
            LOAD(v+S* 6);
            LOAD(v+S* 7);
            LOAD(v+S* 8);
            LOAD(v+S* 9);
            LOAD(v+S*10);
            LOAD(v+S*11);
            LOAD(v+S*12);
            LOAD(v+S*13);
            LOAD(v+S*14);
            LOAD(v+S*15);
            v += S*16;
        }
    }
};

struct JustBoundary64 {
    template <int S>
    static float factor() {
        return S/64.0f;
    }
    template <void (*LOAD)(const char *), int S, int N>
    static void loop(const char *v) {
        static_assert(N%(64*16)==0);
        for (int i=0; i<N; i+=64*16) {
            LOAD(v+64* 1-S);
            LOAD(v+64* 2-S);
            LOAD(v+64* 3-S);
            LOAD(v+64* 4-S);
            LOAD(v+64* 5-S);
            LOAD(v+64* 6-S);
            LOAD(v+64* 7-S);
            LOAD(v+64* 8-S);
            LOAD(v+64* 9-S);
            LOAD(v+64*10-S);
            LOAD(v+64*11-S);
            LOAD(v+64*12-S);
            LOAD(v+64*13-S);
            LOAD(v+64*14-S);
            LOAD(v+64*15-S);
            LOAD(v+64*16-S);
            v += 64*16;
        }
    }
};

struct WithoutBoundary64 {
    template <int S>
    static float factor() {
        return (64-S)/64.0f;
    }
    template <void (*LOAD)(const char *), int S, int N>
    static void loop(const char *v) {
        for (int i=0; i<N; i+=S*16) {
            if ((S* 1)&0x3f) LOAD(v+S* 0);
            if ((S* 2)&0x3f) LOAD(v+S* 1);
            if ((S* 3)&0x3f) LOAD(v+S* 2);
            if ((S* 4)&0x3f) LOAD(v+S* 3);
            if ((S* 5)&0x3f) LOAD(v+S* 4);
            if ((S* 6)&0x3f) LOAD(v+S* 5);
            if ((S* 7)&0x3f) LOAD(v+S* 6);
            if ((S* 8)&0x3f) LOAD(v+S* 7);
            if ((S* 9)&0x3f) LOAD(v+S* 8);
            if ((S*10)&0x3f) LOAD(v+S* 9);
            if ((S*11)&0x3f) LOAD(v+S*10);
            if ((S*12)&0x3f) LOAD(v+S*11);
            if ((S*13)&0x3f) LOAD(v+S*12);
            if ((S*14)&0x3f) LOAD(v+S*13);
            if ((S*15)&0x3f) LOAD(v+S*14);
            if ((S*16)&0x3f) LOAD(v+S*15);
            v += S*16;
        }
    }
};

struct JustBoundary4096 {
    template <int S>
    static float factor() {
        return S/4096.0f;
    }
    template <void (*LOAD)(const char *), int S, int N>
    static void loop(const char *v) {
        static_assert(N%(4096*4)==0);
        for (int i=0; i<N; i+=4096*4) {
            LOAD(v+4096*1-S);
            LOAD(v+4096*2-S);
            LOAD(v+4096*3-S);
            LOAD(v+4096*4-S);
            v += 4096*4;
        }
    }
};


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

template <typename TYPE, void (*LOADa)(const char *), void (*LOADu)(const char *), int S, int N>
void bench(const char *data, int iter, const char *name) {
    long long int t0 = t();
    for (int i=0; i<iter*100000; i++) {
        TYPE::template loop<LOADa, S, N/100000>(data);
    }
    long long int t1 = t();
    for (int i=0; i<iter*100000; i++) {
        TYPE::template loop<LOADu, S, N/100000>(data+1);
    }
    long long int t2 = t();
    for (int i=0; i<iter; i++) {
        TYPE::template loop<LOADa, S, N>(data);
    }
    long long int t3 = t();
    for (int i=0; i<iter; i++) {
        TYPE::template loop<LOADu, S, N>(data+1);
    }
    long long int t4 = t();

    printf("%s-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3fx\n", name, (double)N*iter/(t1-t0)/1000*TYPE::template factor<S>(), (double)N*iter/(t2-t1)/1000*TYPE::template factor<S>(), (float)(t2-t1)/(t1-t0));
    printf("%s-bit,   mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3fx\n", name, (double)N*iter/(t3-t2)/1000*TYPE::template factor<S>(), (double)N*iter/(t4-t3)/1000*TYPE::template factor<S>(), (float)(t4-t3)/(t3-t2));
}

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

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

    printf("Full:\n");
    bench<Full, load32, load32, 4, N>(data, ITER, " 32");
    bench<Full, load64, load64, 8, N>(data, ITER, " 64");
    bench<Full, load128a, load128u, 16, N>(data, ITER, "128");

    printf("\nJustBoundary64:\n");
    bench<JustBoundary64, load32, load32, 4, N>(data, ITER, " 32");
    bench<JustBoundary64, load64, load64, 8, N>(data, ITER, " 64");
    bench<JustBoundary64, load128a, load128u, 16, N>(data, ITER, "128");

    printf("\nWithoutBoundary64:\n");
    bench<WithoutBoundary64, load32, load32, 4, N>(data, ITER, " 32");
    bench<WithoutBoundary64, load64, load64, 8, N>(data, ITER, " 64");
    bench<WithoutBoundary64, load128a, load128u, 16, N>(data, ITER, "128");

    printf("\nJustBoundary4096:\n");
    bench<JustBoundary4096, load32, load32, 4, N>(data, ITER*10, " 32");
    bench<JustBoundary4096, load64, load64, 8, N>(data, ITER*10, " 64");
    bench<JustBoundary4096, load128a, load128u, 16, N>(data, ITER*10, "128");
}