陣列與異質資料結構 (Arrays & Heterogeneous Data Structures)

Overview Table

小節 主題 核心公式 / 重點
3.8.1 (p.291) 陣列基本原理 T A[N]L*N bytes;&A[i] = xA + L*i
3.8.2 (p.293) 指標算術 p+i 的值為 xp + L*i;A[i] ≡ *(A+i);指標相減得元素個數差
3.8.3 (p.294) 巢狀(多維)陣列 Row-major order;&D[i][j] = xD + L*(C*i + j)(Eq. 3.1)
3.8.4 (p.296) 固定大小陣列最佳化 編譯器消去索引 j,改用指標遞增 + 終止指標(Bend)
3.8.5 (p.298) 可變大小陣列 (VLA, C99) &A[i][j] = xA + L*(n*i + j),需 imulq 計算 n*i
3.9.1 (p.301) Structures 欄位以固定 byte offset 存取,連續配置;機器碼不含欄位名稱
3.9.2 (p.305) Unions 所有欄位共用同一塊記憶體;大小 = 最大欄位大小;可繞過型別系統
3.9.3 (p.309) Data Alignment K-byte 基本物件位址須為 K 的倍數;struct 內部與尾端補 padding

3.8.1 陣列配置與存取基本原理 (p.291)

宣告 T A[N](元素型別 T 大小為 L bytes)有兩個效果:配置一塊 連續L*N bytes 記憶體;引入識別字 A 作為指向陣列開頭的指標,其值為起始位址 xA。元素 i(範圍 0 到 N−1)的位址為:

&A[i]=xA+Li

x86-64 記憶體定址模式 Imm(rb, ri, s) 就是為陣列存取設計的:若 xE%rdx、索引 i%rcx,則 movl (%rdx,%rcx,4),%eax 一條指令完成 xE + 4i 位址計算 + 讀取。scale factor 只能是 1, 2, 4, 8,恰好對應常見基本型別大小。

是 5 個「指標」(各 8 bytes),不是 5 個 double。任何指標在 x86-64 都是 8 bytes。

3.8.2 指標算術 (Pointer Arithmetic) (p.293)

C 的指標算術會依「指向型別的大小 L」自動縮放:若 p 值為 xp,則 p+i 的值為 xp + L*i&Expr 取址、*AExpr 解參考,Expr*&Expr 等價;A[i]*(A+i) 完全相同。

int E[](xE%rdxi%rcx)為例:

Expression Type Value Assembly
E int * xE movl %rdx,%rax(書中原文;位址其實應以 8-byte 處理)
E[0] int M[xE] movl (%rdx),%eax
E[i] int M[xE + 4i] movl (%rdx,%rcx,4),%eax
&E[2] int * xE + 8 leaq 8(%rdx),%rax
E+i-1 int * xE + 4i − 4 leaq -4(%rdx,%rcx,4),%rax
*(E+i-3) int M[xE + 4i − 12] movl -12(%rdx,%rcx,4),%eax
&E[i]-E long i movq %rcx,%rax

3.8.3 巢狀陣列與 Row-Major Order (p.294)

int A[5][3] 等價於 typedef int row3_t[3]; row3_t A[5];——「5 個元素、每個元素是 3 個 int 的陣列」。總大小 = 4*5*3 = 60 bytes。元素在記憶體中依 row-major order(列優先)排列:先整個 row 0,再 row 1……

記憶體位址遞增 →
xA        xA+4      xA+8      xA+12     xA+16     xA+20     xA+24  ...  xA+56
+---------+---------+---------+---------+---------+---------+------ ... ------+
| A[0][0] | A[0][1] | A[0][2] | A[1][0] | A[1][1] | A[1][2] | A[2][0] ...A[4][2]|
+---------+---------+---------+---------+---------+---------+------ ... ------+
 \_______ row 0 ________/      \_______ row 1 ________/

對宣告 T D[R][C];,元素位址公式(Equation 3.1):

&D[i][j]=xD+L(Ci+j)

A[5][3]xA/i/j%rdi/%rsi/%rdx 為例,編譯器用 leaq 的 shift-add 能力算 3i,避免乘法:

leaq (%rsi,%rsi,2), %rax    # 3*i  (i + 2i)
leaq (%rdi,%rax,4), %rax    # xA + 12i
movl (%rax,%rdx,4), %eax    # M[xA + 12i + 4j]
公式中乘的是「行數 C(每列元素數)」而非列數 R。逆向工程時(如 Practice Problem 3.38 求 M、N),就是從組語中的縮放係數(如 8*(7i+j) → 每列 7 個 8-byte 元素)反推維度。

3.8.4 固定大小陣列的編譯器最佳化 (p.296)

#define N 16; typedef int fix_matrix[N][N]; 計算矩陣乘積的 (i,k) 元素 0j<Nai,jbj,k 為例,gcc -O1 做了以下轉換:消去索引變數 j,把所有陣列存取改為指標解參考:

