世界を動かす技術を、日本語で。

メモリ効率の良いC構造体の作成

概要

C言語のstructはデータ整理に便利だが、メモリ配置やパディングに注意が必要。 構造体のサイズを最適化するためのテクニックを解説。 フィールドの並び順や型選択、ビットフィールドやenumの活用が重要ポイント。 実例を交えつつ、メモリ削減の効果を具体的に示す。 パフォーマンスと可読性のバランスも考慮事項。

C言語構造体のメモリ最適化テクニック

  • C言語のstruct は、データを効率よく整理・管理するための基本手段。
  • フィールドの型や並び順によって、 構造体のサイズ が予想以上に大きくなる場合あり。
  • パディング(隙間) は、各メンバーのアライメント要件により自動で挿入される現象。
  • 例:Monster構造体を定義し、各フィールドの型サイズを合計しても、 sizeofで得られる値が大きくなる 理由はパディングの存在。
  • アライメント要件により、 int型は4バイト境界、boolやcharは1バイト境界など、システム依存の違いもある。

Monster構造体例とパディングの発生

  • 初期のMonster構造体例では、フィールド合計89バイトだが、sizeof(struct Monster)は 96バイト
  • メモリレイアウトを表にすると、 パディング合計6バイト が追加されていることが分かる。
  • パディングは主に「アライメント調整」と「末尾パディング(tail padding)」の2種類。

パディング削減のためのフィールド再配置

  • アライメント値が同じフィールド をまとめて配置することで、パディングを減らせる。
  • 一般的に「 大きい型から小さい型へ」並べるのが最適。
  • Monster構造体のフィールド順を最適化し、 92バイトに削減

派生状態(Derived State)の活用

  • 例: is_aliveフィールド はhealth > 0で判定できるため、フィールド自体を削除してサイズ削減。
  • この手法により、 88バイトに削減

型の見直しによるサイズ削減

  • healthやspeedなどの値 は、int型よりも小さい型(uint8_tやuint16_t)で十分な場合が多い。
  • stdint.hの型を活用し、Monster構造体を 84バイト に削減。

ビットフィールドの活用

  • bool値 は1バイト消費するが、ビットフィールドを使えば 1ビット単位 で格納可能。
  • Monster構造体のbool系フィールドをビットフィールド化し、 80バイト に削減。

enumによる識別子の最適化

  • 名前やモデル識別に 文字列を直接格納するのは非効率
  • enumを使い、MonsterName型で識別すると、 20バイトまで削減 可能。
  • これにより、モンスター1000体分で96KB→20KBに大幅削減。

最適化のトレードオフと注意点

  • 全ての構造体で極端な最適化を行う必要はない
  • メモリ効率が特に重要なシステムや大量データを扱う場合に有効。
  • 型を小さくし過ぎると オーバーフローや予期しない動作 のリスクもあるため、用途に応じてバランスを考慮。
  • 可読性や保守性とのバランスも重要。

まとめ

  • 構造体のサイズ削減 には、フィールド並び順・型の選択・ビットフィールド・enum活用が有効。
  • 適切な最適化は メモリ削減やパフォーマンス向上 に直結。
  • ただし、 状況や目的に応じて最適化レベルを選択 することが大切。

タグ: blogtutorialC

Hackerたちの意見

paholeやドワーフについての言及はないの? https://lwn.net/Articles/335942/ これはLinuxカーネル開発者たちが使ってる標準ツールだよ。

初心者が始めるとき、こういうツールのリストはどこで探せばいいの?

最近のclangdのバージョンでは、paholeみたいな構造体サイズ/アライメントのパディングオーバーヘッドの注釈が表示されるようになった。これがpaholeを使うよりもずっと便利だと思う。

paholeは素晴らしいツールだね。

完全性のために言うけど、このアライメントの説明は誤解を招くよね。 > 「さて、読者の皆さん、このパディングはCPUがメモリを4バイト単位でアライメントする必要があるから追加されているんです。」 > ... > つまり、構造体が4バイトにアライメントされているから、パディングなしで構造体のサイズが4の倍数なら、パディングは不要ってこと。個々のデータ型にはそれぞれアライメントがあって(例えば、boolcharは1、shortは2、intは4、longは8など)、複合型(構造体みたいな)のアライメントはその構成要素の最大アライメントにデフォルトで設定される。この文では、struct Monsterのアライメントは4で、著者の設定ではintfloatが4のアライメントを持ってるから。intの一つをlongに変えると、CPUによってはアライメントが8になるかもしれないし、intfloatのフィールドを削除すると、ほとんどのCPUでアライメントが1に下がるよ。

それと、これはCPUやコンパイラに特有のことも覚えておいてね。あるコンパイラでは、すべてを4/8バイトでパックしてたけど、通常は8バイトだった。1/2/4/8ではなくてね。それは、データアクセスをうまくやらないとCPUがセグフォルトしちゃうからだった。コンパイラは、オフセットやメモリ移動、シフトを設定すると多くのことを隠してくれたけど、賢いけど遅かった。だから、デフォルトで余分な命令を排除するために十分広いパッキングを選んで、メモリを多く使うことになった。x86はその時一番許容度が高かったけど、ARMは一番厳しかった(少なくとも私が使ってたプラットフォームでは)。MIPSは場合によっては大丈夫だったけど、他のケースではそうでもなかった。

つまり、著者は間違ってる。struct s { char x; };は4バイトアラインされる必要はないんだ。1バイトのサイズを持つこともできるし、だからアラインメントもそうなる。

それに、boolは4バイトかもしれないし、charは2バイト(m68k)かもね!

知ってる限りでは、アライメントはもうあまり重要じゃない(少なくともCPUデータに関しては)。現代のCPUの「ワードサイズ」はキャッシュラインのサイズ(32バイトか64バイト?)だから、例えば32バイトや64バイトのブロック内での非アライメントアクセスは、アライメントされたアクセスと変わらないんだ。ただし、サイズに合わせてアライメントされたアイテムは隣接するキャッシュラインをまたぐことがないっていう利点はまだあるよ。それに、GPUデータを扱うときはまだたくさんの異なるアライメント要件があるし、面白いことにそのアライメント要件はCのアライメントルールとは異なることもあるから、手動でパディングを使ってパックされた構造体を明示的に使う必要があるかもしれない(これも標準Cの機能じゃないけど)。

個々のデータ型にはそれぞれのアライメントがある(例えば、boolcharは1、shortは2、intは4、longは8など)。複合型(構造体のような)のアライメントは、その構成要素の最大アライメントにデフォルトで設定される。これは実装に依存することを付け加えておくよ。確か、標準が構造体のアライメントに課す唯一の制約は、ポインタがその最初のメンバーへのポインタでもあること、つまりアライメントは実質的に最初のフィールドの倍数でなければならないってことだよ。

著者は「speed」の後の未使用のパディングバイトにビットフィールドを収めることに依存してるけど、それは実装依存の動作なんだよね(ビットフィールドに関するほとんどのことが実装依存)。MSVCはunsignedビットフィールドをalignof(unsigned)にアライメントするけど、GCCやclangはパディングバイトを使う。構造体をポータブルにパックするには、unsigned整数を使ってフラグ(monster.flags & CAN_FLYのように)を使うといいよ。詳しくは https://c.godbolt.org/z/K7oY77KGj を見てね。

将来のCのリリースでビットフィールドの標準が定義されないのはなぜ?これは本当に使いたい機能の長年の議論されてきた欠点なんだよね。

Hacker Newsで議論の続きを見る