当前位置: 首页 > 知识库问答 >
问题:

如何在x86\u 64上准确地基准测试未对齐的访问速度?

宇文灿
2023-03-14

在回答中,我说过长期以来(在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个答案

戚衡
2023-03-14

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

对于4k分割,有很大的不同!但是,如果数据没有跨越64字节的边界,就根本没有速度损失(至少对于我测试过的这两个处理器而言)。

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

我的基准测试生成的原始数据应该谨慎对待(正确编写的asm代码很有可能会优于它),但这些结果大部分与harold对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");
}
邹华池
2023-03-14

测试各种偏移量的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的影响,因此减去两个延迟周期:

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

对于向量很大程度上相似,但在延迟测试中使用pexqq

在不同偏移处准备一些数据,例如:

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,这就是为什么向量加载的延迟数字都是2,正如所指出的那样。

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

吞吐量测试每个循环有两个负载,而不是一个负载,以避免受到循环开销的限制。可以使用更多的负载,但这在Haswell上是不必要的(或者我能想到的任何东西,但理论上可能存在分支吞吐量较低或负载吞吐量较高的微体系结构)。

我对TSC读取中的Geofence或补偿其开销(或其他开销)不是特别小心。我也没有禁用Turbo,我只是让它以Turbo频率运行,然后除以TSC速率和Turbo频率之间的比率,这可能会对计时产生一些影响。与1E7级的基准相比,所有这些影响都很小,结果可以四舍五入。

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

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

柴飞扬
2023-03-14

计时方法。我可能会将其设置为通过命令行参数选择测试,以便使用perf stat计时/未对齐测试,并获取性能计数器结果,而不是每个测试的挂钟时间。这样,我就不必关心涡轮/节能,因为我可以测量核心时钟周期。(与参考周期不同,除非禁用turbo和其他频率变化。)

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

您的缓存编号将比内存编号更差,但您可能没有意识到这是因为您的缓存编号可能是由于处理跨越缓存线边界的加载/存储的拆分加载寄存器数量瓶颈造成的。对于顺序读取,缓存的外部级别始终只会看到整个缓存线的一系列请求。只有从L1D获取数据的执行单元才需要关心对齐。要测试非缓存情况下的未对齐,可以进行分散加载,因此缓存线拆分需要将两条缓存线引入一级缓存。

缓存行是64字节宽的1,因此您总是在测试缓存行拆分和缓存行内访问的混合。测试始终拆分的负载会对拆分加载的微架构资源造成更大的瓶颈。(实际上,根据您的CPU,缓存获取宽度可能比行大小更窄。最近的Intel CPU可以从缓存行内获取任何未对齐的块,但那是因为它们有特殊的硬件来提高速度。其他CPU可能只有在自然对齐的16字节块或其他内容中获取时才会最快。@BeeOnRope表示AMD CPU可能会关心16字节和32字节的边界。)

你不是在测试商店→ 根本无法加载转发。对于现有的测试,以及可视化不同路线结果的好方法,请参见此stuffedcow。net博客文章:在x86处理器中存储以加载转发和内存消歧。

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

