为什么使用SSE指令没有提升性能-附代码

TimothyField 2011-03-06 06:33:37
最近在做一个程序,中间有一段大量的计算,最核心的一段主要就是乘加操作,原来的C++代码如下:
struct TStatxSimpleRecord
{
float Mean;
float Stdevp;
};

for(i=minx; i<=maxx;i++)
{
float disx2 = square(i*fstepx - x); //squre是自己写的平方函数

for(j=miny; j<maxy; j++)
{
float disy2 = square(j*fstepy - y);
//下面的Exptor.Exp是自己做的快速指数函数,其实就是查表,跟本帖关系不大
float weight = Exptor.Exp((disx2+disy2)*lamda2);
TStatxSimpleRecord * pcd = pxsr[i][j];
//下面代码本来可以用一个循环的,为了提升性能,已经做了循环展开
pcd[0].Mean += weight*gain[0]; //gain和下面的g2都是事先计算得到的一个4个浮点数的数组
pcd[1].Mean += weight*gain[1]; //其中g2[i] = gain[i]*gain[i]
pcd[2].Mean += weight*gain[2];
pcd[3].Mean += weight*gain[3];
pcd[0].Stdevp += weight*g2[0];
pcd[1].Stdevp += weight*g2[1];
pcd[2].Stdevp += weight*g2[2];
pcd[3].Stdevp += weight*g2[3];
}
}

因为上面这两层循环是在一个需要执行200万次的大循环里面,所以对性能影响非常大,现在程序执行一次大约需要12分钟。考虑到上面的数据刚好都是4个浮点数,所以希望通过SSE指令加速一下。下面是用SSE改写的代码:
__asm {
mov eax, t1
movups xmm0, [eax] //复制gain到xmm0
movaps xmm1, xmm0 //复制xmm0到xmm1
mulps xmm1, xmm1 //xmm1平方,得到g2
}

for(i=minx; i<=maxx;i++)
{
float disx2 = square(i*fstepx - x);

for(j=miny; j<maxy; j++)
{
float disy2 = square(j*fstepy - y);
float weight = Exptor.Exp((disx2+disy2)*lamda2);
pdencity[i][j] += weight;
//下面的TVCT是一个临时的结构,struct TVCT{float x; float y; float z; float w;}
//使用这个结构是因为我不知道其他把下面数组内容拷贝到SSE寄存器的方法,
//有人知道的话也请告诉一声,一样有分
TVCT & t2 = *(TVCT *)(pmean[i][j]);
TVCT & t3 = *(TVCT *)(pstd[i][j]);
__asm
{
movss xmm2, weight
shufps xmm2, xmm2, 0x00 //expand weight to 4

mov eax, t2
movaps xmm3, [eax] //mean
movaps xmm4, xmm0 //load gain
mulps xmm4, xmm2 //weight * gain
addps xmm3, xmm4 //mean += weight*gain
movaps [eax], xmm3

mov eax, t3
movaps xmm5, [eax] //std
movaps xmm6, xmm1 //load gain*gain
mulps xmm6, xmm2 //weight*gain*gain
addps xmm5, xmm6
movaps [eax], xmm5
}
}
}
这段代码运行的结果是正确的,与原来的代码得到的结果完全一样(数据16字节对齐的问题我已经注意到了)。但性能出人意料,一点都没有提升(我原来预计有3倍左右的提升),当然也没有降低。

我检查了汇编结果,确实已经使用了SSE指令,寄存器xmm的内容也是正常的。
数据总线的瓶颈我也考虑过,SSE执行的顺序大致安排使得计算跟数据操作分开。
为避免窗口程序的影响,我另外做了一个控制台程序,性能表现没有区别。

现在我想不出还有什7么理由导致性能无法提升,请高手指点。

我使用的CPU:Intel Core Duo T5750,按手册已经CPUID的测试,是支持SSE指令的。
编译器:C++ Builder 2009
OS:Windows Vista Home Basic
...全文
889 15 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
15 条回复
切换为时间正序
请发表友善的回复…
发表回复
dianyancao 2011-03-09
  • 打赏
  • 举报
回复
int a = 1;
int b = 14;
int c;
c = a / b; //也就是说这样会特别慢吗?

呵呵,我是新手。
polytechnic 2011-03-09
  • 打赏
  • 举报
回复
恭喜楼主,问题解决啊
TimothyField 2011-03-08
  • 打赏
  • 举报
回复
这个问题昨天晚上已经基本解决,因为我已经连续发了3个帖子,系统不让我继续发,所以没有及时更新。

