SSE 比常规函数慢得多
SSE much slower than regular function
我正在使用SSE制作Julia集可视化。这是我的代码类和运算符
class vec4 {
public:
inline vec4(void) {}
inline vec4(__m128 val) :v(val) {}
__m128 v;
inline void operator=(float *a) {v=_mm_load_ps(a);}
inline vec4(float *a) {(*this)=a;}
inline vec4(float a) {(*this)=a;}
inline void operator=(float a) {v=_mm_load1_ps(&a);}
};
inline vec4 operator+(const vec4 &a,const vec4 &b) { return _mm_add_ps(a.v,b.v); }
inline vec4 operator-(const vec4 &a,const vec4 &b) { return _mm_sub_ps(a.v,b.v); }
inline vec4 operator*(const vec4 &a,const vec4 &b) { return _mm_mul_ps(a.v,b.v); }
inline vec4 operator/(const vec4 &a,const vec4 &b) { return _mm_div_ps(a.v,b.v); }
inline vec4 operator++(const vec4 &a)
{
__declspec(align(16)) float b[4]={1.0f,1.0f,1.0f,1.0f};
vec4 B(b);
return _mm_add_ps(a.v,B.v);
}
函数本身:
vec4 TWO(2.0f);
vec4 FOUR(4.0f);
vec4 ZER(0.0f);
vec4 CR(cR);
vec4 CI(cI);
for (int i=0; i<320; i++) //H
{
float *pr = (float*) _aligned_malloc(4 * sizeof(float), 16); //dynamic
__declspec(align(16)) float pi=i*ratioY + startY;
for (int j=0; j<420; j+=4) //W
{
pr[0]=j*ratioX + startX;
for(int x=1;x<4;x++)
{
pr[x]=pr[x-1]+ratioX;
}
vec4 ZR(pr);
vec4 ZI(pi);
__declspec(align(16)) float color[4]={0.0f,0.0f,0.0f,0.0f};
vec4 COLOR(color);
vec4 COUNT(0.0f);
__m128 MASK=ZER.v;
int _count;
enum {max_count=100};
for (_count=0;_count<=max_count;_count++)
{
vec4 tZR=ZR*ZR-ZI*ZI+CR;
vec4 tZI=TWO*ZR*ZI+CI;
vec4 LEN=tZR*tZR+tZI*tZI;
__m128 MASKOLD=MASK;
MASK=_mm_cmplt_ps(LEN.v,FOUR.v);
ZR=_mm_or_ps(_mm_and_ps(MASK,tZR.v),_mm_andnot_ps(MASK,ZR.v));
ZI=_mm_or_ps(_mm_and_ps(MASK,tZI.v),_mm_andnot_ps(MASK,ZI.v));
__m128 CHECKNOTEQL=_mm_cmpneq_ps(MASK,MASKOLD);
COLOR=_mm_or_ps(_mm_and_ps(CHECKNOTEQL,COUNT.v),_mm_andnot_ps(CHECKNOTEQL,COLOR.v));
COUNT=COUNT++;
operations+=17;
if (_mm_movemask_ps((LEN-FOUR).v)==0) break;
}
_mm_store_ps(color,COLOR.v);
SSE 需要 553k 次操作(mull,add,if),完成任务需要 ~320ms另一方面,常规函数需要 1428K 操作,但只需要 ~90ms 即可计算?我使用了vs2010性能分析器,似乎所有数学运算符都运行缓慢。我做错了什么?
您遇到的问题是 SSE 内部函数执行的内存操作比非 SSE 版本多得多。使用你的向量类,我写了这个:
int main (int argc, char *argv [])
{
vec4 a (static_cast <float> (argc));
cout << "argc = " << argc << endl;
a=++a;
cout << "a = (" << a.v.m128_f32 [0] << ", " << a.v.m128_f32 [1] << ", " << a.v.m128_f32 [2] << ", " << a.v.m128_f32 [3] << ", " << ")" << endl;
}
在发布版本中产生了以下操作(我已对其进行编辑以仅显示 SSE):
fild dword ptr [ebp+8] // load argc into FPU
fstp dword ptr [esp+10h] // save argc as a float
movss xmm0,dword ptr [esp+10h] // load argc into SSE
shufps xmm0,xmm0,0 // copy argc to all values in SSE register
movaps xmmword ptr [esp+20h],xmm0 // save to stack frame
fld1 // load 1 into FPU
fst dword ptr [esp+20h]
fst dword ptr [esp+28h]
fst dword ptr [esp+30h]
fstp dword ptr [esp+38h] // create a (1,1,1,1) vector
movaps xmm0,xmmword ptr [esp+2Ch] // load above vector into SSE
addps xmm0,xmmword ptr [esp+1Ch] // add to vector a
movaps xmmword ptr [esp+38h],xmm0 // save back to a
注意:这些地址是相对于ESP的,并且有一些推送可以解释相同值的偏移量的奇怪变化。
现在,将代码与此版本进行比较:
int main (int argc, char *argv [])
{
float a[4];
for (int i = 0 ; i < 4 ; ++i)
{
a [i] = static_cast <float> (argc + i);
}
cout << "argc = " << argc << endl;
for (int i = 0 ; i < 4 ; ++i)
{
a [i] += 1.0f;
}
cout << "a = (" << a [0] << ", " << a [1] << ", " << a [2] << ", " << a [3] << ", " << ")" << endl;
}
编译器为上述内容创建了此代码(再次经过编辑并带有奇怪的偏移量)
fild dword ptr [argc] // converting argc to floating point values
fstp dword ptr [esp+8]
fild dword ptr [esp+4] // the argc+i is done in the integer unit
fstp dword ptr [esp+0Ch]
fild dword ptr [esp+8]
fstp dword ptr [esp+18h]
fild dword ptr [esp+10h]
fstp dword ptr [esp+24h] // array a now initialised
fld dword ptr [esp+8] // load a[0]
fld1 // load 1 into FPU
fadd st(1),st // increment a[0]
fxch st(1)
fstp dword ptr [esp+14h] // save a[0]
fld dword ptr [esp+1Ch] // load a[1]
fadd st,st(1) // increment a[1]
fstp dword ptr [esp+24h] // save a[1]
fld dword ptr [esp+28h] // load a[2]
fadd st,st(1) // increment a[2]
fstp dword ptr [esp+28h] // save a[2]
fadd dword ptr [esp+2Ch] // increment a[3]
fstp dword ptr [esp+2Ch] // save a[3]
在内存访问方面,增量需要:
SSE FPU
4xfloat write 1xfloat read
1xsse read 1xfloat write
1xsse read+add 1xfloat read
1xsse write 1xfloat write
1xfloat read
1xfloat write
1xfloat read
1xfloat write
total
8 float reads 4 float reads
8 float writes 4 float writes
这表明 SSE 使用的内存带宽是 FPU 版本的两倍,内存带宽是一个主要瓶颈。
如果你想认真地最大化SSE,那么你需要在单个SSE汇编函数中写入整个aglorithm,以便尽可能消除内存读/写。使用内联函数并不是优化的理想解决方案。
这是另一个例子(曼德布洛特集合),它几乎与 Julia 集合算法的实现方式相同http://pastebin.com/J90paPVC 基于 http://www.iquilezles.org/www/articles/sse/sse.htm。同样的故事 FPU>SSE 我什至跳过一些不相关的操作。任何想法如何做对吗?
相关文章:
- 是否可以使用单个定义定义函数的常量和常规版本?(使用模板,自动,decltype等)
- G++ 编译器是否在未使用返回值的情况下将 constexpr 函数视为常规函数?
- C++常规指针函数或模板
- 在C++中,为什么使用静态类函数而不是常规函数?
- 模板与常规函数歧义 - UB?
- 如何使用模板生成常规参数列表并将其传递给运行时函数?
- 为什么非放置"新建"和"删除"内置于语言中,而不仅仅是常规函数?
- 是否有任何常规方法可以通知 STL 移动和复制构造函数?
- 使常规函数使用模板包装成员函数
- 如何初始化堆,以便静态构造函数可以在常规 MFC dll 中使用堆?
- Arduino "delay"函数的常规c ++等价物是什么?
- 为什么C++列表初始化也考虑常规构造函数?
- 编译器如何以不同于常规函数的方式处理 lambda?
- 函数模板和常规重载
- C++中的错误处理,构造函数与常规方法
- 函数指针和常规调用的区别
- 使用 boost::this_thread::sleep_for() 和常规睡眠() 函数有什么区别
- 为什么我的函数不跳过尝试解析为不兼容的模板函数,而默认解析为常规函数?
- 指向多值函数的常规指针
- 为什么使用虚函数而不是常规函数