CSAPP-note-4
Arrays
Array Allocation
Basic Principle:T A[L];
- Array of data type T and length L
- Contiguously allocated region of L*sizeof(T) bytes in memory
Array Access
1 | int val[5] = {1, 5, 2, 1, 3}; |
表达式 | 类型 | 值或含义 |
---|---|---|
val[4] | int | 3 |
val | int * | x |
val+1 | int * | x+4 |
&val[2] | int * | x+8 |
val[5] | int | ?? |
*(val+1) | int | 5 |
val + i | int * | x+4*i |
Example
1 |
|
Array Accessing Example
1 | int get_digit(zip_dig z, int digit) { |
传入zip,获取zip的第digit位,对应的汇编代码为
1 | get_digit: |
%rdi是数组的首地址,%rsi是数组的下标。int的大小为4个字节,所以第%rsi个元素对应的内存地址为%rdi+4*%rsi.
Array Loop Example
1 | void zincr(zip_dig z) { |
遍历数组每一个元素,并使其加一。
1 | movl $0, %eax # i = 0 |
Multidimensional Arrays
Declaration T A[R][C];
- 2D array of data type T
- R rows, C columns
Array Size RCsizeof(T) bytes
example
1 |
|
zip_dig pgh[4]等价于int pgh[4][5]。
Nested Array Row Access
A[i] is array of C elements of type T.Starting address A + i * (C * sizeof(T))
1 | get_pgh_zip: |
A[i][j] is element of type T, which requires K bytes
Address A + i * (C * K) + j * K = A + (i*C + j) * K
1 | get_pgh_digit: |
Array Elements
- pgh[index][dig] is int
- Address: pgh + 20index + 4dig = pgh + 4*(5*index + dig)
Multi-Level Array example
1 |
|
Variable univ denotes array of 3 elements.Each element is a pointer 8 bytes.Each pointer points to array of int’s.
1 | get_univ_digit: |
Computation
- Element access Mem[Mem[univ+8index]+4digit]
- Must do two memory reads
- First get pointer to row array
- Then access element within array
NxN Matrix Code
Fixed dimensions.Know value of N at compile time.
1 |
|
Variable dimensions, explicit indexing.
Traditional way to implement dynamic arrays
1 |
|
Variable dimensions, implicit indexing.Now supported by gcc.
1 | int var_ele(size_t n, int A[n][n], size_t i, size_t j) { |
Structures
结构体的声明
1 | struct rec { |
结构体可以被看作是一个能足够装下内部所有成员的内存块。
结构体内部成员在内存中的存储顺序和声明顺序一致,即使有另外一个顺序可以让结构体的布局更紧凑。
编译器确定了结构体的整体大小和字段的位置,机器级程序对源代码中的结构体没有任何理解。
Linked List Loop
Example 1
1 | long length(struct rec*r) { |
上面是计算一个链表长度的C程序,下面是其对应的汇编代码。注意到每次从结构体取出下一个结点的指针时,我们访问的是结构体首地址+24处的内存。原因是?内存对齐,我们会在稍后对其进行详细探讨。
1 | .L3: |
Example 2
让我们再来看一个链表赋值的例子,以加深对结构体成员访问的理解。
1 | void set_val(struct rec*r, int val) { |
这两个例子我们能看出,读取结构体内部成员的内存,就是使用结构体首地址+字段偏移。
1 | .L3: |
内存对齐
1 | struct S1 { |
猜测一下上面这个结构体的size?C语言初学者可能会以为是1+8+8=17。实际上,sizeof(struct S1)的值是24。为什么会这样?大小为B个字节的原始数据类型的首地址必须是B的整数倍。
在这个例子里,char为1字节,而int是4字节,所以char后面有3个字节填充。同样地,double为8字节,而前面是4+4+4=12,所以还需要填充4字节。总共有24字节。
对齐原则
- 对齐数据
- 原始数据类型需要B字节
- 地址必须是B的倍数
- 在某些机器上是必需的;在x86-64上是推荐的
- 为什么要内存对齐?
- 现代计算机架构的硬件被设计成以固定大小的块来访问内存,通常是4字节或8字节(取决于系统)。这意味着读取或写入没有对其到这些边界的数据可能导致多次内存访问,从而减慢性能。
- 内存换成被组织成缓存行,通常长64字节。访问跨越缓存行的数据会导致效率低下,因为可能加载或存储多个缓存行。为了避免跨越缓存行边界,建议将数据至少对齐到16字节边界。这有助于最小化需要访问的缓存行数量,从而提高整体的内存访问效率。
- 虚拟内存系统使用页面来管理内存分配,如果一块数据跨越了两个虚拟内存页面,访问它可能触发多次内存访问,由于额外的页面查找而减慢性能。
常见的对齐情况(x86-64)
1 byte: char, … 没有要求
2 bytes: short, …地址的最低位必须是0
4 bytes: int float, … 地址的低两位是0
8 bytes: double, long, char *, … 地址的低三位是0
结构体对齐规则
在结构体内部:必须满足每个元素的对齐要求
结构体整体:每个结构体都要对齐结构体内部元素的最大的对齐值K。初始地址和结构体长度必须是K的倍数。
节省空间
方法:定义结构体时,把大的数据类型放在前面。
1 | struct S1 { |