C/C++ Memory Alignment

C/C++ 的記憶體排列的一些細節

·

2 min read

起因

x86/ARM 上,變數在記憶體上並不是起始於隨意的位置。而會是該類類型大小的整數倍,這樣讀取資料(變數)可以比較快。

為什麼不是隨意 bytes 位置

CPU 通常是讀 Word (4/8 bytes),假如是隨意的 bytes 位置,那就有可能為了讀一個uint16_t (2 bytes) 去讀兩個 words,會比較慢。

所以有時會在「連續」的變數之間出現 gap。

uint32_t a;  // 4 bytes  
char gap[N]; // N bytes  
uint64_t b;  // 8 bytes

這個 N ,可以是 0 或 4(因為uint32_t只要求起始於 4 的倍數的位置)。 另外,gap 的值並沒有保證是 0。

Struct (Not c++ class)

通常一個 Struct 的 Alignment (起始位置是 k倍數) 會去考慮裡面最寬的成員。

  • k 是最寬的寬度
  • Struct 的起始位置會是 k倍數
  • 尾巴也有可能補足 bytes 使得整個 Struct 大小是 k倍數

主要原因是考量到 Struct 被作為陣列使用時,每個成員的對齊仍然需要滿足前面所說的條件。

Struct Example

首先來看一個範例:

struct bar {  
    char c;      // 1 byte  
    uint64_t v1; // 8 bytes  
    uint16_t v2; // 2 bytes  
};

struct bar 會是 8-byte alignment 的(位置是 8 的倍數),因為裡面有 uint64_t

並且 padding 會長得像是這樣,因為 uint64_t 是 8-byte alignment。

由於 Struct 本身是 8-byte alignment,所以尾巴會有 6 bytes。

struct bar {  
    char c;         // 1 byte  
    char gap[7];    // 7 bytes  
    uint64_t v1;    // 8 bytes  
    uint16_t v2;    // 2 bytes  
    char unused[6]; // 6 bytes   
};

假如是一個 2-byte alignment 的 struct:

struct foo {  
    uint16_t a;  // 2 bytes  
    char c;      // 1 byte  
    char pad[1]; // 1 byte  
}

重排成員

適當重排 Struct 成員可以減少 Struct 的 padding。

本來 Padding 用掉: 7 + 6 = 13

struct bar {  
    char c;      // 1 byte  
    uint64_t v1; // 8 bytes  
    uint16_t v2; // 2 bytes  
};

調換成員的位置後Padding: 5

struct bar {  
    uint64_t v1; // 8 bytes  
    uint16_t v2; // 2 bytes  
    char c;      // 1 byte  
};

對 Struct 重排成員是也有著壞處,程式也是要給人看的,這可能會影響到可讀性。

效能也可能有影響, 本來擺在一起的成員是紀錄同一個功能的, 快取本來可以處理到這塊,但分開後快取效果就消失了。

Packed Struct

不過 c/c++ 並不是沒有把 padding 排除的選項,下編譯參數或者使用巨集都可以讓這 Padding 消失。

struct __attribute__((__packed__)) foo3 {  
    char c;  
    uint64_t v1;  
    uint16_t v2;  
};  
// sizeof(foo3) == 9

測試

  • offsetof(TYPE, MEMBER) 可以取得 type 裡面 member 的位置。
  • sizeof

C/C++

#include <iostream>

struct foo1 {  
    char c;  
    uint64_t v1;  
    uint16_t v2;  
};

struct foo2 {  
    uint64_t v1;  
    uint16_t v2;  
    char c;  
};

int main() {
    std::cout << "foo1:" << sizeof(foo1) << "\\\\n";  
    std::cout << "foo2:" << sizeof(foo2) << "\\\\n";

    std::cout << "offset of foo1::c: " << offsetof(foo1, c) << "\\\\n";  
    std::cout << "offset of foo1::v1: " << offsetof(foo1, v1) << "\\\\n";  
    std::cout << "offset of foo1::v2: " << offsetof(foo1, v2) << "\\\\n";

    std::cout << "offset of foo2::v1: " << offsetof(foo2, v1) << "\\\\n";  
    std::cout << "offset of foo2::v2: " << offsetof(foo2, v2) << "\\\\n";  
    std::cout << "offset of foo2::c: " << offsetof(foo2, c) << "\\\\n";

    return 0;  
}

Golang❓

不支援 packed struct,請參考 cgo#struct-alignment-issues

參考