如何使用缓存技术提高性能

Posted

技术标签:

【中文标题】如何使用缓存技术提高性能【英文标题】:How to improve performance with caching technique 【发布时间】:2013-10-10 22:31:37 【问题描述】:

您好,我正在尝试运行一个程序,该程序使用蛮力和缓存技术(例如此处的 pdf)找到最接近的配对:Caching Performance Stanford

我原来的代码是:

float compare_points_BF(int N,point *P)
    int i,j;
    float  distance=0, min_dist=FLT_MAX;
    point *p1, *p2;
    unsigned long long calc = 0;
    for (i=0;i<(N-1);i++)
        for (j=i+1;j<N;j++)
            if ((distance = (P[i].x - P[j].x) * (P[i].x - P[j].x) +
                    (P[i].y - P[j].y) * (P[i].y - P[j].y)) < min_dist)
            min_dist = distance;
            p1 = &P[i];
            p2 = &P[j];
            
        
    
    return sqrt(min_dist);

这个程序给出了大约这些运行时间:

      N     8192    16384   32768   65536   131072  262144  524288  1048576      
 seconds    0,070   0,280   1,130   5,540   18,080  72,838  295,660 1220,576
            0,080   0,330   1,280   5,190   20,290  80,880  326,460 1318,631

上述程序的缓存版本为:

float compare_points_BF(register int N, register int B, point *P)
    register int i, j, ib, jb, num_blocks = (N + (B-1)) / B;
    register point *p1, *p2;
    register float distance=0, min_dist=FLT_MAX, regx, regy;

    //break array data in N/B blocks, ib is index for i cached block and jb is index for j strided cached block
    //each i block is compared with the j block, (which j block is always after the i block) 
    for (i = 0; i < num_blocks; i++)
        for (j = i; j < num_blocks; j++)
            //reads the moving frame block to compare with the i cached block
            for (jb = j * B; jb < ( ((j+1)*B) < N ? ((j+1)*B) : N); jb++)
                //avoid float comparisons that occur when i block = j block
                //Register Allocated
                regx = P[jb].x;
                regy = P[jb].y;
                for (i == j ? (ib = jb + 1) : (ib = i * B); ib < ( ((i+1)*B) < N ? ((i+1)*B) : N); ib++)
                    //calculate distance of current points
                    if((distance = (P[ib].x - regx) * (P[ib].x - regx) +
                            (P[ib].y - regy) * (P[ib].y - regy)) < min_dist)
                        min_dist = distance;
                        p1 = &P[ib];
                        p2 = &P[jb];
                    
                
            
        
    
    return sqrt(min_dist);

还有一些结果:

Block_size = 256        N = 8192        Run time: 0.090 sec
Block_size = 512        N = 8192        Run time: 0.090 sec
Block_size = 1024       N = 8192        Run time: 0.090 sec
Block_size = 2048       N = 8192        Run time: 0.100 sec
Block_size = 4096       N = 8192        Run time: 0.090 sec
Block_size = 8192       N = 8192        Run time: 0.090 sec


Block_size = 256        N = 16384       Run time: 0.357 sec
Block_size = 512        N = 16384       Run time: 0.353 sec
Block_size = 1024       N = 16384       Run time: 0.360 sec
Block_size = 2048       N = 16384       Run time: 0.360 sec
Block_size = 4096       N = 16384       Run time: 0.370 sec
Block_size = 8192       N = 16384       Run time: 0.350 sec
Block_size = 16384      N = 16384       Run time: 0.350 sec

Block_size = 128        N = 32768       Run time: 1.420 sec
Block_size = 256        N = 32768       Run time: 1.420 sec
Block_size = 512        N = 32768       Run time: 1.390 sec
Block_size = 1024       N = 32768       Run time: 1.410 sec
Block_size = 2048       N = 32768       Run time: 1.430 sec
Block_size = 4096       N = 32768       Run time: 1.430 sec
Block_size = 8192       N = 32768       Run time: 1.400 sec
Block_size = 16384      N = 32768       Run time: 1.380 sec

Block_size = 256        N = 65536       Run time: 5.760 sec
Block_size = 512        N = 65536       Run time: 5.790 sec
Block_size = 1024       N = 65536       Run time: 5.720 sec
Block_size = 2048       N = 65536       Run time: 5.720 sec
Block_size = 4096       N = 65536       Run time: 5.720 sec
Block_size = 8192       N = 65536       Run time: 5.530 sec
Block_size = 16384      N = 65536       Run time: 5.550 sec

Block_size = 256        N = 131072      Run time: 22.750 sec
Block_size = 512        N = 131072      Run time: 23.130 sec
Block_size = 1024       N = 131072      Run time: 22.810 sec
Block_size = 2048       N = 131072      Run time: 22.690 sec
Block_size = 4096       N = 131072      Run time: 22.710 sec
Block_size = 8192       N = 131072      Run time: 21.970 sec
Block_size = 16384      N = 131072      Run time: 22.010 sec

Block_size = 256        N = 262144      Run time: 90.220 sec
Block_size = 512        N = 262144      Run time: 92.140 sec
Block_size = 1024       N = 262144      Run time: 91.181 sec
Block_size = 2048       N = 262144      Run time: 90.681 sec
Block_size = 4096       N = 262144      Run time: 90.760 sec
Block_size = 8192       N = 262144      Run time: 87.660 sec
Block_size = 16384      N = 262144      Run time: 87.760 sec

Block_size = 256        N = 524288      Run time: 361.151 sec
Block_size = 512        N = 524288      Run time: 379.521 sec
Block_size = 1024       N = 524288      Run time: 379.801 sec

