首頁 >後端開發 >Golang >優化 Go 中的記憶體使用:掌握資料結構對齊

優化 Go 中的記憶體使用:掌握資料結構對齊

Barbara Streisand
Barbara Streisand原創
2024-11-16 09:54:03489瀏覽

記憶體最佳化對於編寫高效能軟體系統至關重要。當軟體可使用的內存量有限時,如果記憶體未有效利用,就會出現許多問題。這就是為什麼記憶體優化對於更好的整體效能至關重要。

Go 繼承了 C 的許多優勢特性,但我注意到,很大一部分使用它的人並不了解這種語言的全部功能。原因之一可能是缺乏對它在低水平上如何運作的了解,或者缺乏使用 C 或 C 等語言的經驗。我提到 C 和 C 是因為 Go 的基礎幾乎是建立在 C/C 的精彩功能之上的。我引用 Ken Thompson 在 Google I/O 2012 上的訪談絕非偶然:

對我來說,我對Go 充滿熱情的原因是因為就在我們開始Go 的同時,我閱讀(或嘗試閱讀)C 0x 提議的標準,這是一個令人信服的標準我。

今天,我們將討論如何最佳化 Go 程序,更具體地說,是如何在 Go 中使用結構體。我們先說一下什麼是結構體:

結構體是一種使用者定義的資料類型,它將不同類型的相關變數分組到一個名稱下。

為了充分理解問題所在,我們將提到現代處理器一次不會從記憶體中讀取 1 個位元組。 CPU如何取得儲存在記憶體中的資料或指令?

在電腦體系結構中,字是處理器可以在單一操作中處理的資料單元 - 通常是記憶體的最小可尋址單元。它是固定大小的位元組(二進制數字)。處理器的字長決定了其有效處理資料的能力。常見的字長包括 8 位元、16 位元、32 位元和 64 位元。一些電腦處理器架構支援半字(即一個字中位數的一半)和雙字(即兩個連續的字)。

現今最常見的架構是 32 位元和 64 位元。如果您有 32 位元處理器,則表示它一次可以存取 4 個位元組,這表示字元大小為 4 個位元組。如果你有 64 位元處理器,它一次可以存取 8 個字節,這意味著字大小為 8 個位元組。

當我們將資料儲存在記憶體中時,每個32位元資料字都有一個唯一的位址,如下所示。

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

圖。 1 ‑ 字可定址記憶體

我們可以讀取記憶體中的數據,並使用載入字(lw)指令將其載入到一個暫存器。

了解了上面的理論之後,我們來看看實踐是什麼。對於結構資料結構的描述,我將用C語言進行演示。 C 中的結構是一種複合資料類型,可讓您將多個變數組合在一起並將它們儲存在同一記憶體區塊中。正如我們之前所說,CPU 存取資料取決於給定的架構。 C 中的每種資料類型都有對齊要求。

所以我們有以下簡單結構:

// structure 1
typedef struct example_1 {
    char c;
    short int s;
} struct1_t;


// structure 2
typedef struct example_2 {
    double d;
    int s;
    char c;
} struct2_t;

現在試著計算以下結構的大小:

結構 1 的大小 = (char Short int) 的大小 = 1 2 = 3.

結構 2 的大小 = (double int char) 的大小 = 8 4 1= 13.

使用 C 程式的實際大小可能會讓您大吃一驚。

#include <stdio.h>


// structure 1
typedef struct example_1 {
    char c;
    short int s;
} struct1_t;

// structure 2
typedef struct example_2 {
    double d;
    int s;
    char c;
} struct2_t;

int main()
{
    printf("sizeof(struct1_t) = %lu\n", sizeof(struct1_t));
    printf("sizeof(struct2_t) = %lu\n", sizeof(struct2_t));

    return 0;
}

輸出

sizeof(struct1_t) = 4
sizeof(struct2_t) = 16

正如我們所看到的,結構的大小與我們計算的不同。

這是什麼原因呢?

C 和 Go 採用一種稱為「結構填充」的技術來確保資料在記憶體中適當對齊,由於硬體和架構的限制,這可能會顯著影響效能。資料填充和對齊符合系統架構的要求,主要是透過確保資料邊界與字長對齊來優化CPU存取時間。

讓我們透過一個範例來說明 Go 如何處理填充和對齊,請考慮以下結構:

type Employee struct {
  IsAdmin  bool
  Id       int64
  Age      int32
  Salary   float32
}

bool 為 1 個位元組,int64 為 8 個位元組,int32 為 4 個位元組,float32 為 4 個位元組 = 17 個位元組(總計)。

讓我們透過檢查編譯的 Go 程式來驗證結構大小:

package main

import (
    "fmt"
    "unsafe"
)

type Employee struct {
    IsAdmin bool
    Id      int64
    Age     int32
    Salary  float32
}

func main() {

    var emp Employee

    fmt.Printf("Size of Employee: %d\n", unsafe.Sizeof(emp))
}

輸出

Size of Employee: 24

報告的大小是 24 字節,而不是 17。這種差異是由於記憶體對齊造成的。為了理解對齊是如何運作的,我們需要檢查結構並視覺化它所佔用的記憶體。

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

圖 2 - 未最佳化的記憶體佈局

struct Employee 將消耗 8*3 = 24 個位元組。你現在看到問題了,Employee的佈局中有很多空洞(那些由對齊規則產生的間隙稱為“填充”)。

填充優化和性能影響

