栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 面试经验 > 面试问答

为什么单纯的C ++矩阵乘法比BLAS慢100倍?

面试问答 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

为什么单纯的C ++矩阵乘法比BLAS慢100倍?

这是造成代码与BLAS之间性能差异的三个因素(加上有关Strassen算法的注释)。

在您的内部循环中

k
,您拥有
y[k*dim +col]
。由于内存缓存的安排方式,
k
具有
dim
和的连续值
col
映射到相同的缓存集。缓存的结构方式,每个内存地址都有一个缓存集,在缓存中必须保留其内容。每个高速缓存集都有几行(通常是四行),这些行中的每一行都可以保存映射到该特定高速缓存集的任何内存地址。

因为您的内部循环

y
以这种方式进行遍历,所以每次它使用中的元素时
y
,都必须将该元素的内存加载到与上一次迭代相同的集合中。这将强制清除集合中先前的缓存行之一。然后,在
col
循环的下一次迭代中,所有元素
y
都已从高速缓存中逐出,因此必须重新加载它们。

因此, 每次 循环加载的元素时

y
,都必须从内存中加载它,这需要很多CPU周期。

高性能代码通过两种方式避免了这种情况。第一,它将工作分成较小的块。将行和列划分为较小的大小,并使用较短的循环进行处理,这些循环可以使用高速缓存行中的所有元素,并可以在继续进行下一个块之前多次使用每个元素。因此,对

x
和的元素的大多数引用
y
通常来自缓存,通常是在单个处理器周期中。二,在某些情况下,代码会将数据从矩阵的一列(由于几何结构而导致缓存发生抖动)中复制到临时缓冲区的行中(从而避免出现抖动)。这再次允许大多数内存引用从缓存而不是从内存提供。

另一个因素是使用单指令多数据(SIMD)功能。许多现代处理器的指令

float
都在一条指令中加载多个元素(典型的是四个元素,但现在有八个),存储多个元素,添加多个元素(例如,对于这四个元素中的每一个,将其添加到这四个元素中的相应一个),将多个元素相乘,依此类推。只要您能够安排使用这些指令的工作,只需使用这些指令即可立即使代码快四倍。

这些指令无法在标准C中直接访问。某些优化器现在尝试在可能的情况下使用此类指令,但是这种优化很困难,并且从中获得很多好处并不常见。许多编译器提供了对语言的扩展,这些语言可以访问这些指令。就个人而言,我通常更喜欢使用汇编语言编写以使用SIMD。

另一个因素是在处理器上使用指令级并行执行功能。观察到您的内循环中

acc
已更新。下一个迭代无法添加到
acc
上一个迭代完成更新之前
acc
。高性能代码将使多个和并行运行(甚至多个SIMD和)。这样的结果是,在执行一个和的加法运算时,将开始另一个和的加法运算。在当今的处理器上,通常一次支持四个或更多正在进行的浮点运算。如所写,您的代码根本无法做到这一点。一些编译器将尝试通过重新排列循环来优化代码,但这要求编译器能够看到特定循环的迭代是彼此独立的,或者可以与另一个循环进行交换等。

有效地使用缓存可以使性能提高十倍,SIMD可以提供另外四项,而指令级并行性可以提供另外四项,共计160,这是完全可行的。

根据此Wikipedia页面,这是对Strassen算法效果的非常粗略的估计。维基百科页面说Strassen的略好于直接乘法围绕N
= 100。这表明的执行时间的常数因子的比率为100 3 /100 2.807
≈2.4。显然,这将根据处理器模型,与缓存效果相互作用的矩阵大小等而有很大差异。但是,简单的外推法显示,在n =
4096((4096/100)3-2.807≈2.05)时,斯特拉森的效率约为直接乘法的两倍。再次,这只是一个大概的估计。


至于以后的优化,请在内部循环中考虑以下代码:

bufz[trow][tcol] += B * bufy[tk][tcol];

与此相关的一个潜在问题是,

bufz
通常可能会重叠
bufy
。由于您为
bufz
和使用了全局定义
bufy
,因此编译器可能会知道在这种情况下它们不会重叠。但是,如果将此代码移入作为参数传递
bufz
bufy
作为参数的子例程中,特别是如果在单独的源文件中编译该子例程,则编译器不太可能知道这一点
bufz
并且
bufy
不会重叠。在那种情况下,编译器无法矢量化代码或对代码重新排序,因为
bufz[trow][tcol]
此迭代中的可能与
bufy[tk][tcol]
另一迭代中的相同。

即使编译器可以看到,子程序被调用用不同的

bufz
bufy
当前的源模块中,如果例程有
extern
联动装置(默认值),则编译器必须允许例程可以从外部模块调用,因此它必须如果
bufz
bufy
重叠,则生成可以正常工作的代码。(编译器可以处理此问题的一种方法是生成例程的两个版本,一个版本从外部模块调用,一个版本从当前正在编译的模块调用。是否执行此操作取决于您的编译器,优化会切换,等)。如果您将例程声明为
static
,则编译器知道无法从外部模块调用它(除非您获取其地址,并且该地址有可能传递到当前模块之外)。

另一个潜在的问题是,即使编译器对此代码进行矢量化处理,也不一定会为您执行的处理器生成最佳代码。查看生成的汇编代码,看来编译器仅在

%ymm1
重复使用。一次又一次地,它将内存中的值乘以
%ymm1
,将内存中的值相加,并将内存中
%ymm1
的值存储
%ymm1
至内存。这有两个问题。

第一,您不希望将这些部分总和频繁存储到内存中。您希望将许多附加值累加到一个寄存器中,并且该寄存器将很少被写入存储器。要说服编译器执行此操作,可能需要重写代码,以明确地将部分和保留在临时对象中,并在循环完成后将其写入内存。

第二,这些指令名义上是串行相关的。在乘法完成之前,添加操作无法开始,并且在添加完成之前,存储区无法写入内存。Core
i7具有出色的无序执行功能。因此,尽管它具有等待开始执行的加法,但稍后会在指令流中查看乘法并启动它。(即使该乘法也使用

%ymm1
,处理器将动态地重新映射寄存器,以便它使用不同的内部寄存器来进行乘法运算。)即使您的代码充满了连续的依赖关系,处理器也会尝试一次执行多条指令。但是,许多因素都会干扰这一点。您可以用尽处理器用于重命名的内部寄存器。您使用的内存地址可能会出现错误冲突。(处理器查看大约十二个内存地址的低位,以查看该地址是否与它试图从较早的指令加载或存储的另一个地址相同。如果位相等,则处理器具有延迟当前的加载或存储,直到它可以确认整个地址都不同为止。这种延迟可能比当前的加载或存储更多。

这是我更喜欢在汇编中编写高性能代码的另一个原因。要在C语言中执行此操作,您必须说服编译器通过执行诸如编写自己的一些SIMD代码(使用它们的语言扩展)和手动展开循环(编写多个迭代)之类的指令来进行指导。

复制进出缓冲区时,可能会有类似的问题。但是,您报告90%的时间用在上

calc_block
,因此我没有仔细研究。

另外,斯特拉森的算法是否考虑了剩余的任何差异?



转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/403331.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号