缓存与效率

本文探讨了在多核CPU环境下使用OpenMP和SSE指令进行并行计算的实践经验和教训,特别是在3D应用中的骨骼计算、皮肤计算等方面的应用。通过对不同并行策略的对比测试,揭示了数据局部性和缓存效应对于并行计算性能的影响。

        在CPU多核的今天,考虑如何利用多核的问题已经摆在了每个程序员的面前。对一个稍微有点经验的程序员而言,OMP无疑是最快的捷径,并且收效非常高,基本能提升1.8倍的效率(双核CPU)。

        耐着性子把OMP的介绍文章看完了,在3D应用中实践起来,收效很明显,骨骼计算,皮肤计算,顶点坐标/纹理坐标/法线插值上用得得心应手,一时兴起,把关键部分的计算用SSE指令重新写过后,总体得到了大约4倍的效率提升。

        在这个过程中,发现了一些问题:

        一、使用SSE指令写了一个处理一个数据的函数,同时也写了一个处理多个数据的函数。从通常的单线程思维来看,显然减少函数调用开销可以提升效率。因此,处理一段数据采用了以下写法:

  1. //num_threads :启动的OMP线程个数
  2. int num_threads = omp_get_max_threads();
  3. //spt : 每个线程分配到的数据
  4. int spt = uCount / num_threads;
  5. if(spt > 0)
  6. {
  7. //每个线程处理一段连续的数据
  8. #pragma omp parallel for
  9.     for(int i=0; i<num_threads; ++i)
  10.     {
  11.         int nStart = i * spt;
  12.         SSE_ProcessDatas(buff+nStart,spt);
  13.     }
  14. }
  15. for(int i=num_threads * spt; i<uCount; ++i)
  16.     SSE_ProcessData(buff[i]);

        测试结果差点没让我下巴脱臼,结果表明,这样写法的运行结果还不如单线程快。单线程代码如下:

  1. for(int i=0; i<uCount; i+=4)
  2. {
  3.     SSE_ProcessData(buff[i]);
  4. }

        而简单的在单线程代码前面加上OMP指令,居然有了0.9倍左右(AMD 3800+,双核CPU)的效率提升:

  1. #pragma omp parallel for
  2. for(int i=0; i<uCount; i+=4)
  3. {
  4.     SSE_ProcessData(buff[i]);
  5. }

        非常的不可思议。

        本人不是特别追根问底的人,一切以实践出发,同时记下一条规则:OMP优化规则很好了,一些貌似聪明的小技巧不见得好使,老老实实用OMP吧。在这个思路指引下,接下来的其他优化都很顺畅,令人非常兴奋,又开始了给老婆讲解OMP,MPI如何跟造鞋挂上关系的。加上白天喝了几包咖啡,开始半夜半夜失眠了。

        俗话说,常见江湖漂,哪能不挨刀,常在河边走,哪有不湿鞋?在做射线与模型逐三角面相交的时候,OMP又给我上了一课.按照之前的经验,这回的代码写得很保守很规矩:

        单线程代码: 

  1. FLOAT fMaxDistance = FLT_MAX;
  2. int nTriangle = -1;
  3. for(int i=0;i<nCount; i+=3)
  4. {
  5.     FLOAT temp; //射线ray跟三角面(pVertex[pIndex[i+0]],pVertex[pIndex[i+1]],pVertex[pIndex[i+2]])相交后的距离
  6.     if(FALSE == IntersectTriangleLine(pVertex[pIndex[i+0]],
  7.             pVertex[pIndex[i+1]],
  8.             pVertex[pIndex[i+2]],
  9.             ray,NULL,&temp))
  10.         continue;
  11.     if(temp < 0)
  12.         continue;
  13.     if(temp < fMaxDistance)
  14.     {
  15.         fMaxDistance = temp;
  16.         nTriangle = i;
  17.     }
  18. }

        多线程代码,典型的OMP求最小值的翻版: 

  1. int nTT[2] = {-1,-1};
  2. FLOAT f[2] = {FLT_MAX,FLT_MAX};
  3. #pragma omp parallel for num_threads(2)
  4. for(int i=0;i<nCount; i+=3)
  5. {
  6.     FLOAT temp; //射线ray跟三角面(pVertex[pIndex[i+0]],pVertex[pIndex[i+1]],pVertex[pIndex[i+2]])
  7. 相交后的距离
  8.     if(FALSE == IntersectTriangleLine(pVertex[pIndex[i+0]],
  9.             pVertex[pIndex[i+1]],
  10.             pVertex[pIndex[i+2]],
  11.             ray,NULL,&temp))
  12.         continue;
  13.     if(temp < 0)
  14.         continue;
  15.     int threadid = omp_get_thread_num();
  16.     if(temp < f[threadid])
  17.     {
  18.         f[threadid] = temp;
  19.         nTT[threadid] = i;
  20.     }
  21. }
  22. if(f[0] < f[1])
  23. {
  24.     fMaxDistance = f[0];
  25.     nTriangle = nTT[0];
  26. }
  27. else
  28. {
  29.     fMaxDistance = f[1];
  30.     nTriangle = nTT[1];
  31. }

       很高兴的进行测试(本人单元测试习惯不错),结果再次差点没让我下巴脱臼----效率有了大约1/8的下降!

        而后反复折腾,修改,是不是OMP做了不应该的同步啦,变量是否合理的shared,private,或firstprivate了,查资料,上网,读MSDN.....十七般武艺都用尽了,基本处于放弃的边沿的,终于想到使用最后的一个武器: ASM.由于是测试效率,之前的所有编译和测试都是在Release版下进行的,汇编里加入了OMP的代码后,更难于看懂,且很多变量都不知道值,这次更改为Debug进行效率测试.很快,结果水落石出:
        第一种情况

  1. #pragma omp parallel for
  2. for(int i=0; i<uCount; i+=4)
  3. {
  4.     SSE_ProcessData(buff[i]);
  5. }

        OMP优化结果是

  1. //thread 0:
  2. for(int i=0; i<uCount; i+=8)
  3. {
  4.     SSE_ProcessData(buff[i]);
  5. }
  6. //thread 1:
  7. for(int i=4; i<uCount; i+=8)
  8. {
  9.     SSE_ProcessData(buff[i]);
  10. }

        而第二种情况下,OMP优化结果是:

  1. //thread 0:
  2. for(int i=0; i<uCount/2; i+=3)
  3. {
  4. ...
  5. }
  6. //thread 1:
  7. for(int i=uCount/2; i<uCount; i+=3)
  8. {
  9. ...
  10. }

        显然,差别在循环的方式上.至于为什么OMP的编译结果有这种差别,其规则是什么,现在是不得而知----显然,跟上下文或使用指针的方式有关.要是哪位仁兄知道这些细节,烦请告知.

        报着将信将疑的态度,将第二种情况的代码修改如下:

  1. int nTT[2] = {-1,-1};
  2. FLOAT f[2] = {FLT_MAX,FLT_MAX};
  3. #pragma omp parallel sections num_threads(2)
  4. {
  5. #pragma omp section
  6.     {
  7.         int threadid = omp_get_thread_num();
  8.         for(int i=0;i<nCount; i+=6)
  9.         {
  10.             FLOAT temp;
  11.             if(FALSE == IntersectTriangleLine(pVertex[pIndex[i+0]],
  12.                 pVertex[pIndex[i+1]],
  13.                 pVertex[pIndex[i+2]],
  14.                 ray,NULL,&temp))
  15.                 continue;
  16.             if(temp < 0)
  17.                 continue;
  18.             if(temp < f[threadid])
  19.             {
  20.                 f[threadid] = temp;
  21.                 nTT[threadid] = i;
  22.             }
  23.         }
  24.     }
  25. #pragma omp section
  26.     {
  27.         int threadid = omp_get_thread_num();
  28.         for(int i=3;i<nCount; i+=6)
  29.         {
  30.             FLOAT temp;
  31.             if(FALSE == IntersectTriangleLine(pVertex[pIndex[i+0]],
  32.                 pVertex[pIndex[i+1]],
  33.                 pVertex[pIndex[i+2]],
  34.                 ray,NULL,&temp))
  35.                 continue;
  36.             if(temp < 0)
  37.                 continue;
  38.             if(temp < f[threadid])
  39.             {
  40.                 f[threadid] = temp;
  41.                 nTT[threadid] = i;
  42.             }
  43.         }
  44.     }
  45. }
  46. if(f[0] < f[1])
  47. {
  48.     fMaxDistance = f[0];
  49.     nTriangle = nTT[0];
  50. }
  51. else
  52. {
  53.     fMaxDistance = f[1];
  54.     nTriangle = nTT[1];
  55. }

        (补充:上述代码不是最终代码,最终代码是根据当前CPU核数目分组循环计算的.代码不在手边,且不复杂,就不呈上了)

 

        也就是说,两个线程,线程0处理奇数的三角面,线程1处理偶数个数的三角面。
        测试结果表明,效率终于提升了0.8倍多。

        “嘿嘿嘿,哥们,你好歹已经过了而立之年了,怎么写文章还文不对题呢?”——读者咆哮道,并且四处寻觅着鹅卵石或臭鸡蛋。
        “wusa...wusa...安静,安静,我一直都挂惦着标题呢!”----我举起了早就准备好的雨伞。

        Why?Why?Why?
        有一点在之前的文中我没有提到:为了照顾GPU缓存优化,我的所有模型数据都是针对Geforce2缓存16个顶点进行过缓存优化的,也就是说,在CPU处理这些顶点的时候,同样能享受到这样的好处。在两个线程中,同时处理两个三角面,如果他们处理速度相同,则他们基本都在处理临近的三角面,而这些临近的三角面,无论是索引,还是顶点,都处于CPU的缓存中的概率会相当大,缓存实效带来的效率下降比较小。而将数据分成两段处理,则CPU需要同时缓存四段内存(两处索引,两处顶点),缓存不够大的情况下(AMD的U本来缓存就小),缓存实效带来的效率下降就直接抵消了多线程的效率提升,甚至变得更慢了!

        为了验证这点,我在加载模型的时候,将三角面索引随机进行了替换,以打乱CPU缓存顶点数据,再次测试表明,效率有了可观的下降。

 

         我没有文不对题!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值