---
title: "メモリアライメントとは？構造体パディングとC言語での制御・アーキテクチャ別の挙動"
url: "https://www.issoh.co.jp/tech/details/3922/"
published: 2024-10-22
updated: 2026-07-19
categories: ["プラットフォーム"]
publisher: "株式会社一創"
---

# メモリアライメントとは？構造体パディングとC言語での制御・アーキテクチャ別の挙動

メモリアライメントとは、変数や構造体を「型のサイズに応じた特定のバイト境界」に配置し、CPUが1回のアクセスでデータを読み書きできるようにする仕組みです。この記事はメモリ（CPU）のアライメントを扱います（AIの価値整合や組織のアライメントとは別の概念です）。定義とCPUが遅くなる理由から、構造体パディングの具体的なバイト配置、C言語のalignof・alignas・#pragma packでの制御、x86とARMでの挙動の違い、Go言語の自動調整までをコード付きで整理します。

## まとめ：メモリアライメントの要点

- アライメントとは、N バイトの型を N の倍数のアドレスに置くこと。int は4、double とポインタは8バイト境界が基本。
- 整列していないと、キャッシュライン（64バイト）をまたいだ読み込みで2回のメモリアクセスが必要になり得る。速度低下の主因はこの境界跨ぎ。
- 構造体はメンバ間に自動でパディングが入る。**大きい型から順に宣言**するとパディングが減り、サイズが縮む。
- C言語では `alignof` で要求を調べ、`alignas` で強制、`#pragma pack` で詰め、`offsetof` でパディング位置を確認する。
- x86 は非整列でも動くが、ARM は SIMD・排他命令・Device メモリで整列フォルト（Linux では SIGBUS）を起こす。Go は自動調整するが 32bit 環境の64bitアトミックだけ例外。

## メモリアライメントの定義とCPU高速化の理由

アライメント（alignment）は「整列」を意味し、メモリアライメントはデータを型のサイズに合った境界アドレスへ揃えることを指します。現代のCPUはメモリを1バイト単位ではなく、ワードやキャッシュライン単位でまとめて読み書きするため、この境界が性能を左右します。

### 自然境界（naturally aligned）の考え方

各型には「自然境界」があり、サイズが N バイトの型はアドレスが N の倍数になる位置に置かれるのが基本です。char は1、short は2、int と float は4、double とポインタ（64bit環境）は8バイト境界に揃います。たとえば4バイトの int は 0, 4, 8, 12… のアドレスに配置するのが正しく、アドレス 5 から始めると非整列（misaligned）になります。この境界要求は型ごとに決まっており、C言語では後述の `alignof` で実際の値を確認できます。

### 非整列アクセスでCPUに何が起きるか

