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

4
在下面的代码中,我改变了“dataLen”,导致效率不同。
dataLen = 400 SSE时间:758000微秒 AVX时间:483000微秒 SSE > AVX
dataLen = 2400 SSE时间:4212000微秒 AVX时间:2636000微秒 SSE > AVX
dataLen = 2864 SSE时间:6115000微秒 AVX时间:6146000微秒 SSE ~= AVX
dataLen = 3200 SSE时间:8049000微秒 AVX时间:9297000微秒 SSE < AVX
dataLen = 4000 SSE时间:10170000微秒 AVX时间:11690000微秒 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是英特尔Xeon E3-1225 v2,它有一个L1缓存32KB*4(4个核心)。当运行此代码时,只使用了1个核心,因此使用的L1缓存为32KB。

buf1、buf2和buf3足够小,可以放在L1缓存和L2缓存(L2缓存1MB)中。SSE和AVX都受带宽限制,但随着数据长度的增加,为什么AVX需要比SSE更长的时间?

2个回答

3
这是一个有趣的观察。我能够重现你的结果。通过展开循环(参见下面的代码),我成功地提高了SSE代码的速度。现在对于SSE,dataLen=2864 明显更快,对于较小的值,它几乎和AVX一样快。对于更大的值,它仍然更快。这是由于你的SSE代码中存在着循环依赖关系(即展开循环增加了指令级并行性(ILP))。我没有尝试进一步展开循环。展开AVX代码没有帮助。
不过,我没有一个明确的答案来回答你的问题。我的直觉是,这与ILP有关,以及AVX处理器(如Sandy Bridge)只能同时加载两个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 端口比较
__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);
    }

该程序的性能受到内存带宽的限制,而且在这种情况下,AVX 比 SSE 更慢。AVX 的 256 位加载速度比 SSE 的 128 位加载速度更慢。也许我们可以称之为 CPU 的一个 bug! - myej

1

我认为这是Sandy Bridge架构缓存系统的缺陷。我可以在Ivy Bridge CPU上重现相同的结果,但在Haswell CPU上不能,但Haswell在访问L3时也有同样的问题。我认为这是AVX的一个大缺陷。英特尔应该在下一步或下一代架构中修复这个问题。

N = 1000000
datalen = 2000
SSE time used: 280000 us,
AVX time used: 156000 us,

N = 1000000
datalen = 4000 <- it's still fast on Haswell using L2
SSE time used: 811000 us,
AVX time used: 702000 us,

N = 1000000
datalen = 6000
SSE time used: 1216000 us,
AVX time used: 1076000 us,

N = 1000000
datalen = 8000
SSE time used: 1622000 us,
AVX time used: 1466000 us,

N = 100000  <- reduced
datalen = 20000 <- fit in L2 : 256K / 23 = 21845.3
SSE time used: 405000 us,
AVX time used: 374000 us,

N = 100000  
datalen = 40000 <- need L3
SSE time used: 1185000 us,
AVX time used: 1263000 us,

N = 100000  
datalen = 80000
SSE time used: 2340000 us,
AVX time used: 2527000 us,

尝试展开SSE循环一次。依赖关系会变得更大。此外,dataLen = 4000适合L1缓存,因此L3不应成为问题。 - Z boson
一个计算使用3个浮点数据,因此32768字节的L1可以容纳2730个组件。 - zupet
你说得对。所以看起来,不一致的问题发生在从L1到L2的过程中。 - Z boson
你的内存是64字节对齐的,对吧?很有趣。 - user1610743
@Zboson: dataLen = 4000 = 48 KB的数据,比32 KB的L1数据缓存更大。 - netvope

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接