A (row i 橫走)                B (column k 直走)
[ . . . . ]                   [ . k . . ]
[ i→→→→→ ]  Aptr++ (+4)       [ . ↓ . . ]  Bptr += N (+64)
[ . . . . ]                   [ . ↓ . . ]
[ . . . . ]                   [ . ↓ . . ]   ...直到 Bptr == Bend

對應組語核心:salq $6,%rdx(64i,因每列 16×4=64 bytes)、迴圈內 addq $4,%rdiaddq $64,%rcxcmpq %rsi,%rcx + jne。迴圈測試用 do-while + 指標比較,不需計數器。

3.8.5 可變大小陣列 (Variable-Size Arrays) (p.298)

歷史背景:早期 C 的多維陣列除第一維外,各維大小必須是編譯期常數;動態大小需自行用 malloc/calloc 配置一維陣列 + 手動套 Equation 3.1 做 row-major 索引。ISO C99 引入 VLA:int A[expr1][expr2] 可作為區域變數或函式參數,維度在宣告當下求值。

int var_ele(long n, int A[n][n], long i, long j) { return A[i][j]; }

參數 n 必須出現在 A[n][n] 之前,編譯器才能在遇到 A 時計算維度。產生的組語:

imulq %rdx, %rdi              # n * i   ← 必須用乘法指令!
leaq  (%rsi,%rdi,4), %rax     # xA + 4(n*i)
movl  (%rax,%rcx,4), %eax     # M[xA + 4(n*i) + 4j]

位址公式:&A[i][j]=xA+4(ni+j)

與固定大小的差異:(1) 暫存器配置因多了參數 n 而不同;(2) n*i 無法用 leaq shift-add,必須用 imulq,在某些處理器上有明顯效能代價。

迴圈中的 VLA 最佳化(Figure 3.38):gcc 仍能利用存取規律消去乘法——保留 j 作為迴圈計數兼 Arow[j] 索引,Bptr += n 直走欄;組語同時需要 4n(在 %r9,遞增 Bptr 用)與 n(在 %rdi,比較 j:n 用)兩個值,這在 C 原始碼中因指標算術自動縮放而看不出來。

固定大小 → 編譯器選了「純指標式」(Fig 3.37b);可變大小 → 選了「陣列+指標混合式」(Fig 3.38b)。書中強調這是編譯器選擇的產物 (artifact),不是兩種陣列的本質差異;兩者都成功避開了逐次乘法。

3.9.1 Structures (p.301)

struct 把不同型別的物件聚合成單一物件,以名稱存取欄位。實作上與陣列類似:所有欄位存於 連續記憶體,結構指標 = 第一個 byte 的位址。編譯器為每種 struct 型別維護各欄位的 byte offset,產生記憶體存取指令時以 offset 作為 displacement。

struct rec { int i; int j; int a[2]; int *p; };
Offset:   0       4       8       12      16              24
        +-------+-------+-------+-------+---------------+
        |   i   |   j   | a[0]  | a[1]  |       p       |
        +-------+-------+-------+-------+---------------+
          4B      4B      4B      4B          8B          總計 24 bytes

存取範例(r%rdi):

欄位選擇 完全在編譯期處理:機器碼中沒有任何欄位宣告或欄位名稱的資訊,只剩數字 offset。這也是逆向工程 struct 佈局(Practice Problems 3.41/3.42)的依據——如 movq 2(%rdi),%rdi 洩露了「offset 2 處是一個 8-byte 指標」→ 該 struct 是 {short v; struct ACE *p;} 構成的 linked list。

C 提供 -> 作為「解參考 + 選欄位」的簡寫:rp->width ≡ (*rp).width。注意 *rp.width 會被解讀為 *(rp.width),是錯的(p.302 New to C aside)。struct 是 C 中最接近 C++/Java 物件的機制,但方法只能寫成一般函式。

3.9.2 Unions (p.305)

union 語法與 struct 相同、語義完全不同:所有欄位 參考同一塊記憶體(offset 全為 0),用來繞過 C 的型別系統。union 總大小 = 最大欄位的大小(再考慮對齊)。

Type c i v Size
struct S3 {char c; int i[2]; double v;} 0 4 16 24
union U3 {char c; int i[2]; double v;} 0 0 0 8

用途一:節省空間(欄位互斥時)。二元樹節點:leaf 存 2 個 double、internal 存 2 個子指標,兩者互斥:

typedef enum { N_LEAF, N_INTERNAL } nodetype_t;
struct node_t {
    nodetype_t type;         /* 4 bytes + 4 bytes padding */
    union { struct {...} internal; double data[2]; } info;  /* 16 bytes */
};                           /* 總計 4 + 4 + 16 = 24 bytes */

用途二:存取不同型別的位元模式(bit-level reinterpretation):

