缓存友好的优化:面向对象的矩阵乘法和函数内平铺矩阵乘法

Posted

技术标签:

【中文标题】缓存友好的优化:面向对象的矩阵乘法和函数内平铺矩阵乘法【英文标题】:Cache-friendly optimization: Object oriented matrix multiplication and in-function tiled matrix multiplication 【发布时间】:2013-07-22 10:54:25 【问题描述】:

在使用 this implementation 编写了一个表示两个 1D 缓冲区中的整个矩阵的矩阵类之后,我已经达到了我的项目中的矩阵错误部分并且现在倾向于一些缓存友好的优化。偶然发现了两个选项(问题在此页面的下部):

1) 仅在乘法时间内选择阻塞/平铺子矩阵。

在 c++ DLL 函数中完成,因此没有函数开销。 由于代码会更复杂,因此更难应用额外的优化。

2)从子矩阵类(较小的补丁)构建矩阵类,因此通常在子矩阵类上进行乘法运算。

面向对象的方法为子矩阵的其他优化留出了空间。 C# 的对象标头和填充行为有助于克服关键的进步? 函数开销在多次而不是多次调用后可能会成为问题。

矩阵乘法示例:C=A.B

 A
 1 2 3 4   is used as  1 2    3 4  
 4 3 4 2               4 3    4 2  
 1 3 1 2                           
 1 1 1 2               1 3    1 2  
                       1 1    1 2


 B
 1 1 1 1  --->         1 1    1 1
 1 1 1 1               1 1    1 1
 1 1 1 1 
 1 1 1 1               1 1    1 1
                       1 1    1 1


 Multiplication:       1 2 * 1 1   +   3 4 * 1 1    ==>  upper-left tile of result
                       4 3   1 1       4 2   1 1  


                       same for the upper-right of result

                       1 3 * 1 1   +   1 2 * 1 1    ==> lower left tile of result
                       1 1   1 1       1 2   1 1

                       same for lower-right tile of result

        Multiplication is O(n³) but summation is O(n²).

问题:有没有人尝试过(函数式和面向对象)并进行性能比较?现在,我没有任何这些缓存目标优化的天真的乘法需要:

 Matrix Size   Single Threaded Time    Multithreaded Time
 * 128x128   :     5    ms                 1ms-5ms(time sample error is bigger)
 * 256x256   :     25   ms                 7     ms
 * 512x512   :     140  ms                 35    ms
 * 1024x1024 :     1.3  s                  260   ms
 * 2048x2048 :     11.3 s                  2700  ms 
 * 4096x4096 :     88.1 s                  24    s
 * 8192x8192 :     710  s                  177   s

 Giga-multiplications of variables per second
               Single threaded         Multithreaded           Multi/single ratio               
 * 128x128   :     0.42                    2.0 - 0.4               ?
 * 256x256   :     0.67                    2.39                   3.67x  
 * 512x512   :     0.96                    3.84                   4.00x
 * 1024x1024 :     0.83                    3.47                   4.18x
 * 2048x2048 :     0.76                    3.18                   4.18x
 * 4096x4096 :     0.78                    2.86                   3.67x
 * 8192x8192 :     0.77                    3.09                   4.01x

