メモリアライメントとは、変数や構造体を「型ごとに決まったバイト境界」のアドレスに配置することです。int なら4の倍数、double やポインタ(64bit環境)なら8の倍数のアドレスに置くのが基本です。ここで扱うのはメモリ(CPU)のアライメントで、AIの価値整合やビジネス用語としての意味はアラインメントの意味と分野別の使い方で整理しています。構造体パディング、アライメント違反の検出、C言語での制御、x86とARMの挙動差を、実際に動かしたコードと計測値で説明します。
まとめ:メモリアライメントの要点
- 要求アライメントが N バイトの型は N の倍数のアドレスに置く。多くの基本型ではサイズと同じ値だが、32bit x86 Linux の long double(サイズ12・アライメント4)のように一致しない型もあり、long は Linux(LP64)で8バイト、Windows(LLP64)で4バイトなど、環境で値が変わる。
- 構造体では、次のメンバの境界や全体のアライメントに合わせる必要があるとき、メンバの間と末尾にパディングが入る。要求アライメントの大きいメンバから順に宣言すると、char・int・char の12バイトが8バイトに縮む。
- バイト列を
(uint32_t *)などにキャストして読むのがアライメント違反の典型。C言語では未定義動作で、-fsanitize=alignmentで検出でき、memcpyで読めば避けられる。 - x86 は通常のロードなら非整列でも動くが、SSE の整列必須命令(MOVAPS)はSIGSEGVで落ちる。ARM は Device メモリへの非整列アクセスなどでアライメントフォルトになる。
- Intel Core i9-9880H で8バイトの読み込みを測った範囲では、64バイトのキャッシュライン内の非整列は整列とほぼ同速で、ライン跨ぎで約2倍、4KBページ跨ぎで約6倍遅くなった。差が出たのは境界を跨ぐときだった。
メモリアライメントの定義と型ごとの境界値
C言語の規格はアライメントを「特定の型のオブジェクトを、特定のバイトアドレスの倍数となる記憶域境界に配置する要求」と定義しています(C23の草案 N3096 の 3.2 節)。日本の対応規格 JIS X 3010:2003(プログラム言語C)では、alignment の訳語に「境界調整」を充てています。アライメントの値は常に2の累乗で、C言語では alignof で確かめられます。
自然境界と、CPUが整列を求める理由
サイズが N バイトの基本型を N の倍数のアドレスに置いた状態を自然境界(naturally aligned)と呼びます。4バイトの int を 0, 4, 8, 12… に置けば整列、5 から始めれば非整列(misaligned)です。CPUはメモリを1バイトずつではなく、キャッシュライン(x86-64 と多くの ARM で64バイト)単位でキャッシュに載せます。4・8バイトの値が自然境界に整列していれば、64バイトのライン境界を跨ぐことはありません。境目に値がかかると、2本のラインを読んで結合する処理が必要になります(後述の計測で約2倍の時間がかかりました)。キャッシュの階層そのものはディスクキャッシュとは?仕組み・メモリキャッシュとの違いで扱っています。
ILP32・LP64・LLP64で変わるアライメント表
「int は4、double は8」は多くの環境で成り立ちますが、long・long double・ポインタはデータモデルで変わります。次の表は、clang 21 に各ターゲットを指定して sizeof と _Alignof を出力させた値です(左がサイズ、右がアライメント)。
| 型 | x86-64 Linux(LP64) | Windows x64(LLP64) | AArch64 Linux | 32bit x86 Linux(ILP32) | 32bit ARM Linux |
|---|---|---|---|---|---|
| short | 2 / 2 | 2 / 2 | 2 / 2 | 2 / 2 | 2 / 2 |
| int・float | 4 / 4 | 4 / 4 | 4 / 4 | 4 / 4 | 4 / 4 |
| long | 8 / 8 | 4 / 4 | 8 / 8 | 4 / 4 | 4 / 4 |
| long long・double | 8 / 8 | 8 / 8 | 8 / 8 | 8 / 8(構造体内は4) | 8 / 8 |
| long double | 16 / 16 | 8 / 8 | 16 / 16 | 12 / 4 | 8 / 8 |
| ポインタ | 8 / 8 | 8 / 8 | 8 / 8 | 4 / 4 | 4 / 4 |
注意が要るのは32bit x86 Linux の double と long long です。_Alignof(double) は8を返すのに、struct { char c; double d; } の中では d が offset 4 に置かれ、構造体のサイズは12バイトになります(同じ構造体が x86-64 と32bit ARM では16バイト)。System V の i386 ABI が構造体メンバとしての double を4バイト境界と定めているためで、32bit ARM と64bit環境で作ったバイナリ形式をそのまま i386 で読むと、オフセットがずれます。ファイルや通信でバイト配置をやり取りするなら、表の値を決め打ちせず、対象環境で offsetof を出力して確かめてください。
構造体パディングの規則とメンバ並べ替えによるサイズ削減
コンパイラは各メンバを自分のアライメントの倍数に置くため、メンバの間に詰め物のバイト(パディング)を入れます。さらに構造体全体のサイズは、メンバの最大アライメントの倍数に切り上げられます。配列にしたとき2要素目以降も整列させるためで、末尾にもパディングが入る理由はこれです。
パディングが入る位置と計算式
次の位置にメンバを置くとき、必要なパディングは「(align − offset % align) % align」で求まります。char・int・char の順に並べた構造体で確かめます。
struct Bad {
char a; // offset 0
int b; // offset 4((4 - 1 % 4) % 4 = 3バイトのパディング)
char c; // offset 8
}; // sizeof == 12(最大アライメント4の倍数へ切り上げ、末尾に3バイト)
| オフセット | 0 | 1〜3 | 4〜7 | 8 | 9〜11 |
|---|---|---|---|---|---|
| 内容 | a | パディング | b | c | パディング |
12バイトのうち半分は詰め物です。
要求アライメント順のメンバ並べ替えによる12バイトから8バイトへの削減
struct Good {
int b; // offset 0
char a; // offset 4
char c; // offset 5
}; // sizeof == 8(末尾2バイトのパディングのみ)
同じ3メンバで3分の1小さくなり、1億件の配列なら1.2GBが0.8GBになります。並べ替えの効き方はデータモデルでも変わり、struct { char c; long l; short s; void *p; } は x86-64 Linux で32バイト、long が4バイトの Windows x64 では24バイトでした。並べ替えは構造体のバイナリ互換を壊すので、公開ヘッダやファイル形式の構造体では行わず、内部で大量に確保する構造体に絞ります。
アライメント違反(非整列アクセス)が起きるコードと検出方法
アライメント違反とは、型のアライメントを満たさないアドレスを、その型のポインタとして読み書きすることです。コンパイラが配置した変数や構造体では起きず、プログラマがアドレスを自分で作ったときに起きます。
バイト列のポインタキャストが未定義動作になる理由
典型は、受信したパケットやファイルのバイト列を、途中のオフセットから uint32_t * にキャストして読むコードです。
uint8_t *buf = calloc(64, 1); // ゼロ初期化して確保
if (buf == NULL) abort();
uint32_t *p = (uint32_t *)(buf + 1); // 4の倍数でないアドレス
uint32_t v = *p; // アライメント違反(未定義動作)
C規格は、変換結果のポインタが参照先の型に正しく整列していなければ動作を未定義としています。カーネギーメロン大学のセキュアコーディング規約 SEI CERT C の EXP36-C は、これを「より厳しいアライメントのポインタ型へキャストしない」というルールにしています。x86 では偶然動くため見過ごされ、ARM の実機や最適化レベルを変えたビルドで初めて落ちる、という形で表面化します。
UBSanの -fsanitize=alignment で検出した出力
clang と GCC は、実行時にアライメント違反を検出する -fsanitize=alignment を持っています(clang の UndefinedBehaviorSanitizer の1項目)。上のコードと同じ処理(ub.c)を macOS の clang 21 でビルドして実行すると、次のように違反の行と要求アライメントが表示されました。
$ clang -g -fsanitize=alignment ub.c -o ub && ./ub
ub.c:9:17: runtime error: load of misaligned address 0x600003e8c001 for type 'uint32_t'
(aka 'unsigned int'), which requires 4 byte alignment
既定では報告後も実行を続けます。CI で確実に止めたいときは -fno-sanitize-recover=alignment を足すと、最初の違反で異常終了します(計測環境では終了コード134)。Xcode では Scheme の Diagnostics にある Undefined Behavior Sanitizer の Misaligned pointer チェックが同じ検査です。
memcpyによる非整列データの安全な読み出し
直し方は、キャストをやめて memcpy で整列済みの変数へコピーすることです。解決するのはアライメントだけなので、受信データの長さの確認と、エンディアン(バイト順)の変換は別に行います。
uint32_t v;
memcpy(&v, buf + 1, sizeof v); // どのアドレスからでも定義された動作
最適化を有効にすれば、関数呼び出しのコストは通常残りません。この読み出しを clang 21 の -O2 でコンパイルすると、x86-64 では movl 1(%rdi), %eax、AArch64 では ldur w0, [x0, #1]、32bit ARM(ARMv7)では ldr r0, [r0, #1] の1命令になりました。非整列のワード読み込みを前提にできない ARMv5 向けでは、1バイトずつ4回読んでシフトで組み立てるコードが出ます。キャストと違い、ターゲットの CPU で安全な命令をコンパイラが選びます(インライン展開されるかはコンパイラと最適化レベル次第です)。#pragma pack でパケット全体を詰めた構造体として読む方法もありますが、後述のとおりメンバへのポインタを取ると同じ違反に戻るため、受信バッファの解析は memcpy を基本にするのが安全です。ポインタ演算全般はポインタとは?C言語での使い方とメリットで解説しています。
C言語でアライメントを制御する方法:alignof・alignas・#pragma pack・aligned_alloc
C11 で _Alignof/_Alignas とヘッダ <stdalign.h> が入り、C23 で alignof/alignas がキーワードになりました。C11〜C17 では <stdalign.h> をインクルードすると alignof/alignas の綴りが使えます(中身は _Alignof/_Alignas へのマクロ)。
alignofによる要求値の確認とalignasによる境界の拡大
#include <stdalign.h> // C11〜C17。C23では不要
#include <stdio.h>
#include <stdint.h>
alignas(16) float vec[4]; // SSEの整列必須命令向けに16バイト境界へ
int main(void) {
printf("%zu\n", alignof(double)); // x86-64 では 8
printf("%zu\n", (size_t)((uintptr_t)vec % 16)); // 0(16の倍数に置かれた)
}
alignas は境界を広げる指定で、型本来の値より小さくはできません。標準Cの alignof に渡せるのは型名で、alignof(vec) のように変数を渡す書き方は GCC・clang の拡張です(-pedantic-errors ではエラー)。この記事で扱う alignas の用途は、SIMD のオペランドを16・32バイト境界に揃えることと、後述の偽共有を避けるためにキャッシュライン(64バイト)へ揃えることの2つです。SIMD 命令の側から見た整列の要否はSIMDとは?AVX2・NEONの仕組みと自動ベクトル化・組み込み関数の書き方にまとめています。
#pragma packと__attribute__((packed))によるパディングの除去
#pragma pack(push, 1)
struct Packed {
char a;
int b;
}; // sizeof == 5(パディングなし)
#pragma pack(pop)
struct Packed2 { char a; int b; } __attribute__((packed)); // GCC・clang の書き方
詰めると b は offset 1 の非整列になります。コンパイラは s.b と直接書いた読み書きは非整列対応の命令にしてくれますが、int *q = &s.b; とポインタを取り出した時点でその保証は消えます。__attribute__((packed)) の構造体でこれを書くと、GCC 16 は既定で -Waddress-of-packed-member の警告を出しました。警告が出ない書き方や処理系もあるので、警告がないことは安全の保証になりません。用途はネットワークヘッダやファイル形式とバイト配置を一致させる場面に限り、内部データ構造のメモリ節約には並べ替えを使ってください。
offsetofが返すメンバ位置とパディングの確認
#include <stddef.h>
printf("%zu\n", offsetof(struct Bad, b)); // 4 ← a の後ろに3バイトの穴
printf("%zu\n", offsetof(struct Bad, c)); // 8
前のメンバの offset+サイズより大きな値が出たら、その間がパディングです。外部とやり取りする構造体には _Static_assert(offsetof(struct Hdr, len) == 4, "layout"); を書いておくと、別ターゲットでレイアウトがずれたときにコンパイルで止まります。
malloc・aligned_alloc・posix_memalignの整列保証
C規格上、malloc が返すアドレスは、要求したサイズに収まる基本的なアライメントの型を置ける境界に揃っています。実装はこれより強く保証することが多く、x86-64 の macOS と Linux(glibc)では alignof(max_align_t) も malloc の戻り値の境界も16で、glibc のマニュアルも64bit システムでは16の倍数と説明しています。それより大きな境界が必要なときは aligned_alloc か POSIX の posix_memalign を使います。
void *a = aligned_alloc(64, 128); // サイズはアライメントの倍数にする
void *b;
posix_memalign(&b, 64, 100); // 境界は sizeof(void *) の倍数かつ2の累乗。成功で0を返す
aligned_alloc には実装差があります。C11 の規格書はサイズをアライメントの倍数にするよう求めていましたが、C17 に取り込まれた 欠陥報告 DR460 でこの条件は削除されました。それでも aligned_alloc(64, 100) を実行すると、Linux(glibc)は64バイト境界のアドレスを返し、macOS は NULL を返しました。移植するコードではサイズをアライメントの倍数に切り上げてから呼び、戻り値の NULL を必ず確認してください。
x86とARMで分かれる非整列アクセスの挙動
同じアライメント違反でも、遅くなるだけで済むか、プロセスが落ちるかは CPU と OS で決まります。次の表の x86 の2行は Linux と macOS で動かした結果、ARM の行は Arm の公式解説に基づきます。
| 環境 | 通常のロード/ストア | フォルトする場合 | 届くシグナル |
|---|---|---|---|
| x86-64 | 非整列を許容 | MOVAPS などの整列必須命令(#GP) | SIGSEGV |
| x86-64 で AC フラグを立てた Linux | #AC 対象命令は例外 | #AC 対象命令の非整列アクセス | SIGBUS |
| ARMv8-A(AArch64)の Linux ユーザー空間 | Normal メモリなら許容 | Device メモリ、SCTLR_ELx.A 有効時 | SIGBUS |
x86で落ちる2つのケースを再現した結果
x86-64 の通常の mov 命令は非整列でもそのまま動きます。落ちるのは SSE の整列必須ロードです。16バイト境界から4バイトずらした配列に対し、_mm_loadu_ps(非整列可)は値を返し、_mm_load_ps(MOVAPS にコンパイルされる)は一般保護例外(#GP)になって、macOS でも Linux でも SIGSEGV で終了しました。Intel の命令リファレンス(SDM Vol.2)も、MOVAPS のメモリオペランドが16バイト境界でなければ #GP になると定めています。SIGBUS ではない点に注意してください。セグメンテーション違反として現れるので、セグメンテーションフォルトの原因と調べ方の手順で追うとアドレスの下位ビットが 0 でないことが分かります。
もう1つは EFLAGS の AC(Alignment Check)フラグです。ユーザー空間からこのフラグを立てると、アライメントチェックの対象となる命令の非整列アクセスで #AC 例外が上がります(チェックしない命令もあります)。Linux(カーネル 6.8・Docker 上の gcc 16)では非整列の4バイト読み込みが Bus error(終了コード135=SIGBUS)で止まりました。同じソースを macOS 向けにビルドして実行すると、フォルトせずに値がそのまま読めました。Intel SDM はアライメントチェックが働く条件を CR0.AM=1・EFLAGS.AC=1・特権レベル3の3つとしており、CR0 の AM ビットは OS が決めるためです。Linux 側は arch/x86/kernel/traps.c で #AC を SIGBUS(BUS_ADRALN)に変換しています。x86 上の Linux で非整列アクセスを洗い出す手段にはなりますが、macOS では効きませんでした(Windows は未検証)。
ARMでアライメントフォルトになる条件
Arm の公式解説 Learn the architecture: AArch64 memory attributes and properties は、Normal 属性のメモリへの非整列アクセスは許可され、Device 属性のメモリ(周辺機器のレジスタなど)への非整列アクセスはアライメントフォルトになると説明しています。Normal メモリでも、システム制御レジスタ SCTLR_ELx の A ビットを立てれば非整列をフォルトとして捕捉できます。同資料によると MMU を有効にする前のデータアクセスは Device 扱いなので、ブートローダなどの起動コードでは非整列のデータアクセスが即座に例外になります。
Linux のアプリケーションが使うメモリは Normal なので、AArch64 でも通常の読み書きは x86 と同様に動くことが多いです。落ちやすいのは、ファームウェアやドライバのように Device メモリを扱うコードと、SIMD 命令など整列を前提とする命令に非整列のアドレスを渡したケースで、どちらも x86 の開発機では再現しません。AI/IoTソリューションのような組み込みを含む開発では、-fsanitize=alignment を最初から開発機の CI に入れておきます。
非整列アクセスはどれだけ遅いのか:境界別の実測値
「非整列は遅い」とよく書かれますが、どの程度かを x86-64 の実機で測りました。4KB 境界に置いたバッファの同じアドレスから、オフセットを変えて8バイトの volatile ロードを4億回繰り返し、ループ全体の時間を回数で割った値の中央値(9回計測)を取っています(Intel Core i9-9880H・Apple clang 21.0.0(clang-2100.1.1.101)-O2・2回実行)。計測ループの中心は次の形です。
static uint8_t buf[8192] __attribute__((aligned(4096)));
volatile uint64_t *p = (volatile uint64_t *)(buf + off); // off = 0, 1, 60, 4092
for (long i = 0; i < 50000000; i++) {
a += *p; b += *p; c += *p; d += *p;
a += *p; b += *p; c += *p; d += *p; // 1周8ロード×5千万周=4億ロード
}
値は、L1 キャッシュに載ったデータを4本の加算系列で読み続けるループの時間を回数で割ったもので、1回のロードのレイテンシやメモリのレイテンシではありません。計測コードは非整列ポインタを直接たどる未定義動作の書き方なので、製品コードでは memcpy を使ってください。
| 読み出し位置 | 1ロードあたり | 整列との比 |
|---|---|---|
| 整列(offset 0) | 0.49〜0.54 ns | 1.00 |
| 非整列・ライン内(offset 1) | 0.52〜0.53 ns | 0.98〜1.05 |
| 64バイトライン跨ぎ(offset 60) | 1.00〜1.01 ns | 1.87〜2.03 |
| 4KBページ跨ぎ(offset 4092) | 3.11〜3.19 ns | 5.91〜6.34 |
ライン内に収まる非整列は、整列と区別できない差でした。遅くなるのは値がキャッシュラインを跨ぐときで約2倍、ページ境界を跨ぐと約6倍です。8バイト値の開始位置がライン内の64通りに一様に散らばるなら、ラインを跨ぐのは7通り(64分の7)なので、非整列アクセスの多くは速度上の問題になりません。
この結果から、判断は次のとおりです。メモリ節約は、公開ヘッダやファイル形式に関わらない内部の構造体をメンバの並べ替えで縮めて行い、#pragma pack で全体を詰めるのはやめる(詰めたメンバは境界を跨ぎうるうえ、ポインタ経由のアクセスは条件次第で ARM のフォルトにつながるため)。整列を強める alignas は、SIMD の整列必須命令とスレッド間で共有する変数にだけ使います。後者は偽共有(false sharing)の回避です。2つのスレッドがそれぞれ別の8バイトカウンタを2億回ずつ加算する計測で、同じ64バイトラインに並べた場合は1.28〜1.31秒、alignas(64) で別ラインに分けた場合は0.88〜0.96秒でした。構造体は16バイトから128バイトに膨らむので、スレッドごとのカウンタやキューの先頭・末尾ポインタのように、書き込みが頻繁に競合する変数に限って使います。
Go言語のアライメント自動調整と32bit環境のアトミック操作
Go はコンパイラがアライメントを管理するため、通常はパディングを意識せずに書けます。C の alignof/sizeof/offsetof に当たる関数は unsafe パッケージにあります。各フィールドを自分のアライメントに合わせてパディングする考え方は C と共通ですが、末尾のサイズ0のフィールド(struct{})を特別扱いするなど、細部のレイアウトは C と同一ではありません。
import (
"fmt"
"unsafe"
)
// main 関数の中で実行する。v は任意の構造体の変数
fmt.Println(unsafe.Alignof(int64(0))) // amd64・arm64 では 8、386・32bit ARM では 4
fmt.Println(unsafe.Sizeof(v)) // パディング込みのサイズ
fmt.Println(unsafe.Offsetof(v.field)) // メンバのオフセット
フィールドの並べ替えで縮む構造体は、golang.org/x/tools の fieldalignment アナライザが検出します。注意すべき例外は32bit 環境(386・32bit ARM)です。sync/atomic のドキュメントは、これらの環境で64bit のアトミック操作を使う側が8バイト境界を保証する責任を負うと明記しており、8バイト境界に揃っていない int64(int64 のアライメントが4の環境では、構造体の途中のフィールドがこうなりやすい)を atomic.AddInt64 で操作すると panic します。Go 1.19 で追加された atomic.Int64 型は32bit 環境でも自動で8バイト境界に揃うので、32bit 向けにビルドするコードではこちらを使います。
よくある質問
メモリアライメントとは簡単に言うと何ですか?
型ごとに決まった要求アライメントの倍数のアドレスに、その型の値を置くことです。int なら4の倍数、double とポインタ(64bit環境)なら8の倍数が一般的です。整列しておくと、SIMD 命令などの整列条件を満たし、小さな値がキャッシュラインの境目を跨ぐことも避けられます。値は環境で変わるので、正確には C言語の alignof で確かめます。
アライメント違反とは何ですか?どうなりますか?
型のアライメントを満たさないアドレスを、その型のポインタで読み書きすることです。バイト列を途中から (uint32_t *) にキャストして読むのが典型です。C言語では未定義動作で、x86 では多くの場合そのまま動き、SSE の整列必須命令や ARM の Device メモリへのアクセスでは SIGSEGV や SIGBUS で停止します。-fsanitize=alignment で検出し、memcpy で読むように直します。
構造体のパディングを減らすにはどうすればいいですか?
メンバを要求アライメントの大きい型から順に宣言します。char・int・char の順(12バイト)を int・char・char の順にすると8バイトになります。offsetof で各メンバの位置を出力すると、穴の場所が分かります。公開ヘッダやファイル形式の構造体は並べ替えるとバイナリ互換が崩れるので対象外にします。
C言語でアライメントを指定するには何を使いますか?
要求値を調べるのが alignof、境界を広げるのが alignas、パディングをなくして詰めるのが #pragma pack か __attribute__((packed))、大きな境界でヒープを確保するのが aligned_alloc か posix_memalign です。alignof/alignas は C23 ではキーワード、C11〜C17 では <stdalign.h> をインクルードして使います。
#pragma pack は常に使ってよいですか?
常用は避けてください。詰めた構造体のメンバは非整列になることがあり、メンバのアドレスを取り出して使うと、ARM の Device メモリや SIMD 命令などの条件でフォルトの原因になります。速度面でも、今回の計測ではページ境界を跨ぐ読み込みが約6倍遅くなりました。ネットワークパケットやファイル形式とバイト配置を一致させる箇所だけに使い、メモリ節約が目的ならメンバの並べ替えで対応します。