为什么_mm_stream_ps会产生L1 / LL缓存未命中?

时间:2012-01-30 17:41:38

标签: c performance caching gcc sse

我正在尝试优化计算密集型算法,并且遇到了一些缓存问题。我有一个巨大的缓冲区,它偶尔会随机写入,并且在应用程序结束时只能读取一次。显然,写入缓冲区会产生大量的缓存未命中,并且还会污染之后需要再次进行计算的缓存。我试图使用非时间移动内在函数,但缓存未命中(由valgrind报告并由运行时测量支持)仍然会发生。但是,为了进一步研究非时间动作,我写了一个小测试程序,你可以在下面看到。顺序访问,大缓冲区,只写。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <smmintrin.h>

void tim(const char *name, void (*func)()) {
    struct timespec t1, t2;
    clock_gettime(CLOCK_REALTIME, &t1);
    func();
    clock_gettime(CLOCK_REALTIME, &t2);
    printf("%s : %f s.\n", name, (t2.tv_sec - t1.tv_sec) + (float) (t2.tv_nsec - t1.tv_nsec) / 1000000000);
}

const int CACHE_LINE = 64;
const int FACTOR = 1024;
float *arr;
int length;

void func1() {
    for(int i = 0; i < length; i++) {
        arr[i] = 5.0f;
    }
}

void func2() {
    for(int i = 0; i < length; i += 4) {
        arr[i] = 5.0f;
        arr[i+1] = 5.0f;
        arr[i+2] = 5.0f;
        arr[i+3] = 5.0f;
    }
}

void func3() {
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f);
    for(int i = 0; i < length; i += 4) {
        _mm_stream_ps(&arr[i], buf);
    }
}

void func4() {
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f);
    for(int i = 0; i < length; i += 16) {
        _mm_stream_ps(&arr[i], buf);
        _mm_stream_ps(&arr[4], buf);
        _mm_stream_ps(&arr[8], buf);
        _mm_stream_ps(&arr[12], buf);
    }
}

int main() {
    length = CACHE_LINE * FACTOR * FACTOR;

    arr = malloc(length * sizeof(float));
    tim("func1", func1);
    free(arr);

    arr = malloc(length * sizeof(float));
    tim("func2", func2);
    free(arr);

    arr = malloc(length * sizeof(float));
    tim("func3", func3);
    free(arr);

    arr = malloc(length * sizeof(float));
    tim("func4", func4);
    free(arr);

    return 0;
}

功能1是天真的方法,功能2使用循环展开。功能3使用movntps,实际上至少在我检查-O0时插入到程序集中。在函数4中,我尝试一次发出几个movntps指令,以帮助CPU进行写入组合。我用gcc -g -lrt -std=gnu99 -OX -msse4.1 test.c编译了代码,其中X是[0..3]之一。结果很有意思:

-O0
func1 : 0.407794 s.
func2 : 0.320891 s.
func3 : 0.161100 s.
func4 : 0.401755 s.
-O1
func1 : 0.194339 s.
func2 : 0.182536 s.
func3 : 0.101712 s.
func4 : 0.383367 s.
-O2
func1 : 0.108488 s.
func2 : 0.088826 s.
func3 : 0.101377 s.
func4 : 0.384106 s.
-O3
func1 : 0.078406 s.
func2 : 0.084927 s.
func3 : 0.102301 s.
func4 : 0.383366 s.

正如您所看到的,当程序未通过gcc优化时,_mm_stream_ps比其他程序快一点,但是当打开gcc优化时,它的目的明显失败。 Valgrind仍然报告了很多缓存写入错误。

所以,问题是:为什么即使我正在使用NTA流指令,仍然会发生那些(L1 + LL)缓存未命中?为什么特别是func4这么慢?!有人可以解释/推测这里发生了什么吗?

2 个答案:

答案 0 :(得分:8)

  1. 或许,您的基准测试主要是内存分配性能,而不仅仅是写入性能。您的操作系统可能会分配不在malloc中的内存页,但在第一次触摸时,在func*功能内。在分配了大量内存后,操作系统也可能会进行一些内存改组,因此在内存分配之后执行的任何基准测试都可能不可靠。
  2. 您的代码有aliasing问题:编译器无法保证您的数组指针在填充此数组的过程中不会发生变化,因此必须始终从内存加载arr值而不是使用寄存器。这可能会降低性能。避免别名的最简单方法是将arrlength复制到局部变量,并仅使用局部变量来填充数组。有许多众所周知的建议可以避免全局变量。别名是其中一个原因。
  3. 如果数组由64个字节对齐,则
  4. _mm_stream_ps效果更好。在您的代码中,不保证对齐(实际上,malloc将其对齐16个字节)。这种优化仅适用于短阵列。
  5. 完成_mm_mfence后,最好拨打_mm_stream_ps。这是正确性所必需的,而不是性能。

答案 1 :(得分:2)

不应该是这样的:

void func4() {
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f);
    for(int i = 0; i < length; i += 16) {
        _mm_stream_ps(&arr[i], buf);
        _mm_stream_ps(&arr[i+4], buf);
        _mm_stream_ps(&arr[i+8], buf);
        _mm_stream_ps(&arr[i+12], buf);
    }
}