在CPU多核的今天,考虑如何利用多核的问题已经摆在了每个程序员的面前。对一个稍微有点经验的程序员而言,OMP无疑是最快的捷径,并且收效非常高,基本能提升1.8倍的效率(双核CPU)。
耐着性子把OMP的介绍文章看完了,在3D应用中实践起来,收效很明显,骨骼计算,皮肤计算,顶点坐标/纹理坐标/法线插值上用得得心应手,一时兴起,把关键部分的计算用SSE指令重新写过后,总体得到了大约4倍的效率提升。
在这个过程中,发现了一些问题:
一、使用SSE指令写了一个处理一个数据的函数,同时也写了一个处理多个数据的函数。从通常的单线程思维来看,显然减少函数调用开销可以提升效率。因此,处理一段数据采用了以下写法:
- //num_threads :启动的OMP线程个数
- int num_threads = omp_get_max_threads();
- //spt : 每个线程分配到的数据
- int spt = uCount / num_threads;
- if(spt > 0)
- {
- //每个线程处理一段连续的数据
- #pragma omp parallel for
- for(int i=0; i<num_threads; ++i)
- {
- int nStart = i * spt;
- SSE_ProcessDatas(buff+nStart,spt);
- }
- }
- for(int i=num_threads * spt; i<uCount; ++i)
- SSE_ProcessData(buff[i]);
测试结果差点没让我下巴脱臼,结果表明,这样写法的运行结果还不如单线程快。单线程代码如下:
- for(int i=0; i<uCount; i+=4)
- {
- SSE_ProcessData(buff[i]);
- }
而简单的在单线程代码前面加上OMP指令,居然有了0.9倍左右(AMD 3800+,双核CPU)的效率提升:
- #pragma omp parallel for
- for(int i=0; i<uCount; i+=4)
- {
- SSE_ProcessData(buff[i]);
- }
非常的不可思议。
本人不是特别追根问底的人,一切以实践出发,同时记下一条规则:OMP优化规则很好了,一些貌似聪明的小技巧不见得好使,老老实实用OMP吧。在这个思路指引下,接下来的其他优化都很顺畅,令人非常兴奋,又开始了给老婆讲解OMP,MPI如何跟造鞋挂上关系的。加上白天喝了几包咖啡,开始半夜半夜失眠了。
俗话说,常见江湖漂,哪能不挨刀,常在河边走,哪有不湿鞋?在做射线与模型逐三角面相交的时候,OMP又给我上了一课.按照之前的经验,这回的代码写得很保守很规矩:
单线程代码:
- FLOAT fMaxDistance = FLT_MAX;
- int nTriangle = -1;
- for(int i=0;i<nCount; i+=3)
- {
- FLOAT temp; //射线ray跟三角面(pVertex[pIndex[i+0]],pVertex[pIndex[i+1]],pVertex[pIndex[i+2]])相交后的距离
- if(FALSE == IntersectTriangleLine(pVertex[pIndex[i+0]],
- pVertex[pIndex[i+1]],
- pVertex[pIndex[i+2]],
- ray,NULL,&temp))
- continue;
- if(temp < 0)
- continue;
- if(temp < fMaxDistance)
- {
- fMaxDistance = temp;
- nTriangle = i;
- }
- }
多线程代码,典型的OMP求最小值的翻版:
- int nTT[2] = {-1,-1};
- FLOAT f[2] = {FLT_MAX,FLT_MAX};
- #pragma omp parallel for num_threads(2)
- for(int i=0;i<nCount; i+=3)
- {
- FLOAT temp; //射线ray跟三角面(pVertex[pIndex[i+0]],pVertex[pIndex[i+1]],pVertex[pIndex[i+2]])
- 相交后的距离
- if(FALSE == IntersectTriangleLine(pVertex[pIndex[i+0]],
- pVertex[pIndex[i+1]],
- pVertex[pIndex[i+2]],
- ray,NULL,&temp))
- continue;
- if(temp < 0)
- continue;
- int threadid = omp_get_thread_num();
- if(temp < f[threadid])
- {
- f[threadid] = temp;
- nTT[threadid] = i;
- }
- }
- if(f[0] < f[1])
- {
- fMaxDistance = f[0];
- nTriangle = nTT[0];
- }
- else
- {
- fMaxDistance = f[1];
- nTriangle = nTT[1];
- }
很高兴的进行测试(本人单元测试习惯不错),结果再次差点没让我下巴脱臼----效率有了大约1/8的下降!
而后反复折腾,修改,是不是OMP做了不应该的同步啦,变量是否合理的shared,private,或firstprivate了,查资料,上网,读MSDN.....十七般武艺都用尽了,基本处于放弃的边沿的,终于想到使用最后的一个武器: ASM.由于是测试效率,之前的所有编译和测试都是在Release版下进行的,汇编里加入了OMP的代码后,更难于看懂,且很多变量都不知道值,这次更改为Debug进行效率测试.很快,结果水落石出:
第一种情况
- #pragma omp parallel for
- for(int i=0; i<uCount; i+=4)
- {
- SSE_ProcessData(buff[i]);
- }
OMP优化结果是
- //thread 0:
- for(int i=0; i<uCount; i+=8)
- {
- SSE_ProcessData(buff[i]);
- }
- //thread 1:
- for(int i=4; i<uCount; i+=8)
- {
- SSE_ProcessData(buff[i]);
- }
而第二种情况下,OMP优化结果是:
- //thread 0:
- for(int i=0; i<uCount/2; i+=3)
- {
- ...
- }
- //thread 1:
- for(int i=uCount/2; i<uCount; i+=3)
- {
- ...
- }
显然,差别在循环的方式上.至于为什么OMP的编译结果有这种差别,其规则是什么,现在是不得而知----显然,跟上下文或使用指针的方式有关.要是哪位仁兄知道这些细节,烦请告知.
报着将信将疑的态度,将第二种情况的代码修改如下:
- int nTT[2] = {-1,-1};
- FLOAT f[2] = {FLT_MAX,FLT_MAX};
- #pragma omp parallel sections num_threads(2)
- {
- #pragma omp section
- {
- int threadid = omp_get_thread_num();
- for(int i=0;i<nCount; i+=6)
- {
- FLOAT temp;
- if(FALSE == IntersectTriangleLine(pVertex[pIndex[i+0]],
- pVertex[pIndex[i+1]],
- pVertex[pIndex[i+2]],
- ray,NULL,&temp))
- continue;
- if(temp < 0)
- continue;
- if(temp < f[threadid])
- {
- f[threadid] = temp;
- nTT[threadid] = i;
- }
- }
- }
- #pragma omp section
- {
- int threadid = omp_get_thread_num();
- for(int i=3;i<nCount; i+=6)
- {
- FLOAT temp;
- if(FALSE == IntersectTriangleLine(pVertex[pIndex[i+0]],
- pVertex[pIndex[i+1]],
- pVertex[pIndex[i+2]],
- ray,NULL,&temp))
- continue;
- if(temp < 0)
- continue;
- if(temp < f[threadid])
- {
- f[threadid] = temp;
- nTT[threadid] = i;
- }
- }
- }
- }
- if(f[0] < f[1])
- {
- fMaxDistance = f[0];
- nTriangle = nTT[0];
- }
- else
- {
- fMaxDistance = f[1];
- nTriangle = nTT[1];
- }
(补充:上述代码不是最终代码,最终代码是根据当前CPU核数目分组循环计算的.代码不在手边,且不复杂,就不呈上了)
也就是说,两个线程,线程0处理奇数的三角面,线程1处理偶数个数的三角面。
测试结果表明,效率终于提升了0.8倍多。
“嘿嘿嘿,哥们,你好歹已经过了而立之年了,怎么写文章还文不对题呢?”——读者咆哮道,并且四处寻觅着鹅卵石或臭鸡蛋。
“wusa...wusa...安静,安静,我一直都挂惦着标题呢!”----我举起了早就准备好的雨伞。
Why?Why?Why?
有一点在之前的文中我没有提到:为了照顾GPU缓存优化,我的所有模型数据都是针对Geforce2缓存16个顶点进行过缓存优化的,也就是说,在CPU处理这些顶点的时候,同样能享受到这样的好处。在两个线程中,同时处理两个三角面,如果他们处理速度相同,则他们基本都在处理临近的三角面,而这些临近的三角面,无论是索引,还是顶点,都处于CPU的缓存中的概率会相当大,缓存实效带来的效率下降比较小。而将数据分成两段处理,则CPU需要同时缓存四段内存(两处索引,两处顶点),缓存不够大的情况下(AMD的U本来缓存就小),缓存实效带来的效率下降就直接抵消了多线程的效率提升,甚至变得更慢了!
为了验证这点,我在加载模型的时候,将三角面索引随机进行了替换,以打乱CPU缓存顶点数据,再次测试表明,效率有了可观的下降。
我没有文不对题!
本文探讨了在多核CPU环境下使用OpenMP和SSE指令进行并行计算的实践经验和教训,特别是在3D应用中的骨骼计算、皮肤计算等方面的应用。通过对不同并行策略的对比测试,揭示了数据局部性和缓存效应对于并行计算性能的影响。
723

被折叠的 条评论
为什么被折叠?



