我有2个C序列,它们都乘以两个矩阵。
序列1:
int A[M][N], B[N][P], C[M][P], i, j, k;
for (i = 0; i < M; i++)
for (j = 0; j < P; j++)
for (k = 0; k < N; k++)
C[i][j] += A[i][k] * B[k][j];
序列2:
int A[M][N], B[N][P], C[M][P], i, j, k;
for (i = M - 1; i >= 0; i--)
for (j = P - 1; j >= 0; j--)
for (k = N - 1; k >= 0; k--)
C[i][j] += A[i][k] * B[k][j];
我的问题是:用汇编语言翻译时哪一个更有效率? 我很确定第二个可以使用循环指令编写,而第一个可以使用inc / jl编写。
答案 0 :(得分:3)
首先,您应该了解源代码并未规定汇编语言是什么。 C标准允许编译器以任何方式转换程序,只要生成的可观察行为(由标准定义)保持不变即可。 (可观察行为主要是文件和设备的输出,交互式输入和输出,以及对特殊易失性对象的访问。)
编译器利用此规则来优化您的程序。如果你的循环结果在任何一个方向都相同,那么,在最好的编译器中,在一个方向或另一个方向上写循环没有任何后果。编译器分析源代码并发现循环的影响仅仅是执行一组顺序无关紧要的操作。它抽象地表示循环及其中的操作,然后生成最佳的汇编代码。
如果示例中的数组很大,那么编译器执行循环控制指令所花费的时间就无关紧要了。在典型的系统中,从内存中获取值需要数十个CPU周期或更多。对于大型数组,示例代码中的瓶颈将是从内存中获取数据。 CPU将被强制等待这些数据,并且在等待内存中的数据时,它将很容易地完成任何循环控制或数组地址算术指令。
典型的系统通过包含一些称为缓存的快速内存来处理慢速内存问题。通常,处理器本身的核心内置了非常快的缓存,加上处理器芯片上的一些快速缓存,还有其他级别的缓存。高速缓存中的内存被组织成行,它们是来自内存的连续数据段。因此,一个高速缓存行可以包含八个连续的int
个对象。当处理器需要尚未在高速缓存中的数据时,将从内存中提取整个高速缓存行。因此,您可以使用八个连续的int
对象来避免内存延迟。当您读取第一个(或甚至之前 - 处理器可能预测您的读取并提前开始提取它)时,所有八个都将从内存中准备就绪。所以你的程序只需要等待第一个程序。当它使用第二个到第八个时,它们将已经在缓存中,它们可以立即供处理器使用。
不幸的是,数组乘法对于缓存而言是非常糟糕的。虽然你的循环遍历数组A
的行(使用A[i][k]
,其中k
是编写代码时变化最快的索引),但它遍历B
的列(使用B[k][j]
)。因此,循环的连续迭代使用A
的连续元素,但不使用B
的连续元素。如果数组很大,程序将最终等待从内存中取出B
的元素。并且,如果您更改代码以使用B
中的连续元素,则它不再使用A
中的连续元素。
使用数组乘法,处理此问题的典型方法是将数组乘法拆分为较小的块,一次只执行一部分,可能是8×8块。这是有效的,因为缓存一次可以容纳多行。如果您安排工作,以便从B
(例如,所有元素的行号从16到23,列号从32到39)使用一个8×8块反复一段时间,然后它可以保留在缓存中,其所有数据立即可用。这种重新安排的工作可以极大地加速你的程序,使其速度提高很多倍。它比仅仅改变循环的方向可以提供更大的改进。
有些编译器可以看到i
,j
和k
上的循环可以互换,如果有一些好处,他们可能会尝试重新组织它们。如上所述,很少有编译器可以将例程分解为块。此外,编译器只能重新排列示例中的工作,因为您将A
,B
和C
声明为单独的数组。如果这些对编译器不可见,而是作为指向执行矩阵乘法的函数的指针传递,则编译器将无法看到A
,B
和C
指向单独的数组。在这种情况下,它不能知道循环的顺序无关紧要。如果函数传递的C
指向与A
相同的数组,则函数将在计算输出时覆盖其部分输入,因此循环方向很重要。
有多种矩阵乘法库使用阻塞技术和其他矩阵乘法库有效地执行矩阵乘法。