unsigned long double2bits(double d) {
    union { double d; unsigned long u; } temp;
    temp.d = d;
    return temp.u;   /* u 與 d 位元完全相同(含 sign/exponent/significand) */
}

與 cast (unsigned long) d 對比:cast 做「數值轉換」、union 做「位元重解讀」;兩者結果除 d == 0.0 外完全不同。

Byte ordering 陷阱:用 union 組合不同大小的型別時,端序 (endianness) 會影響結果。uu2double(word0, word1) 在 little-endian(x86-64)上 word0 是 double 的低 4 bytes;在 big-endian 機器上兩參數角色對調(p.308)。可連結 02-Information-Representation/01-Information-Storage-and-Bits

3.9.3 Data Alignment (p.309)

許多系統要求基本型別的位址必須是某個值 K 的倍數(對齊限制),以簡化處理器—記憶體介面硬體:若 double 保證 8-byte 對齊,一次 8-byte 記憶體操作即可讀寫;否則可能跨越兩個 8-byte block、需要兩次存取。

x86-64 無論對齊與否都能正確運作,但 Intel 建議對齊以提升效能。規則:任何 K bytes 的基本物件,其位址必須是 K 的倍數

K Types
1 char
2 short
4 int, float
8 long, double, char *(所有指標)
struct S1 { int i; char c; int j; };     — 內部 padding
Offset:  0        4  5        8        12
       +--------+--+########+--------+
       |   i    |c | 3B gap |   j    |     總大小 12(非 9)
       +--------+--+########+--------+     j 需 4-byte 對齊 → offset 8

struct S2 { int i; int j; char c; };     — 尾端 padding
Offset:  0        4        8  9       12
       +--------+--------+--+########+
       |   i    |   j    |c | 3B pad |     總大小 12(非 9)
       +--------+--------+--+########+
       否則 struct S2 d[4] 的元素位址 xd+9, xd+18, xd+27 無法全部對齊

計算 struct 佈局的通用流程:

逐欄位:offset = 「目前位置向上取整到該欄位對齊 K 的倍數」
             ↓
整體對齊 = max(所有欄位的 K)
             ↓
總大小   = 最後欄位結尾向上取整到「整體對齊」的倍數
強制對齊的例外 (p.312 Aside):一般 x86-64 指令不對齊只是變慢,但部分 SSE 指令 要求 16-byte 對齊,違反會觸發 exception 並使程式終止。因此 malloc/calloc/realloc/alloca 回傳的區塊必須是 16 的倍數,且多數函式的 stack frame 須 16-byte 對齊(見 03-Machine-Level-Programs/04-Procedures-and-the-Stack)。較新的 AVX 指令則無強制對齊要求。

Exam/Test Patterns

情境 / 關鍵字 答案要點
給宣告求「元素大小/總大小/元素 i 位址」(PP 3.36) 元素大小看「元素型別」;指標一律 8B;&A[i] = xA + L*i
給指標運算式求 type/value/assembly(PP 3.37) 指標算術自動 ×L;結果是指標→leaq+%rax;是資料→mov+對應大小暫存器
組語中出現 (%rdx,%rcx,4) 陣列索引:base + index×scale;scale = 元素大小 ∈
&E[i] - E 型指標相減 結果型別 long,值 = 位址差 ÷ L = 索引差
二維陣列逆向求維度 M、N(PP 3.38) 從縮放係數還原 Eq. 3.1:L*(C*i + j),係數/L = 每列元素數 C
組語有 imulq 算索引 可變大小陣列 (VLA):維度 n 非編譯期常數,無法用 leaq shift-add
迴圈用指標比較終止、無索引變數 固定大小陣列 -O1 最佳化:Aptr++/Bptr+=N/Bptr != Bend 模式
對角線走訪每次位址 +68 (N+1)*L:N=16、L=4 → 4*(16+1)=68;終點 4*16*17=1088
mov 帶常數 displacement 存取 %rdi struct 欄位存取:displacement = 欄位 offset(編譯期決定,無欄位名)
由組語 offset 反推 struct 佈局(PP 3.41/3.42) 依 mov 大小(movl→4B, movq→8B)+ offset 推欄位型別與 padding
struct vs union 同欄位 offset 比較(PP 3.43) struct:offset 累加(含 padding);union:全部 offset 0、大小取最大
求 struct 的 offset/總大小/對齊(PP 3.44) K-byte 物件位址須為 K 倍數;整體對齊 = max K;總大小補到其倍數
重排欄位使空間最小(PP 3.45) 依對齊需求(8→4→2→1)由大到小排列欄位
double ↔ 位元模式互轉 union 重解讀位元(值不變 bit pattern);cast 是數值轉換(bit pattern 改變)
union 塞兩個 unsigned 組 double 考 endianness:little-endian 上 u[0] 是低 4 bytes
SSE / 16-byte / exception SSE 記憶體運算元強制 16-byte 對齊,違反→exception;AVX 無此限制