使用循环平铺转置大型二维矩阵没有性能提升

Posted

技术标签:

【中文标题】使用循环平铺转置大型二维矩阵没有性能提升【英文标题】:No performance gain with transpose of large 2d Matrix using Loop tiling 【发布时间】:2017-09-02 07:44:27 【问题描述】:

使用平铺方法(缓存感知)转置大小为 1 gb 的全局 2D 方阵/数组在单线程执行中与普通转置方法相比没有性能提升。不讨论使用 AVX、SSE(SIMD) 或任何其他缓存无意识转置算法的转置加速(http://supertech.csail.mit.edu/papers/FrigoLePr12.pdf)

#include <stdio.h>
#include <sys/time.h>
#define SIZE 16384
float a[SIZE][SIZE], b[SIZE][SIZE];

void testNormalTranspose() 
int i, j, k, l;
b[0][9999] = 1.0;
for (i=0; i<SIZE; i++)
    for (j=0; j<SIZE; j++)
      a[i][j] = b[j][i];


void testTiledTranspose()
    int i, j, k, l;
    b[0][9999] = 1.0;
    int blocksize = 16;
    for (i=0; i<SIZE; i+= blocksize) 
        for (j=0; j<SIZE; j+=blocksize) 
            for (int ii = i;ii <i + blocksize; ++ii) 
                for (int jj = j; jj < j + blocksize; ++jj) 
                    a[ii][jj] = b[jj][ii];
                

            
           
      


int main()

    struct timeval t1, t2;
    /*
      gettimeofday(&t1, NULL);
      testNormalTranspose();
      gettimeofday(&t2, NULL);
      printf("Time for the Normal transpose  is %ld milliseconds\n",
             (t2.tv_sec - t1.tv_sec)*1000 + 
             (t2.tv_usec - t1.tv_usec) / 1000);
    */
      gettimeofday(&t1, NULL);
      testTiledTranspose();
      gettimeofday(&t2, NULL);
      printf("Time for the Tiled transpose  is %ld milliseconds\n",
             (t2.tv_sec - t1.tv_sec)*1000 + 
             (t2.tv_usec - t1.tv_usec) / 1000);
      printf("%f\n", a[9999][0]);

【问题讨论】:

如果您不喜欢谈论缓存一致性,您的问题是什么以及为什么一种方法比另一种更快。 平铺提供空间局部性。它如何帮助提高上述方法的性能:testTiledTranspose 无法重现故障。我所做的所有测试都提供了显着的性能改进(2.5..3.2 倍)。正在发生其他事情。 你不能同时运行这两个代码,因为最后一次运行会将一些矩阵推送到缓存中,因此第二个运行的人都会加快速度。首先运行 NormalTranspose 并注释 TiledTranspose 然后反之亦然 1.使用 8192 的 Ubuntu gcc 没有任何收益,但看到较小尺寸(8192 到 2048)的 10/30% 改进。代码不能在 Windows(cygwin,mingw)上编译,因为它不可能在全局 bss 中分配 2gb。但是对于较小的数组大小,Windows 上的平铺方法可以获得显着的性能提升。在 Windows10 周年更新上尝试使用最新的 Linux 子系统(Ubuntu)也显示了平铺的好处。 【参考方案1】:

循环平铺有助于防止数据被重用。如果你使用一个元素 SIZE 次,你最好使用它 SIZE 次,然后才继续下一个元素。

不幸的是,转置 2D 矩阵并没有重用矩阵 a 和 b 的任何元素。更重要的是,由于在循环中混合了行和列访问(即 a[i][j] = b[j][i]),因此您永远不会同时在 a 和 b 数组上获得单位步长内存访问时间,但仅限于其中之一。

因此,在这种情况下,平铺并不是那么有效,但即使在以下情况下使用“随机”内存访问,您仍然可能会获得一些性能改进:

您现在访问的元素与您之前访问的元素位于同一缓存行中并且 该缓存行仍然可用。

因此,要查看任何改进,这种“随机”访问的内存占用必须适合您系统的缓存。基本上,这意味着您必须仔细选择blocksize,并且您在示例中选择的 16 可能在一个系统上效果更好,而在另一个系统上效果更差。

以下是我的计算机对 2 个块大小和SIZE 4096 的不同幂的结果:

---------------------------------------------------------------
Benchmark                        Time           CPU Iterations
---------------------------------------------------------------
transpose_2d              32052765 ns   32051761 ns         21
tiled_transpose_2d/2      22246701 ns   22245867 ns         31
tiled_transpose_2d/4      16912984 ns   16912487 ns         41
tiled_transpose_2d/8      16284471 ns   16283974 ns         43
tiled_transpose_2d/16     16604652 ns   16604149 ns         42
tiled_transpose_2d/32     23661431 ns   23660226 ns         29
tiled_transpose_2d/64     32260575 ns   32259564 ns         22
tiled_transpose_2d/128    32107778 ns   32106793 ns         22
fixed_tile_transpose_2d   16735583 ns   16729876 ns         41

如您所见,带有blocksize 8 的版本对我来说效果最好,性能几乎翻倍。

以下是SIZE 4131 和 3 个块大小的幂的结果:

---------------------------------------------------------------
Benchmark                        Time           CPU Iterations
---------------------------------------------------------------
transpose_2d              29875351 ns   29874381 ns         23
tiled_transpose_2d/3      30077471 ns   30076517 ns         23
tiled_transpose_2d/9      20420423 ns   20419499 ns         35
tiled_transpose_2d/27     13470242 ns   13468992 ns         51
tiled_transpose_2d/81     11318953 ns   11318646 ns         61
tiled_transpose_2d/243    10229250 ns   10228884 ns         65
fixed_tile_transpose_2d   10217339 ns   10217066 ns         67

关于 16384 大小问题。我无法重现它,即我仍然看到大矩阵的相同增益。请注意,16384 * 16384 * sizeof(float) 为 4GB,这可能会暴露一些系统问题...

【讨论】:

您的解释是正确的,但您可能想尝试非二次方图块大小/矩阵大小,因为已知矩阵转置是缓存行冲突的最坏情况之一。跨度> @Nonyme 我刚刚尝试了 3 的幂 - 结果完全相同:平铺有帮助,但您必须找到“最佳位置”。更新了答案... 感谢这些额外的测试。值得一提的是,内存访问可能会得到预测,因此缓存预取已经解决了大部分问题。 @Nonyme 是的,现在预取器相当好......请注意,在您的示例矩阵中,SIZE 必须是 blocksize 的乘积。否则需要处理剩余部分...

以上是关于使用循环平铺转置大型二维矩阵没有性能提升的主要内容,如果未能解决你的问题,请参考以下文章

使用多线程时性能几乎没有提升

性能提升 - 使用 Get 方法循环

在 app.yaml 中定义路由与在 AppEngine 中的 WSGIApplication 中定义一个大型映射相比有性能提升吗?

在 app.yaml 中定义路由与在 AppEngine 中的 WSGIApplication 中定义一个大型映射相比有性能提升吗?

MNN卷积性能提升90%!ARMv86正式投用

使用 AVX 的平铺矩阵乘法