不要假设,要证明。
1. 数组
对于数组声明T A[N];
会产生两个效果:
- 内存中会分配一个L * N字节的连续区域,L为数据类型T的大小
- 引入标识符A,A可用于作为指向数组第一个元素的指针
对于数组访问A[i]
,若A
的地址存放于%rdx
,i
存放于%rcx
则指令movl (%rdx, %rcx, 4), %eax
能访问int类型数组第i个元素。
指针运算
- 设指针
p1
、p2
为指向类型T
的指针,的值分别为xp1
、xp2
p + i
结果为xp + i * L
p1 - p2
结果为(xp1 - xp2) / L
: 指针的差结果类型为long,值等于两个地址之差除以数据类型的大小
嵌套数组
对于嵌套数组,机器级的实现仍使用连续的内存区域。数组元素按照行优先的顺序进行排列。这种排列顺序说明嵌套数组和嵌套声明等价。即int A[5][3]
和下面的语句等价。
1 |
|
对于嵌套数组元素访问的实现,对于T D[R][C];
,数组元素D[i][j]
的内存地址为D + L * ( i * n + j)
。例如对于A[5][3]
1 |
|
1.1. 定长/变长数组机器级实现的优化
- 定长数组
- 使用指针替代
i, j
作为循环变量,并用指针加法替代索引访问 - 循环的结束点使用一个指针固定值
- 使用指针替代
- 变长数组:从ISO C99开始引入了变长数组的功能,即数组可以在运行期指定长度
T A[expr1][expr2];
。变长数组能够作为局部变量或是函数参数(传指针)。
2. 结构体和联合的机器级实现
2.1. 结构
- 按照内存地址自低到高结构体内部的各个成员按序占据着单个内存块
- 编译器维护结构每个成员的类型信息,这些类型信息指示着字段偏移。通过将偏移作为内存引用指令中的位移,从而可以产生对结构元素的引用。
1 |
|
若%rdi
中保存r
的指针,%rsi
中保存了i
。访问a[1]
的指令leaq 8(%rdi, %rsi, 4), %rax
而对于访问r->p = r->a[r->i + r->j]
,则需要
1 |
|
2.1.1. 对齐
对齐的目的
- 避免低效的对跨cache行(64字节)的数据块的读写
- 避免低效的跨内存页数据的访问
对齐原则
- 结构体内部:
- 满足每个元素的对齐要求
- 任何K字节的基本对象的地址必须是K的倍数
- 汇编代码可以使用命令指明全局数据的对齐
.align 8
,则该命令之后的数据均按照8字节对齐
- 满足每个元素的对齐要求
- 整个结构体:
- 每个结构体对象都有对齐要求K
- K = Largest alignment of any element
- 初始地址和结构长度必须是K的倍数
- 每个结构体对象都有对齐要求K
1 |
|
例子:对于下面的结构体数组的访问
1 |
|
则a[idx].j
能被翻译为
1 |
|
2.2. 联合
联合绕过了类型系统提供的安全措施,允许使用多种类型引用同一个内存块。
节省内存
例如: 对于一个表示二叉树节点的结构体。对于叶子节点,有double类型的数据;对于内部节点,有左右指针。则可以用下面方式表示以节省内存:
1 |
|
访问不同数据类型的位模式
试想,如果我们想用unsigned long
类型的位模式解释一个double
类型的数据。
强制类型转换无法达到目的:
1 |
|
但是可以使用联合达到要求:
1 |
|
注:当使用联合表示多种大小不同的数据时,字节顺序会影响到结果。
1 |
|
对于大端法,高位字节位于u[0]
。对于小端法,低位字节位于u[0]
。
3. 浮点数指令
3.1. 历史
- x87 FP
- SSE FP (Stream SIMD Extension)
- 寄存器为16字节
- AVX FP (Advanced Vector Extension)
- 最新版本
- 寄存器为32字节
3.2. 浮点数指令基础
- 寄存器被称为MM,
XMM
对应低16字节,YMM
对应这个32字节。共16个。 - 既可以当作标量使用,也可以作为表示数组的向量使用(SIMD)
- 后缀
- sd,single double
- ss, single float
- 过程调用
- 所有XMM寄存器都是调用者保存
- 参数通过
%xmm0, %xmm1
传递 - 返回值通过
%xmm0
返回
- 内存引用
- 整数和指针通过通过寄存器传递
- 浮点数通过XMM寄存器传递
- 不同的mov指令在xmm寄存器间和xmm寄存器和内存间移动