首先要感谢polytechnic的提醒,我又仔细检查了各个部分单独花的时间,因为没有合适的工具,我是通过简单注释掉部分代码看执行时间的变化来查找疑点的。前面提到注释掉SSE代码的时候我是把相关的代码也注释掉了,现在再降低注释的粒度。

首先注意到其实性能瓶颈确实不在SSE代码部分,而是FastExp函数。这确实有点出乎意料,因为这个函数只是简单的一个查表:
inline float TFastExp::Exp(float x)
{
int n = (int)100*x;
return data[n];
}
由于知道x的范围,所以连参数检查都没有,这样的一个函数怎么会成为性能瓶颈呢?

我刚开始是怀疑由于n的取值变化比较大,所以data[n]的访问导致大量的cache missing,所以专门写了一段类似的程序模拟测试,数组的索引用n*31%size模拟随机访问(random函数太慢了),结果并没有发现类似的现象。

于是唯一的一个可能原因就是浮点数到整数的转换了。C编译器产生的浮点到整数的转换比较慢我是知道的,但到底多慢就没有概念了,好在验证起来比较简单,我把n设置为一个固定的整数,执行时间一下子就缩短了。

知道原因之后就比较容易解决了,现在已经把这个函数改写为:
float TFastExp::Exp(float x)
{
int n;
float y = 100*x;
_asm fld y
_asm fistp n
return data[n];
}

用两条汇编指令,6个时钟周期搞定。(因为inline函数中不能使用嵌入式汇编,所以这个函数不再加上inline)

这个地方修改之后,程序执行时间一下降低到106秒。平均单个循环只需要150个CPU TICK左右,比较原来需要570个CPU TICK,可以猜测一个浮点数到整数的转换在C++ Builder的缺省实现中需要约400个时钟周期!!!这个猜测比较吓人,但确实是现在得到的数据暗示的结论。

再重新比较一下不使用SSE指令的C++版本算法,实测执行时间是248秒,也就是说使用SSE指令进一步循环展开后,执行时间降低到不使用SSE版本的约1/2.5。这跟原来期望差不多了。
polytechnic 2011-03-07
  • 打赏
  • 举报
回复
我仔细看了下代码
建议把代码顺序调整一下试试呢,
看下面的行不行
__asm
{
// x86的CPU指令跟SSE的指令分开
mov eax, t2
mov edx, t3

movss xmm2, weight
shufps xmm2, xmm2, 0x00 //expand weight to 4

movaps xmm3, [eax] //mean
movaps xmm5, [edx] //std

movaps xmm4, xmm0 //load gain
mulps xmm4, xmm2 //weight * gain
addps xmm3, xmm4 //mean += weight*gain

movaps xmm6, xmm1 //load gain*gain
mulps xmm6, xmm2 //weight*gain*gain
addps xmm5, xmm6

movaps [eax], xmm3
movaps [edx], xmm5
}

x86的指令最好不要夹杂在SSE指令里面,一方面增强代码可读性,另一方面提高CPU解码效率
还有就是,数据传送指令都放在SSE指令的开头和结尾。我不知道这样能不能提高效率。我是看inter的手册上都是采用这样的代码,所以试试看也无妨。
dianyancao 2011-03-07
  • 打赏
  • 举报
回复
帖子要被推荐了,呵呵
polytechnic 2011-03-07
  • 打赏
  • 举报
回复
照你的描述看来,我觉得很有可能是cache的问题了
一个CPU指令最多只有十几个周期吧,但如果是cache miss跑到内存中拿数据,就要几百个周期了

我在想,你的t2和t3是不是位于内存的不同区域,以致不能同时容纳在L1 cache里面,导致每次循环的时候,不停地从t2和t3所在的内存读数据
能不能把t2和t3放到一起呢
比如说原来是这样的结构:
TVCT t2[M][N];
TVCT t3[M][N];
能不能改成这样
typedef struct {
TVCT t2;
TVCT t3;
}TVCT2;
TVCT2 t4[M][N];

可能需要修改很多代码
先备份一下代码,如果无效的话,再改回来
polytechnic 2011-03-07
  • 打赏
  • 举报
回复
我觉得有下面几种可能性
1.就像你说的,原来的代码性能已经足够高了
2.数据传送没有安排好,导致在IO浪费大量时间。这个我也不知道怎么去验证或者排除。
3.性能瓶颈并不在这段代码上
我不知道你做过profile没有,测一下关键函数用的总时间,和关键代码用的总时间(时钟周期),看看是不是差不多
如果差很多,那说明时间都用在别的地方了,比如分配变量,push、pop一下都要好多个时钟周期的。
还有就是占总执行时间的多少。如果只占20%,那优不优化都差不多了
4.有没有别的影响因素,比如程序用了多线程,线程切换需要很多时间
TimothyField 2011-03-07
  • 打赏
  • 举报
