文章目录
- 线索栏
- 笔记栏
-
- [1. 内存布局:行优先顺序](#1. 内存布局:行优先顺序)
- [2. 数组元素地址计算(通用公式)](#2. 数组元素地址计算(通用公式))
- [3. 优化示例1:矩阵乘法元素计算 (fix_prod_ele)](#3. 优化示例1:矩阵乘法元素计算 (fix_prod_ele))
- [4. 优化示例2:设置矩阵对角线 (fix_set_diag)](#4. 优化示例2:设置矩阵对角线 (fix_set_diag))
- 5.练习题
-
- [1)练习题3.38 解答(逆向推导M, N)](#1)练习题3.38 解答(逆向推导M, N))
- [2)练习题3.39 解答(验证优化代码的初始值)](#2)练习题3.39 解答(验证优化代码的初始值))
- [3)练习题3.40 解答(通用化的对角线设置优化代码)](#3)练习题3.40 解答(通用化的对角线设置优化代码))
- 总结栏
线索栏
- 内存布局:C语言声明 int A[5][3]在内存中如何布局?"行优先"顺序的具体含义是什么?
- 地址公式:对于一个声明为 T D[R][C]的数组,元素 D[i][j]的地址计算公式是什么?(公式3.1)
- 逆向推导:如何根据实现数组元素访问的汇编代码(sum_element函数),反向推导出数组的维度 M和 N?(练习题3.38)
- 编译器优化:对于定长多维数组(如 #define N 16),编译器可以进行哪两类关键优化来提升循环性能?(结合fix_prod_ele和 fix_set_diag示例)
- 在 fix_prod_ele_opt中,编译器如何将数组下标访问转换为指针遍历?指针 Aptr, Bptr, Bend分别是什么作用?
- 在 fix_set_diag的优化汇编中,地址增量 68和循环上限 1088是如何根据 N=16和int类型计算出来的?如何将其推广为含 N的通用表达式?(练习题3.40)
笔记栏
1. 内存布局:行优先顺序
(1)声明:int A[5][3];等价于定义一个包含5个元素的数组,每个元素是一个包含3个int的数组 (typedef int row3_t[3]; row3_t A[5]; )。
(2)大小:总大小 = 5×3×4=60字节。
(3)行优先存储:在内存中,先行后列。先连续存储第0行 (A[0][0], A[0][1], A[0][2]),接着是第1行,以此类推。如图3-36表格所示。
2. 数组元素地址计算(通用公式)
对于声明 T D[R][C],其中 L = sizeof(T),元素 D[i][j]的地址为:
& D [ i ] [ j ] = x D + L × ( C ⋅ i + j ) D[i][j]=x_D+L×(C⋅i+j) D[i][j]=xD+L×(C⋅i+j) (3.1)
(1)推导:要访问第 i行第 j列,需先跳过前面的 i整行(每行有 C个元素),再在该行内偏移 j个元素。
(2)示例(汇编实现):对于 int A[5][3](L=4, C=3),访问 A[i][j]。
地址 = x A + 4 × ( 3 i + j ) = x A + 12 i + 4 j x_A+4×(3i+j)=x_A+12i+4j xA+4×(3i+j)=xA+12i+4j
汇编代码(Ain %rdi, iin %rsi, jin %rdx):
c
leaq (%rsi,%rsi,2), %rax # 计算 3i
leaq (%rdi,%rax,4), %rax # 计算 x_A + 12i
movl (%rax,%rdx,4), %eax # 读取 M[x_A + 12i + 4j]
3. 优化示例1:矩阵乘法元素计算 (fix_prod_ele)
当数组维度是编译时常数(#define N 16)时,编译器可进行激进优化。
1)原始C代码
标准的三层循环,通过索引 A[i][j]和 B[j][k]访问。
2)编译器优化策略
(1)消除冗余地址计算:内层循环中,A[i][j]的地址每次增加 L(int为4),B[j][k]的地址每次增加 L*N(N=16时为64)。编译器将步进值预先算出。
(2)用指针遍历代替索引计算:
①Aptr初始指向 A[i][0],每次循环 +4。
②Bptr初始指向 B[0][k],每次循环 +64。
③Bend指向假想的 B[N][k]作为循环终止条件。
优化后的C代码 (fix_prod_ele_opt) 及对应汇编,清晰地展示了上述指针遍历过程。
4. 优化示例2:设置矩阵对角线 (fix_set_diag)
1)原始C代码
for(i=0; i<N; i++) A[i][i] = val;
2)编译器优化分析 (N=16, int类型)
(1)目标:依次设置 A[0][0], A[1][1], ..., A[15][15]。
(2)地址计算:元素 A[i][i]的地址 = x A + 4 × ( 16 ⋅ i + i ) = x A + 4 × 17 i = x A + 68 i x_A+4×(16⋅i+i)=x_A +4×17i=x_A +68i xA+4×(16⋅i+i)=xA+4×17i=xA+68i
(3)汇编代码解读 (Ain %rdi, valin %esi):
c
movl $0, %eax # i = 0
.L13:
movl %esi, (%rdi,%rax) # A[i][i] = val。%rax 初始为0,每次增加68
addq $68, %rax # i++ 等价于地址 +68
cmpq $1088, %rax # 比较地址增量是否达到 68 * 16 = 1088
jne .L13 # 若未达到,继续循环
5.练习题
1)练习题3.38 解答(逆向推导M, N)

(1)已知:long P[M][N];, long Q[N][M];。函数 sum_element返回 P[i][j] + Q[j][i]。其汇编代码计算了两个地址。
(2)分析汇编:
计算 P[i][j]的地址偏移 (%rdx):指令 leaq 0(,%rdi,8), %rdx; subq %rdi, %rdx; addq %rsi, %rdx等价于 %rdx = 8i - i + j = 7i + j。最后用 P(,%rdx,8)寻址,所以 P的元素大小为8,总偏移为 8*(7i+j)。根据公式(3.1),P的步长应为 8N。因此 8N = 56=> N = 7。
计算 Q[j][i]的地址偏移 (%rdi):指令 leaq (%rsi,%rsi,4), %rax; addq %rax, %rdi等价于 %rdi(原为i) 被更新为 i + 5j。最后用 Q(,%rdi,8)寻址,总偏移为 8*(i+5j)。根据公式(3.1),Q的步长应为 8M。因此 8M = 40=> M = 5。
(3)答案:M = 5, N = 7。

2)练习题3.39 解答(验证优化代码的初始值)

