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

问题描述 投票:5回答:3

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);
    }
}
performance x86 x86-64 benchmarking inline-assembly
3个回答
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。最近的英特尔CPU可以从缓存线内部获取任何未对齐的块,但这是因为它们具有特殊的硬件来实现这一点。其他CPU在自然获取时可能只有最快的速度 - 对齐16B块或其他东西.@BeeOnRope says that AMD CPUs may care about 16B and 32B boundaries。)

你根本不测试store-> load forwarding。对于现有的测试,以及一种可视化不同路线结果的好方法,请参阅这篇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你需要查看多长时间才能找到非向量的问题在高速缓存行中加载。它将是一个32位的CPU,但你仍然可以用MMX或SSE甚至x87测试8B负载.P5奔腾以及后来保证对齐的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

另见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 [email protected].
;; 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],因此cycle = 4 * L1-dcache-loads。 4c延迟。我没有禁用turbo或类似的东西。由于核心没有任何东西,核心时钟周期是最好的衡量方法。

  • [base + 0..2047]:4c负载使用延迟,11c缓存行拆分,11c 4k页拆分(即使在同一个巨页内)。有关更多详细信息,请参阅Is there a penalty when base+offset is in a different page than the base?:如果base+dispbase位于不同的页面,则必须重播加载uop。
  • 任何其他寻址模式:5c延迟,11c缓存线分割,12c 4k分割(甚至在一个巨大的页面内)。这包括[rax - 16]。这不是disp8与disp32的区别。

所以:hugepages无助于避免页面拆分惩罚(至少在TLB中两个页面都很热时都不会)。高速缓存行拆分使寻址模式无关紧要,但“快速”寻址模式对正常和页面拆分负载的延迟降低1c。

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

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

  • 没有分裂: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的计数器= qxxswpoi的2x数,在另一个使用基本相同的循环的测试中。 (这是一个依赖负载情况,而不是吞吐量限制。)在AGU之前,您无法检测到分割负载。

据推测,当加载uop发现它需要来自第二行的数据时,它会查找拆分寄存器(Intel CPU用来处理拆分加载的缓冲区),并将第一行所需的数据部分放入该拆分中REG。并且还向RS发回需要重播的信号。 (这是猜测。)

有关uop重放的更多信息,另请参阅mov rdi, [rdi]。 (但请注意,uops依赖于负载,而不是负载uop本身。我认为缓存未命中负载会设置所有内容,以便在数据到达时使用数据,而不必自行重放。问题是调度程序主动调度当负载数据可能从L2缓存到达时,uops消耗数据以在循环中调度,而不是等待一个额外的周期来查看它是否有。在那个Q&A中,依赖的uops也主要是负载。)

因此,我认为即使不存在高速缓存行,分割负载重放也应该在几个周期内发生,因此分割两侧的需求加载请求可以立即进行。


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

有趣的是,4k-split吞吐量是非整数的。我认为我的测量结果具有足够的精度和可重复性。请记住,每次加载都是4k-split,没有其​​他工作正在进行(除了在一个小的dec / jnz循环中)。如果你有真正的代码,你做的事情真的错了。

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


4
投票

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

Weird performance effects from nearby dependent stores in a pointer-chasing loop on IvyBridge. Adding an extra load speeds it up?

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

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

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

这里是原始(!)xmm矢量结果,其中包括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 的影响,因此减去2个周期的延迟:

pextrq

测试代码是

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 的延迟为2,这就是为什么矢量负载的延迟数据都太高,如上所述。

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

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

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

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

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


1
投票

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

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

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

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

pextrq

这是代码:

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
© www.soinside.com 2019 - 2024. All rights reserved.