了解記憶體對齊和填充如何影響應用程式的效能至關重要。具體來說,資料對齊會影響存取結構體中的欄位所需的 CPU 週期數。這種影響主要來自 CPU 快取效應,而不是原始時脈週期本身,因為快取行為很大程度上取決於記憶體區塊內的資料局部性和對齊。

現代 CPU 將資料從記憶體提取到更快的中介(稱為快取)中,以固定大小的區塊(通常為 64 位元組)組織。當資料在相同或更少的快取行中良好對齊和本地化時,由於快取載入操作減少,CPU 可以更快地存取它。

考慮以下 Go 結構來說明較差對齊與最佳對齊:

// structure 1
typedef struct example_1 {
    char c;
    short int s;
} struct1_t;


// structure 2
typedef struct example_2 {
    double d;
    int s;
    char c;
} struct2_t;

對齊如何影響效能

CPU 以字大小而不是位元組大小讀取資料。正如我在開頭所描述的,64 位元系統中的一個字是 8 個位元組,而 32 位元系統中的一個字是 4 個位元組。簡而言之,CPU 以字大小的倍數讀取位址。為了取得變數 PassportId,我們的 CPU 需要兩個週期來存取數據,而不是一個。第一個週期將取得記憶體 0 到 7,後續週期將取得其餘記憶體。這是低效率的——我們需要資料結構對齊。透過簡單地對齊數據,電腦確保可以在一個CPU週期內檢索到var PassportId。

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

圖 3 - 比較記憶體存取效率

Padding是實現資料對齊的關鍵。之所以會發生填充,是因為現代 CPU 經過最佳化,可以從記憶體中的對齊位址讀取資料。這種對齊方式允許 CPU 在單一操作中讀取資料。

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

圖 4 - 簡單對齊資料

如果沒有填充,資料可能會錯位,導致多次記憶體存取和效能下降。因此,雖然 padding 可能會浪費一些內存,但它可以確保您的程式高效運行。

填充優化策略

對齊結構消耗更少的內存,因為與未對齊結構相比,它具有更好的結構字段順序。由於填充,兩個 13 位元組的資料結構分別變為 16 位元組和 24 位元組。因此,只需重新排序結構欄位即可節省額外的記憶體。

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

圖 5 - 最佳化現場秩序

不正確對齊的資料會降低效能,因為 CPU 可能需要多個週期來存取未對齊的欄位。相反,正確對齊的資料可以最大限度地減少快取行負載,這對於效能至關重要,尤其是在記憶體速度成為瓶頸的系統中。

讓我們做一個簡單的基準來證明這一點:

#include <stdio.h>


// structure 1
typedef struct example_1 {
    char c;
    short int s;
} struct1_t;

// structure 2
typedef struct example_2 {
    double d;
    int s;
    char c;
} struct2_t;

int main()
{
    printf("sizeof(struct1_t) = %lu\n", sizeof(struct1_t));
    printf("sizeof(struct2_t) = %lu\n", sizeof(struct2_t));

    return 0;
}

輸出

sizeof(struct1_t) = 4
sizeof(struct2_t) = 16

如您所見,遍歷對齊物件確實比遍歷對齊物件花費的時間更少。

添加填充是為了確保每個結構體字段根據其需要在記憶體中正確排列,就像我們之前看到的那樣。但是,雖然它可以實現高效訪問,但如果字段排序不好,填充也會浪費​​空間。

了解如何正確對齊結構體欄位以最大程度地減少填充導致的記憶體浪費對於高效記憶體使用非常重要,尤其是在效能關鍵型應用程式中。下面,我將提供一個結構對齊不良的範例,然後展示相同結構的最佳化版本。

在對齊不良的結構中,欄位的排序不考慮其大小和對齊要求,這可能導致增加填充和增加記憶體使用量:

// structure 1
typedef struct example_1 {
    char c;
    short int s;
} struct1_t;


// structure 2
typedef struct example_2 {
    double d;
    int s;
    char c;
} struct2_t;

總記憶體可能是 1 (bool) 7 (padding) 8 (float64) 4 (int32) 4 (padding) 16 (string) = 40 位元組。

優化的結構按從最大到最小的順序排列字段,顯著減少或消除對額外填充的需要:

#include <stdio.h>


// structure 1
typedef struct example_1 {
    char c;
    short int s;
} struct1_t;

// structure 2
typedef struct example_2 {
    double d;
    int s;
    char c;
} struct2_t;

int main()
{
    printf("sizeof(struct1_t) = %lu\n", sizeof(struct1_t));
    printf("sizeof(struct2_t) = %lu\n", sizeof(struct2_t));

    return 0;
}

總記憶體將整齊地包含 8 (float64) 16 (string) 4 (int32) 1 (bool) 3 (padding) = 32 個位元組。

我們來證明一下上面的內容:

sizeof(struct1_t) = 4
sizeof(struct2_t) = 16

輸出

type Employee struct {
  IsAdmin  bool
  Id       int64
  Age      int32
  Salary   float32
}

將結構大小從 40 位元組減少到 32 位元組意味著每個 Person 實例的記憶體使用量減少 20%。這可以在創建或儲存許多此類實例的應用程式中節省大量成本,提高快取效率並有可能減少快取未命中的數量。

結論

資料對齊是優化記憶體利用率和增強系統效能的關鍵因素。透過正確排列結構數據,記憶體使用不僅變得更加高效,而且 CPU 讀取時間也變得更快,從而顯著提高整體系統效率。

以上是優化 Go 中的記憶體使用:掌握資料結構對齊的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn