文章目录
- 线索栏
- 笔记栏
-
- [1. 变长数组的定义与声明](#1. 变长数组的定义与声明)
- [2. 基础访问机制:与定长数组的对比](#2. 基础访问机制:与定长数组的对比)
- [3. 循环中的优化:以矩阵乘法为例](#3. 循环中的优化:以矩阵乘法为例)
- [4. 汇编代码印证与"伸缩值"概念](#4. 汇编代码印证与“伸缩值”概念)
- 总结栏
线索栏
- 核心特性:ISO C99引入的"变长数组"(VLA)与传统的"定长数组"最根本的区别是什么?
- 声明与求值:变长数组的维度何时确定?在函数声明中,维度参数(如 n)和数组参数(如 A[n][n])的顺序有何强制要求?为什么?
- 地址计算对比:访问变长数组元素 A[i][j]的地址计算公式,与定长数组有何本质相同之处?在机器指令实现上有何关键不同?(为何必须用imul而非 leaq优化?)
- 循环优化:在涉及变长数组的循环(如矩阵乘法)中,编译器能否进行优化?其优化策略与定长数组的"指针遍历"优化(图3-37)有何相似与不同?
- 伸缩值:在图3-38的优化汇编代码中,为何需要同时维护 n和 4n这两个值?它们分别用于什么目的?这揭示了指针运算的什么本质?
笔记栏
1. 变长数组的定义与声明
(1)定义:数组的维度是表达式,在数组被分配时(如函数调用时)才计算得出,而非编译时固定。
(2)声明示例:int A[expr1][expr2];
(3)函数参数中的声明:
c
int var_ele(long n, int A[n][n], long i, long j) {
return A[i][j];
}
(4)关键顺序:维度参数 n必须在数组参数 A[n][n]之前声明,以便在解析数组类型时能确定其维度 n。
2. 基础访问机制:与定长数组的对比
1)地址公式(本质相同)
对于 int A[n][n],元素 A[i][j]的地址为:
&A[i][j] = x A + 4 ∗ ( n ∗ i + j ) x_A + 4*(n * i + j) xA+4∗(n∗i+j)
这与定长数组公式 x D + L ∗ ( C ∗ i + j ) x_D + L*(C*i + j) xD+L∗(C∗i+j)完全一致,其中 C = n, L = 4。
2)指令实现(关键不同)
(1)定长数组(如A[5][3]):n(即C=3)是编译时常数,可用 leaq (%rsi, %rsi, 2), %rax(计算3i)等移位加法指令高效计算 n*i。
(2)变长数组:n是运行时变量,必须使用乘法指令 imulq来计算 n * i。
c
# var_ele 函数的汇编代码 (n in %rdi, A in %rsi, i in %rdx, j in %rcx)
imulq %rdx, %rdi # 计算 n * i, 结果在 %rdi
leaq (%rsi,%rdi,4), %rax # 计算 x_A + 4*(n*i)
movl (%rax,%rcx,4), %eax # 读取 M[x_A + 4*(n*i) + 4j]
ret
(3)性能启示:在一些处理器上,乘法指令开销较大,但这是实现变长数组灵活性所不可避免的代价。
3. 循环中的优化:以矩阵乘法为例

尽管每次计算 n*i需要乘法,但在规律访问的循环中,编译器仍能进行重要优化。
1)原始C代码 (var_prod_ele)
计算矩阵 A和 B乘积的元素 (i, k)。内层循环对 j求和:result += A[i][j] * B[j][k]。
2)编译器优化分析
(1)识别步长规律:
①A[i][j]的访问:在循环中,j每次+1,地址固定增加 4字节(int大小)。
②B[j][k]的访问:在循环中,j每次+1,地址增加 4 * n字节(跳过一整行)。
(2)优化策略:与定长数组优化类似,将二维数组访问转换为一维指针遍历,避免在循环内部重复计算乘法 n*j。
①引入 Arow指针,指向 A[i][0],每次循环 +4。
②引入 Bptr指针,指向 B[0][k],每次循环 + 4*n。
优化后的C代码 (var_prod_ele_opt) 清晰体现了上述指针遍历思想。
4. 汇编代码印证与"伸缩值"概念
优化后循环的汇编代码及其寄存器使用,完美印证了优化策略:
c
# 寄存器: n in %rdi, Arow in %rsi, Bptr in %rcx, 4n in %r9, result in %eax, j in %edx
.L24: # loop:
movl (%rsi,%rdx,4), %r8d # 读取 Arow[j] (地址: Arow + 4*j)
imull (%rcx), %r8d # 乘以 *Bptr
addl %r8d, %eax # 加到 result
addq $1, %rdx # j++
addq %r9, %rcx # Bptr += 4n (关键!%r9中存的是4n)
cmpq %rdi, %rdx # 比较 j 和 n
jne .L24 # 若不等,继续循环
1)关键洞察
编译器同时维护了 n(在 %rdi) 和 4n(在 %r9) 两个值。
(1)n(%rdi) :用于循环边界检查 (cmpq %rdi, %rdx)。
(2)4n(%r9) :用于指针步进 (addq %r9, %rcx),因为C语言指针运算 Bptr += n会根据 int类型自动伸缩,实际地址增量是 4*n字节。
2)"伸缩值"的意义
这揭示了高级语言指针运算的底层实质。Bptr += n并非地址加 n,而是加 n * sizeof(int)。编译器将此伸缩因子预先计算好(4n),在循环中直接使用,避免了每次循环都做一次乘法,这是重要的优化。
总结栏
本节揭示了变长数组(VLA)的底层实现机制,及其在灵活性与性能之间与定长数组的微妙平衡。
- 公式统一,实现分化:变长与定长数组的元素寻址遵循相同的数学公式 x A + L ∗ ( C ∗ i + j ) x_A + L*(C*i + j) xA+L∗(C∗i+j)。核心区别在于,维度 C(n)是运行时变量而非编译时常数。这导致在计算 n*i时,无法使用编译时优化(如移位、加法),必须使用乘法指令,可能带来性能损失。
- 循环优化的持续性:尽管单次访问需要乘法,但编译器在处理规律性访问的循环时,依然能发挥强大优化能力。通过识别内存访问的固定步长(如 +4和+4n),并将其转换为指针算术,可以将乘法移出循环(预计算 4n),从而在循环体内仅用高效的加法指令。图3-38的优化与定长数组的优化(图3-37)在思想上同源。
- "伸缩值"的编译智慧:优化汇编中同时维护 n和4n是点睛之笔。它直观展示了C语言指针运算的"类型伸缩"特性在机器级的落实,也体现了编译器通过预计算循环不变量来提升性能的经典策略。
最终启示:变长数组提供了更高的编程灵活性,但其性能特征与定长数组有所不同。理解其底层实现有助于在"灵活性"与"确定性性能"之间做出明智选择。对于性能至关重要的多维数组运算,如果维度在编译时已知,使用定长数组(或宏定义)能让编译器进行更激进的优化;若维度必须在运行时确定,变长数组提供了语法上的便利,但需知晓其潜在开销,并依赖编译器在循环中的优化来弥补。