从我们可以看到运行时间比非缓存代码慢。 这是由于编译器优化吗?代码是坏的还是仅仅是因为算法在平铺方面表现不佳?我使用用 32 位可执行文件编译的 VS 2010。提前致谢!

【问题讨论】:

你认为你的 CPU 有多少个寄存器? @SJuan76 我的 cpu 是 i7 980x. It has 32KB L1 data, 32KB L1 instruction per core L2 cache: 256KB per core, inclusive L3 cache: 12MB accessible by all cores, inclusive. 如果您想知道,我确实尝试过使用代码中的 register 符号减少块大小和变量,但仍然没有更好的性能。这可能是由于寄存器溢出吗? register 在现代编译器上没有任何作用。编译器比你更清楚,而且大多会忽略它。 您正在使用一篇介于登月和今天之间的论文进行当时有效的特定优化,并尝试将其应用于现代架构。我并不是说这一切都不适用,但我会小心的。至于具体建议?我很难解码您的代码,但是带有所有奇怪比较和分支的内部循环看起来并不健康。我希望您知道您的编译器实际上并没有在您运行时编译代码,因此单行实际上并不比正确格式化和拆分代码更快。 ulrich drepper on memory 大约有六年的历史,但仍然与现代商品 cpus 相关。 【参考方案1】:

这是一个有趣的案例。编译器在两个内部循环中的循环不变提升方面做得很差。即,两个内部 for 循环在每次迭代中检查以下条件:

(j+1)*B) < N ? ((j+1)*B) : N

(i+1)*B) < N ? ((i+1)*B) : N

计算和分支都很昂贵;但它们实际上对于两个内部 for 循环是循环不变的。一旦手动将它们从两个内部 for 循环中提升出来,我就能够使缓存优化版本的性能优于未优化版本(当 N==524288 时为 10%,当 N=1048576 时为 30%)。

这里是修改后的代码(真的很简单,找u1,u2):

//break array data in N/B blocks, ib is index for i cached block and jb is index for j strided cached block
//each i block is compared with the j block, (which j block is always after the i block) 
for (i = 0; i < num_blocks; i++)
    for (j = i; j < num_blocks; j++)
        int u1 =  (((j+1)*B) < N ? ((j+1)*B) : N);
        int u2 =  (((i+1)*B) < N ? ((i+1)*B) : N);
        //reads the moving frame block to compare with the i cached block
        for (jb = j * B; jb < u1 ; jb++)
            //avoid float comparisons that occur when i block = j block
            //Register Allocated
            regx = P[jb].x;
            regy = P[jb].y;
            for (i == j ? (ib = jb + 1) : (ib = i * B); ib < u2; ib++)
                //calculate distance of current points
                if((distance = (P[ib].x - regx) * (P[ib].x - regx) +
                        (P[ib].y - regy) * (P[ib].y - regy)) < min_dist)
                    min_dist = distance;
                    p1 = &P[ib];
                    p2 = &P[jb];
                
            
        
    

【讨论】:

嗯,你明白了。你建议如何让他们摆脱困境?您可以在这里发布您的代码吗? 刚刚也确认了。如果我从循环不变量中得到条件,我们会获得更好的性能。如果您想获得更好的视图,可以发布您的代码:) 如果您能想到任何其他修改以获得更好的性能,请在此处与我们分享 :) 你不会从中得到太多(如果有的话)。一旦它被吊出两个内环,它就真的微不足道了。优化的关键始终是内部循环(当然除了算法级别的变化)。在您的情况下,内部循环代码非常简单,这里没有太多工作要做。如果你真的需要坚持单线程,我会考虑并行化(算法应该很容易并行化)或 SSE。【参考方案2】:

Tiling 可能是一个古老的概念,但它在今天仍然非常重要。在您的原始代码中,对于每个 i,您可以在仍然缓存的同时重用大部分 P[j] 元素,但前提是内部循环的长度足够小以适合那里。实际大小应由您要针对平铺的缓存级别确定 - L1 将提供最佳性能,因为它是最快的,但由于它也是最小的,您需要小块并且平铺开销可能太多。 L2 允许更大的图块,但会略微降低性能,等等。

请注意,您不需要在此处使用 2d 平铺,这不是矩阵乘法 - 您正在遍历同一个数组。你可以简单地平铺内循环,因为它是溢出缓存的循环,一旦你这样做了 - 外循环 (i) 可以在当前缓存的内循环元素块上一直运行到最后。 2d 平铺实际上没有意义,因为没有人会重用外循环的元素(与矩阵 mul 不同)

因此,假设 Point 是 64 位大,您可以将 512 个这样的数组元素安全地放入 32k L1 中,或者将 4096 个元素放入 256k L2 中。如果 i 超出当前 j 块的范围,您将不得不错过每个块上的 P[i] 一次,但这可以忽略不计。

顺便说一句 - 这个解释可能仍然过时,因为足够好的编译器可能会尝试为您完成所有这些工作。虽然它相当复杂,所以我有点怀疑任何常见的甚至会尝试,但在这里应该很容易证明重新排序是安全的。当然,有人可能会争辩说“足够好的编译器”是一个悖论,但这是题外话......

【讨论】:

以上是关于如何使用缓存技术提高性能的主要内容,如果未能解决你的问题,请参考以下文章

如何利用SCM/NVM技术提高数据库性能?

Kong入门指南 - 通过代理缓存提高性能

Kong入门指南 - 通过代理缓存提高性能

如何通过缓存提高Web 场景中的ASP.NET App性能

如何编写最能利用 CPU 缓存来提高性能的代码?

如何优化 Django REST Framework 的性能