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

Posted

技术标签:

【中文标题】为啥 _mm_stream_ps 会产生 L1/LL 缓存未命中?【英文标题】:Why does _mm_stream_ps produce L1/LL cache misses?为什么 _mm_stream_ps 会产生 L1/LL 缓存未命中? 【发布时间】:2012-01-30 17:41:37 【问题描述】:

我正在尝试优化计算密集型算法,但有点卡在一些缓存问题上。我有一个巨大的缓冲区,它偶尔会随机写入,并且在应用程序结束时只读取一次。显然,写入缓冲区会产生大量缓存未命中,并且会污染之后再次需要进行计算的缓存。我尝试使用非时间移动内在函数,但缓存未命中(由 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 这么慢?!有人可以解释/推测这里发生了什么吗?

【问题讨论】:

如果您在启用优化的情况下进行编译,您需要查看程序集才能真正了解发生了什么。 我正在查看程序集,顺便说一句,每个优化级别都越来越难以阅读,但它并没有告诉我为什么忽略非时间提示。至少我猜它被忽略了,因为 valgrind 仍然会报告我期望没有的缓存未命中。无论如何,我知道这个问题是相当不具体的,所以我非常感谢任何关于这里可能发生的事情的意见。 【参考方案1】:
    可能您的基准测试主要测量内存分配性能,而不仅仅是写入性能。您的操作系统可能不在malloc 中分配内存页面,而是在第一次触摸时在func* 函数中分配内存页。操作系统也可能在分配大量内存后进行一些内存洗牌,因此在分配内存后执行的任何基准测试都可能不可靠。 你的代码有aliasing问题:编译器不能保证你的数组指针在填充这个数组的过程中不会改变,所以它必须总是从内存中加载arr值而不是使用寄存器。这可能会降低一些性能。避免别名的最简单方法是将arrlength 复制到局部变量并仅使用局部变量来填充数组。有许多众所周知的建议可以避免使用全局变量。别名是原因之一。 如果数组按 64 字节对齐,_mm_stream_ps 效果会更好。在您的代码中,不保证对齐(实际上,malloc 将其对齐 16 个字节)。这种优化只对短数组很明显。 在完成_mm_stream_ps 之后,最好致电_mm_mfence。这是为了正确性,而不是为了性能。

【讨论】:

非常感谢 Evgeny! 1. 就是这样。我没有意识到这一点。当我将代码更改为只分配一次内存时,它会将运行时极大地改变为我最初所期望的。 func3+4 比 func1+2 快大约 2-3 倍。 2. 你能详细说明一下吗?我认为别名只是关于虚拟内存物理内存的问题。我看不出这是哪里的问题。 3. 好的,所以我必须使用 valloc() 或其他一些 libc 特定函数?对运行时没有任何影响。缓存行对齐应该有助于 CPU 进行写入组合,对吗? 4. 好的。 我添加了一些关于别名的解释以及***链接。缓存行对齐允许对数组的前 64 个字节正确使用写入组合。对于对齐,您可以使用几个与平台相关的函数,我现在不记得所有这些函数。或者你可以使用(p+63)&amp;~63 技巧。或者,如果您的数组总是大于兆字节,则忽略对齐。 关于别名问题,您应该尝试将 'arr' 和 'length' 作为参数传递给函数,而不是将它们作为全局变量。这可能提高编译器的优化机会。【参考方案2】:

func4 不应该是这样的:

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

【讨论】:

你是对的。谢谢 :-) 这使 func4 的结果与 func3 大致相同。

以上是关于为啥 _mm_stream_ps 会产生 L1/LL 缓存未命中?的主要内容,如果未能解决你的问题,请参考以下文章

第十六天:内置函数的继续:

为啥在 gcc 和 clang 上通过优化将大 double 转换为 uint16_t 会给出不同的答案

为啥 false++ 在 Firefox 中会产生 SyntaxError 而在 Chrome 中会产生 ReferenceError?

为啥引发异常会产生副作用?

为啥下面的代码会产生死锁

为啥 FirebaseVisionImage.fromMediaImage() 会产生 OutOfMemoryError