(学习笔记)3.8 指针运算(3.8.5 变长数组)

文章目录


线索栏

  1. 核心特性:ISO C99引入的"变长数组"(VLA)与传统的"定长数组"最根本的区别是什么?
  2. 声明与求值:变长数组的维度何时确定?在函数声明中,维度参数(如 n)和数组参数(如 A[n][n])的顺序有何强制要求?为什么?
  3. 地址计算对比:访问变长数组元素 A[i][j]的地址计算公式,与定长数组有何本质相同之处?在机器指令实现上有何关键不同?(为何必须用imul而非 leaq优化?)
  4. 循环优化:在涉及变长数组的循环(如矩阵乘法)中,编译器能否进行优化?其优化策略与定长数组的"指针遍历"优化(图3-37)有何相似与不同?
  5. 伸缩值:在图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)的底层实现机制,及其在灵活性与性能之间与定长数组的微妙平衡。

  1. 公式统一,实现分化:变长与定长数组的元素寻址遵循相同的数学公式 x A + L ∗ ( C ∗ i + j ) x_A + L*(C*i + j) xA+L∗(C∗i+j)。核心区别在于,维度 C(n)是运行时变量而非编译时常数。这导致在计算 n*i时,无法使用编译时优化(如移位、加法),必须使用乘法指令,可能带来性能损失。
  2. 循环优化的持续性:尽管单次访问需要乘法,但编译器在处理规律性访问的循环时,依然能发挥强大优化能力。通过识别内存访问的固定步长(如 +4和+4n),并将其转换为指针算术,可以将乘法移出循环(预计算 4n),从而在循环体内仅用高效的加法指令。图3-38的优化与定长数组的优化(图3-37)在思想上同源。
  3. "伸缩值"的编译智慧:优化汇编中同时维护 n和4n是点睛之笔。它直观展示了C语言指针运算的"类型伸缩"特性在机器级的落实,也体现了编译器通过预计算循环不变量来提升性能的经典策略。

最终启示:变长数组提供了更高的编程灵活性,但其性能特征与定长数组有所不同。理解其底层实现有助于在"灵活性"与"确定性性能"之间做出明智选择。对于性能至关重要的多维数组运算,如果维度在编译时已知,使用定长数组(或宏定义)能让编译器进行更激进的优化;若维度必须在运行时确定,变长数组提供了语法上的便利,但需知晓其潜在开销,并依赖编译器在循环中的优化来弥补。

相关推荐
南境十里·墨染春水2 小时前
C++笔记 构造函数 析构函数 及二者关系(面向对象)
开发语言·c++·笔记·ecmascript
AI成长日志2 小时前
【笔面试算法学习专栏】堆与优先队列专题:数组中的第K个最大元素与前K个高频元素
学习·算法·面试
Dxy12393102162 小时前
Python如何删除文件到回收站
开发语言·python
AI-Ming2 小时前
程序员转行学习 AI 大模型: 踩坑记录,HuggingFace镜像设置未生效
人工智能·pytorch·python·gpt·深度学习·学习·agi
斌味代码2 小时前
RAG 实战:用 LangChain + DeepSeek 搭建企业私有知识库问答系统
开发语言·langchain·c#
途经六月的绽放2 小时前
常见设计模式及其应用示例
java·设计模式
REI-2 小时前
黑马点评项目启动
java·后端
编程之升级打怪2 小时前
当前的软件和硬件开发难题
c语言
talen_hx2962 小时前
《零基础入门Spark》学习笔记 Day 07
笔记·学习·spark