CPUはメモリをキャッシュライン（多くのx86／ARMで64バイト）単位でキャッシュに載せます。4バイトの int がアドレス 62 から始まると、その4バイトは 62〜63 と 64〜65 の2本のキャッシュラインに分割されます。CPUは両方のラインを読み、結果を結合してから値を返すため、整列時の1アクセスに対して2アクセス分のコストがかかります。キャッシュの仕組みそのものは[ディスクキャッシュとメモリキャッシュの違い](https://www.issoh.co.jp/tech/details/3534/)で整理していますが、アライメントが問題になるのは常に「境界を跨いだとき」だという点が要です。

## 構造体のパディングとメモリ配置

アライメントが最も表面化するのが構造体です。コンパイラは各メンバをその型の境界に揃えるため、メンバの間や末尾に「パディング」と呼ばれる詰め物のバイトを自動で挿入します。宣言順によってこのパディング量が変わり、同じメンバでも構造体のサイズが変わります。

### パディングが挿入される仕組み（バイト配置の具体例）

char・int・char を順に並べた次の構造体を、64bit環境（int=4バイト境界）で考えます。

```
struct Bad {
    char  a;   // offset 0（1バイト）
    int   b;   // offset 4（aの後ろに3バイトのパディング）
    char  c;   // offset 8
};             // sizeof(struct Bad) == 12（末尾に3バイトのパディング）
```

a は1バイトですが、次の int b を4の倍数アドレスに置くため a の後ろに3バイトの穴が空きます。さらに構造体全体のサイズは、その構造体の最大境界（ここでは4）の倍数に切り上げられるため、c の後ろにも3バイトのパディングが入り、合計12バイトになります。実際に使われているのは6バイト（12バイト中）で、半分が詰め物です。

| オフセット | 0 | 1〜3   | 4〜7 | 8 | 9〜11  |
| ----- | - | ----- | --- | - | ----- |
| 内容    | a | パディング | b   | c | パディング |

### メンバ並べ替えによるサイズ削減

大きい型から小さい型へ並べ替えると、中間のパディングが消えてサイズが縮みます。

```
struct Good {
    int   b;   // offset 0
    char  a;   // offset 4
    char  c;   // offset 5
};             // sizeof(struct Good) == 8（末尾2バイトのパディングのみ）
```

同じ3メンバでも 12バイト→8バイトへ、3分の1削減できました。構造体を大量に配列で確保する場面（数百万件のレコードなど）では、この差がそのままメモリ使用量とキャッシュ効率に直結します。**迷ったら「サイズの大きいメンバから宣言する」**のが実用的な指針です。

## C言語でのアライメント制御

C言語はアライメントを言語仕様として明示的に操作できます。C11 で `_Alignof`／`_Alignas` と `<stdalign.h>` が導入され、C23 では `alignof`／`alignas` がそのまま組み込みキーワードになりました（C11〜C17 では `<stdalign.h>` をインクルードすると `alignof` という綴りが使えます）。

### alignofによる要求値の確認

```
#include <stdalign.h>   // C11〜C17。C23では不要
#include <stdio.h>

printf("%zu\n", alignof(int));      // 4
printf("%zu\n", alignof(double));   // 8
printf("%zu\n", alignof(char));     // 1
```

`alignof` は型の境界要求をバイト数（size\_t）で返します。移植性のあるコードでは「4バイト境界」と決め打ちせず、この値を使って判断します。

### alignas・#pragma packでの指定

`alignas` は境界を「広げる」指定です。SIMD 命令はオペランドを16バイトや32バイト境界に要求することが多く、次のように明示します。

```
alignas(16) float vec[4];   // 16バイト境界に配置（SSE向け）
```

逆にパディングを「詰める」のが `#pragma pack` です。ネットワークプロトコルやファイルフォーマットのように、バイト配置を厳密に一致させたいときに使います。

```
#pragma pack(push, 1)
struct Packed {
    char a;
    int  b;
};                 // sizeof == 5（パディングなし）
#pragma pack(pop)
```

ただし詰めた構造体のメンバは非整列配置になるため、ARM など整列に厳しい環境では後述のフォルトを招きます。`#pragma pack` は「外部フォーマットと一致させる目的」に限定し、常用しないのが安全です。

### offsetofでのパディング位置の確認

```
#include <stddef.h>

offsetof(struct Bad, b);   // 4 ← aの後ろに3バイトのパディングがある証拠
offsetof(struct Bad, c);   // 8
```

`offsetof` は構造体先頭から各メンバまでのバイト数を返します。想定より大きな値が出たらそこにパディングが入っています。メモリ配置は[ポインタでアドレスを直接扱う](https://www.issoh.co.jp/tech/details/4290/)場面で意識する必要があり、非整列なポインタ経由のアクセスはアーキテクチャによって挙動が分かれます。

## アーキテクチャ別の非整列アクセスの挙動

非整列アクセスが「遅いだけ」で済むか「クラッシュする」かは、CPUアーキテクチャで大きく異なります。ここを取り違えると、x86 で動いたコードが ARM 実機で落ちます。

| アーキテクチャ          | 通常のロード/ストア        | フォルトする場合                         |
| ---------------- | ----------------- | -------------------------------- |
| x86 / x86-64     | 非整列を許容（速度低下のみ）    | SSEの整列必須命令（MOVAPS等）で#GP          |
| ARMv6以降（v7/v8含む） | Normalメモリなら非整列を許容 | LDM/STM、排他/順序付き命令、SIMD、Deviceメモリ |
| ARMv5以前          | 非整列を正しく扱えない       | フォルトまたは値のローテーション（実装依存）           |

x86 は非整列アクセスをハードウェアが吸収するため実行は継続し、コストは境界跨ぎ時の追加アクセスに留まります。ただし SSE の整列必須ロード（MOVAPS など）は16バイト境界でないと一般保護例外（#GP）で落ちるため、SIMD では `alignas` が必須です。ARMv6 以降は通常の単一ロード/ストアなら非整列を許容しますが、複数レジスタ転送（LDM/STM）、排他・順序付きアクセス、NEON の一部 SIMD 命令、Device メモリへのアクセスは整列フォルトを発生させ、Linux ユーザー空間では SIGBUS になります。「x86 で問題ないから大丈夫」という判断はクロスプラットフォームでは成り立ちません。

## 非整列アクセスは本当に遅いのか

「非整列は必ず遅い」と語られがちですが、現代の x86-64 ではそのペナルティは限定的です。境界を跨がない非整列アクセスは、整列アクセスとほぼ同じ速度で実行されます。速度差が顕在化するのは主に次の2ケースに絞られます。

- **キャッシュライン（64バイト）を跨ぐアクセス**：2本のラインを読むため追加コストが発生する。ページ境界（4KB）跨ぎはさらに重い。
- **SIMD の整列必須命令**：ここは速度以前に、非整列だと例外で停止する。

したがって実務の判断は「すべてを整列させる」ではなく、**ホットループで扱う大きな配列・SIMD 対象・境界を跨ぎやすいデータだけを整列させ、それ以外は詰めてメモリを節約する**のが合理的です。構造体パディングの削減（メモリ節約・キャッシュ効率）と、SIMD 向けの過剰整列（`alignas`）は目的が逆であり、同じ「アライメント最適化」でも狙いを分けて考える必要があります。むやみに全体を `#pragma pack` で詰めると、非整列アクセスが増えて ARM で落ちるだけでなく x86 でも境界跨ぎが増え、逆効果になり得ます。

## Go言語のアライメント自動調整と例外

Go はコンパイラがアライメントを自動で管理するため、通常はパディングを意識せずに書けます。C の `alignof`／`sizeof`／`offsetof` に相当する機能は `unsafe` パッケージにあります。

```
import "unsafe"

unsafe.Alignof(int64(0))   // 8
unsafe.Sizeof(myStruct)    // パディング込みのサイズ
unsafe.Offsetof(s.field)   // メンバのオフセット
```

Go でも構造体のパディングは C と同じ規則で入るため、フィールドを大きい型から並べるとサイズが縮みます。並べ替えの余地は fieldalignment アナライザ（golang.org/x/tools）が自動検出してくれます（`go vet -vettool` や gopls 経由で実行）。

注意すべき例外が1つあります。32bit 環境（32bit ARM や 386）では、`sync/atomic` の64bit操作（AtomicなInt64など）が対象を8バイト境界に要求し、構造体の途中フィールドだと非整列になってパニックする既知の制約があります。Go 1.19 以降の `atomic.Int64` 型を使えばこの整列は保証されるため、32bit 対応が必要なら生の `int64` \+ `atomic.AddInt64` ではなく型ベースのAPIを選ぶのが安全です。メモリ配置全般の理解には[Rustで見るスタックとヒープのメモリ管理](https://www.issoh.co.jp/tech/details/2814/)もあわせて参照してください。

## よくある質問

### メモリアライメントとは簡単に言うと何ですか？

N バイトの型を N の倍数のアドレスに置くことです。CPU がメモリをまとめて読む単位に合わせておくと、1回のアクセスでデータを取得でき、境界を跨いだ余分な読み込みを避けられます。

### アライメントを揃えないとどうなりますか？

x86 では速度がやや落ちる程度で動作は継続します。一方 ARM や MIPS では、SIMD 命令や複数レジスタ転送などで整列フォルトが起き、Linux では SIGBUS でプログラムが停止することがあります。

### 構造体のパディングを減らすにはどうすればいいですか？

メンバをサイズの大きい型から順に宣言します。char・int・char の順（12バイト）を int・char・char の順（8バイト）に変えるだけでサイズが縮みます。`offsetof` で穴の位置を確認できます。

### C言語でアライメントを指定するには何を使いますか？

境界を広げるなら `alignas`、詰めるなら `#pragma pack`、要求値を調べるなら `alignof` を使います。`alignof`／`alignas` は C23 でキーワード、C11〜C17 では `<stdalign.h>` をインクルードして使います。

### #pragma pack は常に使ってよいですか？

常用は避けるべきです。詰めた構造体は非整列配置になり、ARM などでフォルトの原因になります。ネットワークパケットやファイルフォーマットなど、外部とバイト配置を一致させたい箇所だけに限定してください。

## 関連記事

- [ポインタの基本概念とその役割に関する詳細解説](https://www.issoh.co.jp/tech/details/4290/)
- [Rustのメモリ管理におけるスタックとヒープの基本概念を理解する](https://www.issoh.co.jp/tech/details/2814/)
- [ディスクキャッシュとは｜仕組み・メモリキャッシュとの違い・確認方法](https://www.issoh.co.jp/tech/details/3534/)

---

出典: [メモリアライメントとは？構造体パディングとC言語での制御・アーキテクチャ別の挙動](<https://www.issoh.co.jp/tech/details/3922/>)（株式会社一創）