我忘记了它只是用于存储转发还是用于常规加载,但当加载在缓存行边界(8:8向量,也可能是4:4或2:2整数拆分)上均匀拆分时,可能会受到更少的惩罚。您应该测试一下。(我可能在考虑P4lddqu或Core 2movqdu

英特尔的优化手册中有大量的未对齐与存储转发表,从广泛的存储转发到狭窄的重新加载,这些都包含在手册中。在某些CPU上,当宽存储区自然对齐时,即使它不跨越任何缓存线边界,这种方法在更多情况下仍然有效。(可能是在SnB/IvB上,因为它们使用一个具有16B个存储组的一级缓存,而这些存储组之间的拆分可能会影响存储转发。

我没有重新检查手册,但如果你真的想通过实验来测试这一点,那就是你应该寻找的。)

这提醒了我,未对齐的负载更有可能引发SnB/IvB上的缓存库冲突(因为一个负载可能触及两个缓存库)。但您不会看到来自单个流的加载,因为在一个周期内两次访问同一行中的同一银行是可以的。它只是在不同的线路上访问同一家银行,这在同一个周期内是不可能发生的。(例如,当两次内存访问间隔为128字节的倍数时。)

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

您无法在对齐的地址上测试movups,因此无法检测到movups比Core上的movaps慢

去看看Agner Fog的东西,了解更多关于未对齐的负载如何变慢的信息,并编写测试来练习这些情况。实际上,Agner可能不是最好的资源,因为他的微架构指南主要关注通过管道获得uops。只是简单地提到缓存行拆分的成本,没有深入介绍吞吐量与延迟。

另请参阅Dark Shikari的博客(x264首席开发人员)中的《缓存线拆分》,第二篇,讨论Core2上未对齐的加载策略:检查对齐情况并对块使用不同的策略是值得的。

脚注:

  1. 如今,64B缓存线是一个安全的假设。奔腾3和更早的版本有32B线路。P4有64B的线,但它们通常以128B对齐的线对传输。我想我记得读到P4实际上在L2或L3中有128B行,但这可能只是64B行成对传输的失真。对于P4 130nm,7-CPU明确表示两级缓存中都有64B缓存线

另请参见Skylake的uarch测试台结果。显然,有人已经编写了一个测试程序来检查相对于缓存线边界的所有可能的未对齐。

##我在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],自然对齐,所以周期=4*L1-dcache-load。4c延迟。我没有禁用turbo或类似的东西。由于没有什么会偏离核心,因此核心时钟周期是衡量的最佳方式。

  • [base 0...2047]:4c加载使用延迟,11c缓存行拆分,11c4k页拆分(即使在同一hugpage内)。请参阅当基本偏移量与基本偏移量位于不同的页面时是否有惩罚?有关更多详细信息:如果base disp原来位于与base不同的页面中,则必须重放加载uop。
  • 任何其他寻址模式:5c延迟、11c缓存行拆分、12c4k拆分(甚至在hugpage中)。这包括[rax-16]。这不是差异8与差异32。

因此:hugepages无助于避免页面分割惩罚(至少在TLB中两个页面都很热的情况下是这样)。缓存线拆分使寻址模式不相关,但对于正常和页拆分加载,“快速”寻址模式的延迟较低。

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\u dispatched\u端口的计数器。port\u 2port\u 3=在另一个使用基本相同回路的测试中,mov rdi的2倍数量。(这是一种依赖的负载情况,而不是吞吐量限制。)在AGU之后才能检测到拆分负载。

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

我认为即使拆分中没有任何缓存行,拆分加载重播也应该在几个周期内发生(也许是在加载端口向RS报告它是拆分之后,即地址生成之后)。因此拆分双方的需求加载请求可以同时运行。

在IvyBridge上的指针追踪循环中,也可以看到附近依赖存储的奇怪性能影响。添加额外负载会加快速度吗?有关uop重播的更多信息。(但请注意,这是针对依赖于负载的uop,而不是负载uop本身

缓存未命中加载本身不需要重放以在准备就绪时“接受”传入数据,只需要依赖UOP。请参阅聊天室讨论,在RS调度、完成或其他时间,是否从RS释放负载操作?。这https://godbolt.org/z/HJF3BNi7-6700k上的NASM测试用例显示,无论L1d命中还是L3命中,调度的加载UOP数都相同。但调度的ALU UOP数量(不计算循环开销)从每负载1个增加到了约8.75个。当加载数据可能从二级缓存到达时,调度程序会积极地调度消耗数据的UOP,而不是等待一个额外的周期来查看它是否到达。

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

有趣的是,4k分割吞吐量是非整数的。我认为我的测量有足够的精度和重复性来说明这一点。记住,这是因为每个负载都是4k分割,没有其他工作在进行(除了在一个小的dec/jnz循环内)。如果你在真实的代码中有这样的代码,那你就真的做错了。

我对为什么它可能是非整数没有任何确切的猜测,但很明显,对于4k拆分,在微体系结构上必须发生很多事情。它仍然是缓存线拆分,必须检查TLB两次。

 类似资料:
  • 也许我误解了什么,但x86中的未对齐访问似乎带来了安全问题,例如返回地址完整性问题。 > 为什么x86设计器首先允许未对齐的访问?(性能是我能想到的唯一好处。) 如果x86设计人员允许这种未对齐的访问问题,他们应该知道如何解决它,不是吗?是否可以使用静态技术或清理技术检测未对齐的访问?

  • 我想模拟x86/x86_64上禁止未对齐内存访问的系统。是否有一些调试工具或特殊模式来执行此操作? 当使用为SPARC或其他类似CPU设计的软件(C/C)时,我想在几台x86/x86_64PC上运行许多(CPU密集型)测试。但是我对Sparc的访问是有限的。 正如我所知,Sparc总是检查内存读写的对齐是否正常(从任何地址读取一个字节,但仅当地址可被4整除时才允许读取一个4字节的字)。 可能是Va

  • 我定期关注GitHub Rakudo存储库,查看Rakudo编译器发生了哪些变化。 我有时会看到提交,其中单个函数的速度提高了一定的百分比,时间如下图所示。 评估这一点的工作流是什么?我很想了解这一点,以便了解您的功能是如何执行的,并相应地进一步优化,从而为Rakudo的发展做出贡献。 我在这里阅读帮助。我谷歌了一下,但找不到此信息。我还通过命令行选项了解了MoarVM分析器,它会生成输出。要寻找

  • 11.4. 基准测试 基准测试是测量一个程序在固定工作负载下的性能。在Go语言中,基准测试函数和普通测试函数写法类似,但是以Benchmark为前缀名,并且带有一个*testing.B类型的参数;*testing.B参数除了提供和*testing.T类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N,用于指定操作执行的循环次数。 下面是IsPalindrome函数的基准测试,其中循

  • GoCPPLua (JIT) 策略执行的负载在model_b_test.go中进行基准测试。 测试是: 英特尔 酷睿 i7-6700HQ CPU @ 2.60GHz, 2601 Mhz, 4 核, 8 处理器 go test -bench= -benchmem 的测试结果如下 (op = 一次 Enforce() 调用, ms = 毫秒, KB = 千字节): 测试用例 规则大小 时间开销 (m

  • 我得到低于错误,而运行基准测试使用与默认设置的标尺。 <17:26:41>未能设置基准驱动程序(将关闭并退出)。类org.apache.ignite.igniteCheckedException:无法启动管理器:GridManagerAdapter[enabled=true,name=org.apache.ignite.internal.managers.discovery.gridDiscove