(1.4GHz fx8150 的平均结果以及使用 32 位浮点数的 avx 优化代码)(Visual Studio C# 的 Parallel.For() 中 dll 函数中的 c++ avx-intrinsics)

上述哪种大小的矩阵可能会受到缓存未命中、临界步长和其他不良情况的影响?你知道我怎样才能获得那些使用内在函数的性能计数器吗?

感谢您的时间。

编辑: DLL 内的内联优化:

 Matrix Size   Single Threaded Time    Multithreaded Time           Multi/Single radio
 * 128x128   :  1     ms(%400)          390us avrage in 10k iterations(6G mult /s)
 * 256x256   :  12    ms(%108 faster)   2     ms   (%250 faster)         6.0x
 * 512x512   :  73    ms(%92 faster)    15    ms   (%133 faster)         4.9x
 * 1024x1024 :  1060  ms(%22 faster)    176   ms   (%48 faster)          6.0x
 * 2048x2048 :  10070 ms(%12 faster)    2350  ms   (%15 faster)          4.3x
 * 4096x4096 :  82.0  s(%7 faster)      22    s    (%9 faster)           3.7x
 * 8192x8192 :  676   s(%5 faster)      174   s    (%2 faster)           4.1x

在内联之后,较小乘法的阴影性能变得可见。 仍然存在 DLL-function-C# 开销。 1024x1024 的情况似乎是缓存未命中的起点。虽然工作量只增加了 7 倍,但执行时间却增加到了 15 倍。

编辑::本周将尝试使用 Strassen 的面向对象方法的 3 层深度算法。主矩阵将由 4 个子矩阵组成。然后它们将由每个子 4 个子组成。然后它们将分别由 4 个 sub-sub-sub 组成。这应该会产生接近 (8/7)(8/7)(8/7)= +%50 的加速。如果可行,会将 DLL 函数转换为使用更多缓存的补丁优化函数。

【问题讨论】:

查看每秒的操作数(或类似的)以确定缓存开始影响计算的位置 - 我没有对您的样本进行精确的数学计算,但只是查看它并制作我脑海中的数学运算(毫无疑问是错误的)表明 2K x 2K 到 4K x 4K 比其他的要大得多(无论是单核还是多核变体,这不足为奇)。 好的,每秒计算的乘法,512x512 表现最好。但是最大的飞跃是在 256x256 和 512x512 之间并且是正的,这一定是函数开销,最大的下降是在 512x512 和 1024x1024 之间。这是否意味着,两个矩阵的 4M 元素是 16M,即使在 L3 中也不适合? 是的,听起来确实像您的较小的那些会受到功能开销的影响。我不知道你的处理器有什么大小的缓存,但是是的,当越过缓存边缘时仍然会有很大的下降。实际上,我希望它比您的结果显示的更明显-因为实际的内存吞吐量会差很多-但我怀疑您还没有遇到最坏的情况(还没有?)。 1024x1024 部分使用超过 20GB/s,这对于我的 1866MHz 双通道 ddr3 内存来说非常困难。一小部分可能来自缓存使用。 L1=64 Kilo Bytes 每 2 个内核共享,L2=2MB 每两个内核,L3=8MB 共享。 1024 x 1024 x sizeof(double) = 8MB。因此,此时您显然不会将 A 和 B 矩阵都放入任何缓存中 - 但是,对于 4 个内核,您应该能够将大部分放入 L2 缓存中。 【参考方案1】:

将 Strassen 算法仅应用于一层(例如 256x256 中的四个作为 512x512)作为面向对象的方法(超类是 Strassen,子矩阵是 matrix 类):

 Matrix Size   Single Threaded Time    Multithreaded Time           Multi/Single radio
 * 128x128   :  **%50 slowdown**        **slowdown**              
 * 256x256   :  **%30 slowdown**        **slowdown**         
 * 512x512   :  **%10 slowdown**        **slowdown**            
 * 1024x1024 :  540   ms(%96 faster)    130   ms   (%35 faster)     4.15     
 * 2048x2048 :  7500  ms(%34 faster)    1310  ms   (%79 faster)     5.72    
 * 4096x4096 :  70.2  s(%17 faster)     17    s    (%29 faster)     4.13
 * 6144x6144 :          x               68    s               
 * 8192x8192 :  outOfMemoryException    outOfMemoryException     

DLL 函数和 C# 之间的开销仍然有效,因此小矩阵无法变得更快。但是当有加速时,它总是超过 8/7(%14),因为使用更小的块更适合缓存使用。

将编写一个基准类,反复测试 Stressen 算法的不同叶子大小,而不是简单的算法,以找到临界大小。 (对于我的系统,它是 512x512)。

超类将递归地构建子矩阵树,直到它达到 512x512 大小,并将对 512x512 节点使用朴素算法。然后在 DLL 函数中,一个修补/阻塞算法(将在下周添加)将使其更快一些。但我不知道如何选择合适的补丁大小,因为我不知道如何获取 cpu 的缓存行大小。将在递归 Strassen 完成后进行搜索。

我的施特拉森算法的实现需要五倍的内存(正在处理它)。

编辑:一些递归已经完成,随着结果的到来更新表格。

 Matrix Size   Single Threaded Time    Multithreaded Time           
 * 2048x2048 :  x                       872     ms  average(double layer)    
 * 2560x2560 :  x                       1527    ms  average(double layer)  

并行化树解析,减少内存占用并引入完全递归:

 Matrix Size   Single Threaded Time    Multithreaded Time                         m/s
 * 1024x1024 :  547   ms               123     ms  average(single layer)          4.45x
 * 2048x2048 :  3790  ms               790     ms  average(double layer)          4.79x
 * 4096x4096 :  26.4  s                5440    ms  average(triple layer)          4.85x
 * 8192x8192 :  185   s                38      s   average(quad layer)            4.87x
 * 8192x8192(4GHz):   x                15      s   average(quad layer)            4.87x

每秒乘法次数 (x10^9):

 Matrix Size   Single Threaded         Multithreaded                          
 * 1024x1024 :  1.71                   7.64     (single layer)          
 * 2048x2048 :  1.73                   8.31     (double layer)          
 * 4096x4096 :  1.74                   8.45     (triple layer)         
 * 8192x8192 :  1.74                   8.48     (quad layer)           
 * 8192x8192(4GHz):   x                21.49    (quad layer)           
 Strassen's cpu flops is multiplied by 7/8 for each layer.

刚刚发现使用类似价格的 gpu 使用 opencl 可以在 1 秒内完成 8kx8k。

【讨论】:

以上是关于缓存友好的优化:面向对象的矩阵乘法和函数内平铺矩阵乘法的主要内容,如果未能解决你的问题,请参考以下文章

cpumemory.pdf - 缓存优化矩阵乘法

在 CUDA 的平铺矩阵乘法中访问矩阵作为其转置

使用 valgrind 进行平铺矩阵乘法的 C++ 性能分析

CUDA 平铺矩阵乘法解释

矩阵乘法优化之分块矩阵

DP优化:矩阵乘法