(1)目标:用公式(3.1)解释 fix_prod_ele_opt中 Aptr, Bptr, Bend的初始化。
(2)推导:
Aptr = &A[i][0] = x A + 4 ∗ ( 16 ∗ i + 0 ) = x A + 64 i x_A + 4*(16*i + 0) = x_A + 64i xA+4∗(16∗i+0)=xA+64i(汇编第3行 leaq (%rdi,%rsi,64), %rax将 x_A和 64i结合)。
Bptr = &B[0][k] = x B + 4 ∗ ( 16 ∗ 0 + k ) = x B + 4 k x_B + 4*(16 * 0 + k) = x_B + 4k xB+4∗(16∗0+k)=xB+4k。
Bend = &B[N][k] = x B + 4 ∗ ( 16 ∗ N + k ) = x B + 4 k + 64 N x_B + 4*(16*N + k) = x_B + 4k + 64N xB+4∗(16∗N+k)=xB+4k+64N。由于 Bptr初值为 x B + 4 k x_B+4k xB+4k,所以 Bend = Bptr + 64N。N=16,所以 Bend = Bptr + 1024。汇编第5行 leaq 1024(%rcx), %rsi正是此计算。

3)练习题3.40 解答(通用化的对角线设置优化代码)

c
void fix_set_diag_opt(fix_matrix A, int val) {
int *Aptr = &A[0][0];
int *Aend = Aptr + N * (N + 1); // 或 Aptr + N*N + N
int stride = N + 1; // 相邻对角线元素之间的地址增量(以int为单位)
do {
*Aptr = val;
Aptr += stride;
} while (Aptr != Aend);
}
说明:Aptr初始指向 A[0][0]。每次循环后,Aptr增加 (N+1)个 int的长度,从而指向下一个对角线元素 A[i][i]。循环直到 Aptr达到假想的终点 A[N][N](即 A[0][0] + N*(N+1)的位置)。

总结栏
本节深入探讨了多维数组的存储、访问和编译器优化,是理解高效数值计算的基础。
- 行优先是根本:C语言多维数组在内存中按"行优先"连续存储。元素 D[i][j]的地址公式 &D[i][j] = x D + L ∗ ( C ∗ i + j ) x_D + L*(C*i+ j) xD+L∗(C∗i+j)是理解所有相关操作(访问、优化、逆向工程)的基石。
- 定长带来优化:当数组维度 (N) 是编译时常数时,编译器可以进行激进优化:
(1)消除冗余计算:将循环中的乘法、加法等地址计算,转换为简单的常量步进(如 fix_set_diag中的 68)或指针算术(如 fix_prod_ele中的指针遍历)。
(2)指针遍历替代索引:用移动指针(*Aptr++)代替通过公式计算地址(A[i][j]),大幅减少指令数和计算量。这是编译器优化循环的经典手段。 - 从实例学习推理:练习题3.38是逆向工程的典范,要求从看似复杂的地址计算中反推出数组维度。核心是识别汇编代码中的地址计算模式,并使其与通用公式(3.1)匹配。练习题3.39和3.40则训练了正向理解和应用优化思想的能力。
最终启示:多维数组的高效使用,既需要程序员理解其内存布局(避免缓存不友好的访问模式),也依赖于编译器对定长情况的深度优化。在性能关键代码中,有时手动进行类似的指针优化是必要的。理解本节内容,是进行矩阵运算、图像处理等科学计算编程的重要前提。