当内存带宽受限时 SSE 和 AVX 的性能

标签 performance caching sse avx

在下面的代码中,我更改了“dataLen”并获得了不同的效率。

dataLen = 400 SSE 时间:758000 us AVX 时间:483000 us SSE > AVX

dataLen = 2400 SSE 时间:4212000 us AVX 时间:2636000 us SSE > AVX

dataLen = 2864 SSE 时间:6115000 us AVX 时间:6146000 us SSE ~= AVX

dataLen = 3200 SSE 时间:8049000 us AVX 时间:9297000 us SSE < AVX

dataLen = 4000 SSE 时间:10170000us AVX 时间:11690000us SSE < AVX

SSE 和 AVX 代码都可以简化为: buf3[i] += buf1[1]*buf2[i];

#include "testfun.h"
#include <iostream>
#include <chrono>
#include <malloc.h>
#include "immintrin.h"
using namespace std::chrono;

void testfun()
{
int dataLen = 4000; 
int N = 10000000;
float *buf1 = reinterpret_cast<float*>(_aligned_malloc(sizeof(float)*dataLen, 32));
float *buf2 = reinterpret_cast<float*>(_aligned_malloc(sizeof(float)*dataLen, 32));
float *buf3 = reinterpret_cast<float*>(_aligned_malloc(sizeof(float)*dataLen, 32));
for(int i=0; i<dataLen; i++)
{
    buf1[i] = 1;
    buf2[i] = 1;
    buf3[i] = 0;
}
//=========================SSE CODE=====================================
system_clock::time_point SSEStart = system_clock::now();
__m128 p1, p2, p3;

for(int j=0; j<N; j++)
for(int i=0; i<dataLen; i=i+4)
{
    p1 = _mm_load_ps(&buf1[i]);
    p2 = _mm_load_ps(&buf2[i]);
    p3 = _mm_load_ps(&buf3[i]);
    p3 = _mm_add_ps(_mm_mul_ps(p1, p2), p3);
    _mm_store_ps(&buf3[i], p3);
}

microseconds SSEtimeUsed = duration_cast<milliseconds>(system_clock::now() - SSEStart);
std::cout << "SSE time used: " << SSEtimeUsed.count() << " us, " <<std::endl;

//=========================AVX CODE=====================================
for(int i=0; i<dataLen; i++) buf3[i] = 0;

system_clock::time_point AVXstart = system_clock::now();
__m256  pp1, pp2, pp3; 

for(int j=0; j<N; j++)
for(int i=0; i<dataLen; i=i+8)
{       
    pp1 = _mm256_load_ps(&buf1[i]);
    pp2 = _mm256_load_ps(&buf2[i]);
    pp3 = _mm256_load_ps(&buf3[i]);
    pp3 = _mm256_add_ps(_mm256_mul_ps(pp1, pp2), pp3);
    _mm256_store_ps(&buf3[i], pp3);

}

microseconds AVXtimeUsed = duration_cast<milliseconds>(system_clock::now() - AVXstart);
std::cout << "AVX time used: " << AVXtimeUsed.count() << " us, " <<std::endl;

_aligned_free(buf1);
_aligned_free(buf2);
}

我的 cpu 是 Intel Xeon E3-1225 v2,它有一个 L1 缓存 32KB*4(4 核),运行这段代码时它只使用了 1 个核,所以使用的 L1 缓存是 32KB。

buf1 buf2 和buf3 足够小,可以定位在L1 缓存和L2 缓存中(L2 缓存1MB)。SSE 和AVX 都是带宽有限的,但是随着dataLen 的增加,为什么AVX 比SSE 需要更多的时间?

最佳答案

这是一个有趣的观察。我能够重现你的结果。我设法通过展开循环来大大提高您的 SSE 代码速度(请参阅下面的代码)。现在上证所dataLen=2864显然更快,对于较小的值,它几乎与 AVX 一样快。对于更大的值,它仍然更快。这是由于您的 SSE 代码中存在携带循环依赖性(即展开循环会增加指令级并行性 (ILP))。我没有尝试进一步展开。展开 AVX 代码没有帮助。

不过我对你的问题没有明确的答案。我的预感是它与 ILP 以及 Sandy Bridge 等 AVX 处理器只能同时加载两个 128 位字(SSE 宽度)而不是两个 256 位字的事实有关。因此,在 SSE 代码中,它可以同时进行一次 SSE 加法、一次 SSE 乘法、两次 SSE 加载和一次 SSE 存储。对于 AVX,它可以同时执行一个 AVX 加载(通过端口 2 和 3 上的两个 128 位加载)、一个 AVX 乘法、一个 AVX 加法和一个 128 位存储(AVX 宽度的一半)。换句话说,尽管使用 AVX,乘法和加法的工作量是 SSE 的两倍,但加载和存储仍然是 128 位宽。也许这会导致 AVX 与 SSE 相比更低的 ILP,有时代码由加载和存储主导?

有关端口和 ILP 的更多信息,请参阅此 Haswell, Sandy Bridge, Nehalem ports compared .

__m128 p1, p2, p3, p1_v2, p2_v2, p3_v2;
for(int j=0; j<N; j++)
    for(int i=0; i<dataLen; i+=8)
    {
        p1 = _mm_load_ps(&buf1[i]);
        p1_v2 = _mm_load_ps(&buf1[i+4]);
        p2 = _mm_load_ps(&buf2[i]);
        p2_v2 = _mm_load_ps(&buf2[i+4]);
        p3 = _mm_load_ps(&buf3[i]);
        p3_v2 = _mm_load_ps(&buf3[i+4]);
        p3 = _mm_add_ps(_mm_mul_ps(p1, p2), p3);
        p3_v2 = _mm_add_ps(_mm_mul_ps(p1_v2, p2_v2), p3_v2);
        _mm_store_ps(&buf3[i], p3);
        _mm_store_ps(&buf3[i+4], p3_v2);
    }

关于当内存带宽受限时 SSE 和 AVX 的性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18691322/

相关文章:

sse - 用SSE对数,还是改用FPU?

java - System.currentTimeMillis 执行时间

performance - Riak 性能 - 意想不到的结果

css - 如何防止浏览器缓存 CSS 文件?

linux - 主从 Memcache 复制

c++ - 编写可移植的 SSE/AVX 版本的 std::copysign

html - CSS 子选择器性能与类膨胀

PHP mysqli 加载缓慢。

c# - IDictionary 有 LRU 实现吗?

c - _mm_store_si128 上的 SSE 段错误