回复
to #4楼

你的建议我试了一下,可惜没有什么变化。

我又猜测是不是原来的循环展开已经重复利用流水线,性能已经足够高了,而使用SSE之后的MULPS、ADDPS、MOVAPS [eax] xmm3之类的指令本身又比较长,而且有数据关联,不好并行。于是把for(j=miny; j<maxy; j++)再做一次循环展开,改为for(j=miny; j<maxy-3; j+=4)...

这样循环里面就有8次SSE乘法、8次加法以及一堆内存操作,这样按说应该可以并发起来提高性能吧,但实测结果仍然没有丝毫变化。

TimothyField 2011-03-07
  • 打赏
  • 举报
回复
根据上面说的数据估算了一下,每次循环大约耗时570个CPU TICK。不过8个乘加操作而已,实在有点过了。
TimothyField 2011-03-07
  • 打赏
  • 举报
回复
补充一下,如果单独注释掉SSE代码部分,整个程序执行时间为36秒(完整执行为420秒)。前面说的2秒是整个函数注释掉之后的结果,但这个结果没有什么意义,所以补充一下。

所以比较明显都就是这段代码占了超过90%的执行时间。
TimothyField 2011-03-07
  • 打赏
  • 举报
回复
1、我原帖中的C代码循环展开后可能与原来的SSE代码性能上差不多,但我又对SSE的那段代码继续做循环展开,但性能也没有继续提升;

2、数据传送也是我怀疑的地方,但这方面我没有太多经验,不知道该如何调整代码。我试过改变代码顺序,但对性能没有影响——另外,代码的实际执行顺序应该跟书写顺序没有必然关系吧,只要没有数据依赖,据说CPU会自己安排执行顺序?
数据传送的另一个疑点是cache,pxsr所指的这块内存位置可能会存在cache污染,这个我会再看看。

3、如果把这段代码注释掉,整个程序不到2秒钟就执行完了,所以性能瓶颈肯定在这。我手头没有profile的工具——C++ Builder似乎没有,而我不知道有什么工具可以在C++ Builder中使用,如果你能推荐一个,请说一下。

4、为了避免多线程的影响,我另外写了一个单线程的控制台程序,结果是一样的。实际上这段代码只在一个线程中执行,跟其他线程之间没有临界资源,同时我的机器是双核的,所以基本上不存在多线程对性能的影响问题。

我现在比较想知道如何才能看到这些指令执行的时序,到底有没有并发起来。通过查阅SSE指令指令,发现这些指令在不同的端口操作,例如MULPS在p0端口,执行需要4个周期,但只有2个周期就可以执行下一条,Addps在p1端口执行,需要3个周期,2周期后可以继续执行。这样的话,如果有多个乘法、加法,只要没有数据依赖,按说是可以继续提升性能的才对。

另外,我估算了整个程序的计算量,大约有13G次乘加,这个计算量其实并不大,应该不需要太多时间,所以优化空间肯定是有的。
dianyancao 2011-03-07
  • 打赏
  • 举报
回复
楼上说话,好好听,好靓丽啊
TimothyField 2011-03-06
  • 打赏
  • 举报
回复
回1楼
1、我是想知道SSE那段代码性能怎么没有提升。
2、原来的C++代码有改进空间,我昨晚改了一下,取消了squre函数,减少了exp中的乘法,但这些部分占的比例很小,提升不大。
3、对我感兴趣的那一段代码,我看过产生的汇编,跟我写的是一样的,所以应该无所谓编译选项了。
polytechnic 2011-03-06
  • 打赏
  • 举报
回复
既然用SSE也没有性能提升,那说明瓶颈不在CPU指令的执行时间上,应该是在IO部分了
建议试试这些:
1. 把for循环里面的变量都用register声明
2. square和Exptor.Exp,改成inline,或者用寄存器传递参数
3. 我没用过C++Builter的编译器,不知道编译器有没有选项把代码针对速度优化
dianyancao 2011-03-06
  • 打赏
  • 举报
回复
可以用定点数吗?(0.0001)*10000

2,408

社区成员

发帖
与我相关
我的任务
社区描述
高性能计算
社区管理员
  • 高性能计算社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