C 矩阵乘法:为什么非阻塞优于阻塞?
我试图通过阻塞循环来提高缓存性能,从而加快矩阵乘法算法的速度,但无论矩阵大小、块大小(我尝试了很多介于2和200之间的值,potenses为2和其他值)和优化级别如何,非阻塞版本的速度都会显著提高 非阻止版本:C 矩阵乘法:为什么非阻塞优于阻塞?,c,caching,matrix-multiplication,cpu-architecture,C,Caching,Matrix Multiplication,Cpu Architecture,我试图通过阻塞循环来提高缓存性能,从而加快矩阵乘法算法的速度,但无论矩阵大小、块大小(我尝试了很多介于2和200之间的值,potenses为2和其他值)和优化级别如何,非阻塞版本的速度都会显著提高 非阻止版本: for(size_t i = 0; i < n; ++i) { for(size_t k = 0; k < n; ++k) { int r = a[i][k]; for(size_t j = 0; j < n; ++j)
for(size_t i = 0; i < n; ++i)
{
for(size_t k = 0; k < n; ++k)
{
int r = a[i][k];
for(size_t j = 0; j < n; ++j)
{
c[i][j] += r * b[k][j];
}
}
}
for(size_t kk = 0; kk < n; kk += BLOCK)
{
for(size_t jj = 0; jj < n; jj += BLOCK)
{
for(size_t i = 0; i < n; ++i)
{
for(size_t k = kk; k < kk + BLOCK; ++k)
{
int r = a[i][k];
for(size_t j = jj; j < jj + BLOCK; ++j)
{
c[i][j] += r * b[k][j];
}
}
}
}
}
(大小i=0;i
{
对于(尺寸k=0;k
阻止版本:
for(size_t i = 0; i < n; ++i)
{
for(size_t k = 0; k < n; ++k)
{
int r = a[i][k];
for(size_t j = 0; j < n; ++j)
{
c[i][j] += r * b[k][j];
}
}
}
for(size_t kk = 0; kk < n; kk += BLOCK)
{
for(size_t jj = 0; jj < n; jj += BLOCK)
{
for(size_t i = 0; i < n; ++i)
{
for(size_t k = kk; k < kk + BLOCK; ++k)
{
int r = a[i][k];
for(size_t j = jj; j < jj + BLOCK; ++j)
{
c[i][j] += r * b[k][j];
}
}
}
}
}
(大小kk=0;kk
{
对于(大小jj=0;jj
我也有一个bijk版本和一个6-loops bikj版本,但它们都比非阻塞版本表现更好,我不明白为什么会发生这种情况。我遇到的每一篇论文和教程似乎都表明,被阻止的版本应该要快得多。如果有必要的话,我将在Core i5上运行此功能。尝试仅在一维中阻塞,而不是在二维中阻塞
矩阵乘法彻底地处理两个矩阵中的元素。左矩阵上的每一行向量都会被重复处理,并放入右矩阵的连续列中
如果矩阵不能同时放入缓存中,则某些数据最终会加载多次
我们可以做的是分解操作,以便一次处理大约缓存大小的数据量。我们希望缓存左操作数的行向量,因为它会对多个列重复应用。但我们应该(一次)只获取足够的列,以保持在缓存的限制范围内。例如,如果我们只能获取25%的列,这意味着我们必须将行向量传递四次。最后,我们从内存中加载左矩阵四次,右矩阵只加载一次
(如果要多次加载任何内容,则应该是左侧的行向量,因为它们在内存中是平面的,这得益于突发加载。许多缓存体系结构可以比随机访问加载更快地从内存向相邻缓存线执行突发加载。如果右矩阵以列主顺序存储,则这将是even更好:然后我们在平面阵列之间进行交叉积,这样可以很好地预取内存。)
我们也不要忘记输出矩阵。输出矩阵也会占用缓存中的空间
我怀疑2D分块方法的一个缺陷是,输出矩阵的每个元素都依赖于两个输入:左矩阵中的整行和右矩阵中的整列。如果矩阵是分块访问的,这意味着每个目标元素会被访问多次以累积部分结果
如果我们做一个完整的行-列点积,我们不必访问c[i][j]
多次;一旦我们把j
列带进行i
,我们就完成了c[i][j]
什么编译器?什么优化设置?变量仍然太多。我只在一个维度上进行了阻塞实验。kk-I-k-j循环获得了最佳性能,并设法将非阻塞版本的性能提高了2倍。选择k循环作为要阻塞的循环实际上是有意义的,因为这意味着将受到影响的矩阵e大多数缓存未命中都是输出缓存,但写入操作永远不会使处理器停止运行,因此不会影响吞吐量。@terrankinkah:指令仍必须按顺序退出,因此缓存未命中存储最终可以防止新的UOP进入ROB。重新排序的缓冲区约为200 UOP,这不足以完全隐藏缓存未命中的延迟这必须等待主内存(经过良好调优的代码将在每个时钟上执行超过2个UOP,每个时钟最多执行4个融合域UOP)。