Go でのメモリ使用量の最適化: データ構造の調整をマスターする

Barbara Streisand
リリース: 2024-11-16 09:54:03
オリジナル
421 人が閲覧しました

メモリの最適化は、パフォーマンスの高いソフトウェア システムを作成するために非常に重要です。ソフトウェアが使用できるメモリの量が限られている場合、そのメモリが効率的に使用されないと多くの問題が発生する可能性があります。そのため、全体的なパフォーマンスを向上させるにはメモリの最適化が重要です。

Go は C の優れた機能の多くを継承していますが、私が気づいたのは、Go を使用する人の大部分がこの言語の能力を十分に理解していないということです。理由の 1 つは、低レベルでどのように動作するかに関する知識の不足、または C や C などの言語の経験の不足である可能性があります。 C と C について言及したのは、 Go の基礎が C/C の素晴らしい機能に基づいて構築されているためです。 Google I/O 2012 での Ken Thompson のインタビューを引用するのは偶然ではありません:

私にとって、Go に熱中していた理由は、Go を始めたのとほぼ同じ時期に、提案されている C 0x 標準を読んだ (または読もうとした) ためです。私。

今日は、Go プログラムを最適化する方法、具体的には Go で構造体を使用するのがどのように良いかについて話します。まず構造とは何かを説明しましょう:

構造体は、異なる型の関連する変数を 1 つの名前でグループ化するユーザー定義のデータ型です。

問題がどこにあるのかを完全に理解するために、最新のプロセッサはメモリから一度に 1 バイトを読み取らないことに言及します。 CPU はメモリに保存されているデータや命令をどのようにフェッチしますか?

コンピュータ アーキテクチャにおいて、ワードとはプロセッサが 1 回の操作で処理できるデータの単位であり、一般にアドレス指定可能なメモリの最小単位です。これは、固定サイズのビット (2 進数) のグループです。プロセッサのワード サイズによって、データを効率的に処理できる能力が決まります。一般的なワード サイズには、8、16、32、および 64 ビットが含まれます。一部のコンピューター プロセッサ アーキテクチャは、ワード内のビット数の半分であるハーフ ワードと、連続する 2 つのワードであるダブル ワードをサポートしています。

現在、最も一般的なアーキテクチャは 32 ビットと 64 ビットです。 32 ビット プロセッサを使用している場合、一度に 4 バイトにアクセスできるということは、ワード サイズが 4 バイトであることを意味します。 64 ビット プロセッサを使用している場合は、一度に 8 バイトにアクセスできます。これは、ワード サイズが 8 バイトであることを意味します。

データをメモリに保存すると、以下に示すように、各 32 ビット データ ワードに一意のアドレスが割り当てられます。

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

図。 1 ‑ ワードアドレス指定可能なメモリ

ロード ワード (lw) 命令を使用して、メモリ内のデータを読み取り、1 つのレジスタにロードできます。

上記の理論を理解した後、実践方法を見てみましょう。構造体データ構造の場合の説明については、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
ログイン後にコピー

報告されたサイズは 17 バイトではなく 24 バイトです。この不一致はメモリのアライメントによるものです。アライメントがどのように機能するかを理解するには、構造を検査し、それが占有するメモリを視覚化する必要があります。

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

図 2 - 最適化されていないメモリ レイアウト

構造体 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はデータにアクセスするのに1サイクルではなく2サイクルかかります。最初のサイクルでメモリ 0 ~ 7 がフェッチされ、次のサイクルで残りがフェッチされます。そして、これは非効率的です。データ構造の調整が必要です。データを調整するだけで、コンピュータは var パスポート ID を 1 CPU サイクルで取得できるようになります。

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

図 3 - メモリ アクセス効率の比較

パディングはデータの配置を達成するための鍵です。パディングが発生するのは、最新の CPU がメモリから整列されたアドレスでデータを読み取るように最適化されているためです。この位置合わせにより、CPU は 1 回の操作でデータを読み取ることができます。

Optimizing Memory Usage in Go: Mastering Data Structure Alignment

図 4 - 単純なデータの位置合わせ

パディングを行わないと、データの位置がずれて、複数のメモリ アクセスが発生し、パフォーマンスが低下する可能性があります。したがって、パディングによって一部のメモリが浪費される可能性がありますが、プログラムが効率的に実行されることが保証されます。

パディングの最適化戦略

Aligned struct は、Misaligned と比較して構造体フィールドの順序が優れているため、消費するメモリが少なくなります。パディングにより、2 つの 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
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

ご覧のとおり、Aligned のトラバースには、対応するものよりも実際に時間がかかりません。

前に見たように、各構造体のフィールドがそのニーズに基づいてメモリ内で適切に整列するように、パディングが追加されました。ただし、パディングにより効率的なアクセスが可能になる一方で、フィールドが適切に順序付けされていない場合、スペースを無駄にする可能性もあります。

パディングによるメモリの無駄を最小限に抑えるために構造体フィールドを適切に配置する方法を理解することは、特にパフォーマンスが重要なアプリケーションにおいて効率的にメモリを使用するために重要です。以下に、適切に調整されていない構造の例を示し、次に同じ構造の最適化されたバージョンを示します。

適切に配置されていない構造体では、フィールドのサイズと配置要件が考慮されずに順序付けされるため、パディングが追加され、メモリ使用量が増加する可能性があります。

// 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 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート