以前このサイトとブログに,何度かアラインメントに関する記事を書きました (サイト内関連ページ参照). そのせいか「アラインメント」で検索して来てくれる人が多いので, 過去の記事に加筆修正してこのページを新たに作成しました.
加筆した点は次のとおりです.
C言語等の解説サイトでは, 構造体のアラインメントについて解説しているところも多いのですが, その場合問題にしているのはメンバのアラインメントであって, 構造体全体のアラインメントについては何も触れていません. そのうえメンバは (暗黙のうちに) 基本データ型という前提になっています.
つまり複合データ型それ自体のアラインメントはどうなるのか,
ということに触れているサイトはまだ見たことがありません.
しかし上の追記に挙げたのサイトも, 「なぜ複合データ型のアラインメント規則がそうなっているのか」 ということは説明していません.
たぶんこのページは,日本中で最も (もしかしたら唯一の?)
詳しくアラインメントを解説したページだと思います.
(2007/01/14(日) 現在)
このページの主な更新は Blog でお知らせします.
以前某所で malloc() のアラインメントについて書いたとき, 「それはC言語の仕様なのか?」と訊かれたことがある. また「C言語 アラインメント」などで検索してここに来る人も多い. アラインメントを C/C++ の問題だと思っている人が多いようだが, 特定のプログラミング言語に起因する問題ではない. アラインメントは機能ではなく制約なので, わざわざそんなものを好きこのんで言語仕様に入れるメリットは何もない.
アラインメントは CPU のハードウェアに起因する問題であり, データのメモリアドレスに関する制約である. C/C++ (そしてアセンブラ) でそれが問題となる理由は, データのメモリアドレスがプログラマに見える (操作できる) 言語だからである.C/C++ のアラインメントに関する仕様は, CPU の制約を「代弁」しているにすぎない.
CPU が扱うデータ型は,基本的には整数型 (と浮動小数型) だけである. 文字列は文字コード (整数型) の配列として表現される. 32ビット CPU では普通,扱える整数型のサイズは1,2,または4バイトである. これらはC言語ではそれぞれ,(signed または unsigned) char,short,int (または long) 型と呼ばれる. 浮動小数型には4バイト長の単精度型 (C言語の float 型) と, 8バイト長の倍精度型 (C言語の double 型) がある. (x86 ファミリーの中には,8バイト整数,16バイト整数, 10バイト拡張倍精度浮動小数を扱えるものもあるが,ここでは割愛する.)
最近のほとんどの CPU は,メモリ上のデータを1バイト単位で読み書きできる (バイトマシン)注1. つまりメモリは,巨大なバイト配列と考えることができる. 1バイト整数をメモリに格納する場合,どれでもいいから1つの配列要素を選び, そこに格納すればよい.その配列要素の番号がメモリのバイトアドレスとなる. (C言語の配列と同じく,要素番号 (バイトアドレス) は0から始まる.)
2バイト以上の整数型や浮動小数型を格納するには複数の配列要素が必要なので, 例えば4バイト整数なら4つの連続する配列要素に格納する. この場合,最初の配列要素の番号が4バイト整数のバイトアドレスとなる. 注意すべき点は,1バイトデータの場合とは異なり, 複数バイトデータのバイトアドレスは好きなように決められるわけではないということである.次にこの点について説明する.
このページの AOF32L(l) というマクロは,変数 l
のアドレスを返すが,Cray
の場合だけはそれより4バイト後ろのアドレスを返すらしい).ということは,&l
がワードの先頭で AOF32L(l) が有効な4バイトの先頭なのか?
Cray では,構造体の先頭メンバや共用体のメンバが short 型の場合, そのオフセットは4になるのかもしれない.
CPU とメモリの間は,データをやりとりするための電線の束で結ばれている.
これをデータバスという.32ビット CPU では普通,この電線は32本あり
(32ビット・データバス,下図),CPU はメモリ上のデータを一度に32ビット
(=4バイト) 読み書きすることができる注2.
このため,メモリの最初の4バイト (アドレス0〜3),次の4バイト
(アドレス4〜7),更に次の4バイト (アドレス8〜11)
… は,それぞれ一度で読み書きできる.
普通,このような4バイトをワード (word:語) と呼ぶ.
(注意:x86 など 8/16 ビットから進化してきた CPU
シリーズでは,16ビット版との互換性のため2バイトをワード,
4バイトをダブルワードなどと呼ぶものが多い.
逆に最初から32ビットとして登場した CPU シリーズでは,
32ビットをワード,16ビットをハーフワードと呼ぶものもある.)
したがって4バイト整数または4バイト浮動小数をメモリに格納する場合, 先頭アドレスが4の倍数になるようにすれば上図の1ワードに収まるため, 1回のメモリアクセスで読み書きできる. このようにデータの先頭アドレスを4の倍数にすることを, 「4バイト境界にアライン (align:整列,位置合わせ) する」という.
■参考
では,4バイトデータが4バイト境界にアラインされていない
(4バイト境界をまたぐ,ともいう) 場合はどうなるのか?
この場合,4バイトデータXは下図の例のように複数ワードにまたがっている.
(Y,Zは別のデータ)
このようなXを読み書きするように命令された CPU はどうするだろうか? 「2回に分けて読み書きする」と思うだろう.x86 は実際そのように動作する (ただし後述する場合を除く). 当然読み出しにはメモリアクセス1回分余計に時間がかかる. 最近の (キャッシュを持つ) CPU にとって,メモリアクセスは非常に時間のかかる処理である. 例えばメモリ上のデータを読み書きする命令は,それがキャッシュに入っていない場合, 最も速い命令 (整数型の加減算やビット演算など) に比べて約100倍以上の時間がかかる.
ここまでは、メモリは1サイクルか2サイクルでアクセスができるという想定でパイプラインを考えてきたが、この想定がある程度成り立っていたのは30年以上も昔のことである。それ以降の30年で、プロセサのクロックは1000倍程度速くなったが、メモリのアクセスタイムは10倍程度しか向上しておらず、結果として、現在のマイクロプロセサがDRAMで構成されたメモリをアクセスするには、100サイクル以上を必要とするようになってきている。
メモリのロード、ストアを行う命令の出現頻度は、典型的なプログラムでは30%程度である。
(中略)
実行ユニットは、毎サイクル1命令を実行する能力があるが、メモリアクセスに45倍もの時間がかかり、1秒間に実行できる命令数はクロック周波数の1/46に低下してしまう。
さらに,アラインされていないデータがワード境界どころかキャッシュライン境界をもまたいでいた場合, もう一つのキャッシュライン (普通は32〜256バイトらしい) も読むはめになってしまう!
書き込みの場合はもっとメモリアクセス回数が増える場合もある. 仮に CPU が図2の2ワードに対して単純にXを書き込んだらどうなるだろうか? YとZが巻き添えを食らって (無効な値に) 書き替えられてしまう. これを防ぐには,書き込み前に図2の2ワードを読み出しておかなければならない. つまり合計4回のメモリアクセスが必要になる.
ただし CPU がワードに書き込みを行う際, ワード内のどのバイトに書き込むかを選択できる回路がついていれば, そのような問題は発生しない. この場合のメモリアクセス回数は読み出しの場合と同じである.
x86 以外の多くの CPU (特に
RISC)
は上記のようには動作せず,エラーとして処理する.
つまり「不正アラインメント例外注3」を発生させる.
このとき動いている OS が UNIX 系ならば,SIGBUS というシグナルが発生してコアダンプするというオチがつく
( HP-UX での実例 ).
他の OS でも同様だろう.
なぜエラー扱いするのかは CPU の設計者に聞いてみないとわからないが, まず間違いなく CPU の回路を単純にするためだろう. ワード境界をまたぐデータを2回に分けてアクセスするとなると, それだけ制御回路が複雑になる.特に RISC はそもそも次のような設計思想で生まれたものである.
だから1つの基本データ型を2回に分けてアクセスするなどということは, RISC にとっては反革命思想・アラインメント違反罪であり,UNIX 共栄圏では SIGBUS の刑に処せられる.(笑)
(そのうち暇ができれば, アラインメントと CPU 回路の単純化の関係について追記するつもり.)
ARM も RISC なので不正アラインメント例外を発生するが, ARM Linux カーネルは例外ハンドラで x86 と同様の「アラインされていないデータのアクセス」を実現しているらしい. ただしこれは x86 に比べて非常に遅いはず.
2007/10/25(木) 追記
CPU によっては,アラインメント例外を発生させる代わりに,
アドレスの下位ビットを強制的に0と見なして処理するものもある (↓実例).
2009/05/30(土) 追記
参考:ハードウェアを意識したプログラミングの基礎(後編)
x86,ARM,PowerPC,MicroBlaze でアラインされていないデータにアクセスしてみる実験.
ARM v6 以降は寛容になったらしい.
2010/07/03(土) 追記
逆に x86 がアラインメントに寛容な理由は,最初からそういう仕様で設計されたわけではなく,8bit CPU 時代 (1974年の
i8080)
からの命令互換性を連綿と維持するためそうせざるを得なかったからにすぎない.
x86 は (アーキテクチャに関してはボロクソに言われながらも)
互換性の維持によって現在の地位を築くことができたのである.
(i8080 → i8086
はアセンブラソースレベル互換,i8086 以後はバイナリ互換.)
■参考
8080や8085は8ビットCPUとしてよく売れたため、新しい16ビットCPU 8086の開発に当たって、インテルは8ビットCPU用に書かれたソフトウエア資産の継承に配慮した。
(中略)
8086は8ビットCPUとの連続性を重視した結果として、そのアーキテクチャが「汚い」とか「洗練されていない」とか言われることがある。 これに対して、Motorola社の68000は、8ビットCPUとの連続性を考えずに設計されたこともあり、そのアーキテクチャは「美しい」と言われている。
(中略)
しかしそれにも関わらず、販売量という点ではMotorola社はインテル社に比べてまったく及ばなかった。
(中略)
6809という6800系の8ビットCPUの開発においても同様であった。6809は「究極の8ビット・マイクロプロセッサ」と一部で呼ばれるほど優れたアーキテクチャを持っていたが、それ以前の6800系8ビットCPUと互換性を持っておらず、それらのCPUで動作していたプログラムが動作しなかった。Motolora社は、インテル社と異なり、それ以前のソフトウェア資産を重視しなかったのである。
過去の製品からの連続性(上位互換)を維持するためには止むを得ないのであるが、この辺りがx86アーキテクチャの汚いところである。このため、 Intelのアーキテクトがこれを清算したいと考え、HPと協力してItaniumのIA-64アーキテクチャを開発したのは無理からぬところであると筆者は思うのであるが、その結果はご存知の通りである。これはアーキテクチャが技術だけでは決まらないという好例であろう。
2013/01/02(水) 追記
x86 でも,次の場合は不正アラインメント例外を発生する.
Visual C/C++ では,SSE のためのデータ型 (__m128 など) をサポートしているが, 32bit Windows 用の malloc,new,Win32 アロケータ等は8バイト・アラインメントしか保証していないので, これらを用いて SSE 用のデータ型を含む構造体などを確保すると, アラインメント違反 (一般保護例外 #GP) が発生する場合がある. DirectXMath / XNA Math / D3DX 等,SSE 対応のライブラリを使用する場合は _aligned_malloc() 等を使用する必要がある.
align (Visual C++ Language Reference)
To create an array whose base is properly aligned, use _aligned_malloc, or write your own allocator. Note that normal allocators, such as malloc, C++ operator new, and the Win32 allocators return memory that will most likely not be sufficiently aligned for __declspec(align(#)) structures or arrays of structures.
Windows 7 (SP1) で確認したところ,どうやら DispatchMessage() がこっそり catch して握りつぶしているらしく, 例外が発生してもエラーダイアログが出ないままメッセージループが継続するので気づかない.
何バイト境界にアラインすればよいか (これをアラインメント注4 (alignment) という) は,CPU とデータ型によって異なるが,一般には2の冪乗 (1,2,4,8,16,…) になる. 基本的なデータ型のアラインメントは,その型のサイズ (バイト数) に一致することが多い. つまり1バイト整数のアラインメントは1バイト, 2バイト整数のアラインメントは2バイト, 4バイト整数/単精度浮動小数のアラインメントは4バイト, 8バイト倍精度浮動小数のアラインメントは8バイトである.
ただし例外もある.例えばデータバス幅より大きい基本データ型をサポートする CPU では,データバス幅がアラインメントの上限になる場合もある (最初に書いたメモリアクセス回数の話からすれば当然). 例えば16ビット CPU H8/300 シリーズは4バイト整数型をサポートするが,そのアラインメントは2バイトである. (H8 の他のシリーズは使ったことがないので未確認.)
たまに「(CPU名) (データ型名) アラインメント」等で検索してくる人がいるけど, 最初に見るべきなのはコンパイラか CPU のマニュアル. そんな検索ワードじゃ無関係なページが山盛り出てくるうえ, 自分の環境と違うものを間違えて見つけてしまうかもしれない. 手抜きをするつもりが時間の浪費になってしまう. 最初からマニュアルを見る方が早くて確実.
プロセッサによっては, サイズが2の冪乗でない基本データ型 (24ビット整数や48ビット整数,80ビット浮動小数など) を持つものもある. それらのアラインメントについては個別に確認する必要がある. そのようなデータ型をC言語で扱う場合には注意が必要である. (後述の「sizeof とアラインメントの関係」を参照.)
- ここでは説明の都合上C言語の用語や構文を使うが,特定の言語 (特に C/C++) の複合型の仕様についての話ではなく, メモリ上の物理的なデータ配置に関する CPU の制約の話である. したがって言語仕様よりも強制力が強い. (言語の規格に従わない「なんちゃって言語」でも動作するプログラムは書けるが, アラインメント制約を守らなければ動作しなかったり, 速度低下を招いたりする.)
- ここでいう「構造体」,「共用体」,「配列」の定義 (たぶんCの規格よりも厳しい定義).
- 不要なパディングを入れない (特に先頭部分).
⇒ 構造体の最初のメンバ,配列の先頭要素, 共用体の全メンバのオフセットは必ず0.- 構造体は指定された順序でメンバが並ぶ.
- 「正味のサイズ」の定義 (配列と共用体の場合についても追記すべき).
- C++ の class/struct については,隠れたメンバ変数 (仮想関数テーブルへのポインタ) を持つ場合があったり, オフセット計算の基準位置が状況によって変わったり, ソースコード上の見かけから想像されるのとは異なるデータ構造 (仮想基底クラス) があったりするので, ここでは全く対象外.言語仕様書やソースコードだけをいくら眺めててもダメで, 処理系の ABI を理解しなければ確かなことはわからない.
ある複合データ型全体のアラインメントを求めるには, 次のように考えてみるとわかりやすいだろう.
複合データが,ある適切な (つまりアラインされた) アドレスに配置されている. このデータのアドレスを少し移動させたい場合, 何バイト単位で移動させることができるか?
複合データ型の変数 D に含まれる各要素データを Di,そのアラインメントを Ai とする (i=1,2,…,n). Di は Ai バイト単位でしか移動できないので, D もまた Ai バイト単位でしか移動できない. これをすべての Di について満たすためには, D は A1,A2,…,An の最小公倍数 LCM(A1,A2,…,An) バイト単位でしか移動できない.したがって D のアラインメントは LCM(A1,A2,…,An) である. アラインメントは2の冪乗でなければならないから, それらの最小公倍数は最大値に等しい.したがって D のアラインメントは Max(A1,A2,…,An) である.
つまり (バイトマシンでは) 複合データ型のアラインメントは, それに含まれる要素のアラインメントの中で最も大きい (厳しい) ものに等しい. 特に配列の場合はすべての要素が同じ型なので, 配列のアラインメントは要素のアラインメントに等しい.
複合データ D のバイトアドレスを Addr(D) と書くことにする. D の要素 Di の,D の先頭からのバイトオフセットを Offset(Di) とすると,次式が成立する.
Addr(Di) = Addr(D) + Offset(Di)
D が構造体 (型名を D_t とする) の場合, 上の式をC言語の構文で書けば次のようになる.
assert((char*)&Di == (char*)&D + offsetof(D_t, Di)); struct D_t ┏━━━┓←┬─ &D (== &D0) ┃D0 ┃ │ ┠───┨ │ ┃D1 ┃ │offsetof(D_t, Di) … Ai の倍数でなければならない. ┠───┨ │ : : │ : : ↓ ┠───┨←┴─ &Di ┃Di ┃ ┠───┨ : : : : ┠───┨ ┃D(n-1)┃ ┗━━━┛
Di のアラインメントを Ai とすると, Addr(Di) は Ai の倍数である (アラインメントの定義). また「複合データ型全体のアラインメント」 に書いたことから,Addr(D) も Ai の倍数である. したがって Offset(Di) もまた Ai の倍数でなければならない.つまり複合データ型の要素のオフセットは, その要素のアラインメントの倍数でなければならない.
「構造体メンバのオフセットは, そのメンバのアラインメントの倍数に切り上げられる (パディングが挿入される)」ということは, C言語を解説している本やサイトでよく見かけるが,その理由は上記のとおりである. これをちゃんと説明しているサイトはほとんどない. まして,複合データ型のアラインメントを解説しているサイトは全く見たことがない (本はどうだか知らないが).
なお,共用体の場合はメンバのオフセットは常に0なので, このことを考慮する必要はない.
D が配列の場合,最初の要素はオフセット0なので何の問題もない. 要素のサイズがアラインメントの倍数の場合には, 次の要素は前の要素のすぐ後に続けて配置すればよい. そうでない場合には, 要素サイズをアラインメントの倍数に切り上げたオフセットに配置しなければならない. 3番目以降の要素も同様である.したがって配列要素の間隔 (stride) は, 要素の正味のサイズ注5ではなく, それをアラインメントの倍数に切り上げた値でなければならない.
普通の基本データ型ではサイズとアラインメントが同じなのでこの点は問題ないが, 構造体を配列にしようとすると問題になる可能性がある. 例えば次の構造体を配列にすることを考えよう.
typedef struct { int a; // サイズ,アラインメントとも4バイトとする. char b; // サイズ,アラインメントとも1バイト (Cの仕様). } S_t;
b は1バイト・アラインメントなので, アラインメント調整は必要なく,a と b の間にパディング (隙間) ができることもない.したがってこの構造体 S_t の正味のサイズは4+1=5バイト, アラインメントは最も厳しいメンバである a と同じ4バイトである. S_t を配列にする場合,要素間隔は正味の S_t のサイズ (5バイト) を S_t のアラインメント (4バイト) の倍数に切り上げた8バイトとなる. つまり b の後ろには3バイトのパディングが必要.
実は,C言語の sizeof 演算子が返すのは構造体の正味のサイズではなく, それを配列にしたときの要素間隔である.つまり sizeof(S_t) == 8 (「sizeof とアラインメントの関係」を参照).
2017/03/26(日) 追記
Swift の
MemoryLayout
では正味のサイズ (MemoryLayout<構造体名>.size)
と要素間隔 (MemoryLayout<構造体名>.stride)
をちゃんと区別しているようだ.(C/C++ もこうだったら,#pragma pack
などでアラインメントをないがしろにしなくても (配列要素ではない)
構造体の末尾パディングをなくすことができるのに….)
2012/03/03(土) 追記 (「共用体 サイズ」,「共用体 パディング」等で検索して来る人がいるので.)
「sizeof(共用体) はメンバのサイズの最大値」と思っている人が多いと思うが, それだけでは不正確.上記の構造体の場合と同様,sizeof の仕様により共用体全体のアラインメントの倍数に切り上げなければならない.
// 前提:double 型のサイズおよびアラインメントは8バイトとする. typedef union U { char string[17]; // サイズ:17バイト,アラインメント:1バイト. double d[2]; // サイズ:16バイト,アラインメント:8バイト. } U_t;
U_t の正味のサイズはメンバのサイズの最大値 max(17, 16)=17 バイト.
U_t のアラインメントはメンバのアラインメントの最大値 max(1, 8)=8 バイト.
sizeof(U_t) は U_t の正味のサイズを U_t のアラインメントの倍数に切り上げた24バイト.
したがって最大メンバ string の後にも7バイトのパディングが入る.
(実例:VC2008 による実験結果)
構造体のアラインメントに関して,次のように誤解 (中途半端に理解) している人を見かけたことがある.
以前から「構造体 4バイト(境界)」とか「構造体 4の倍数」などの検索ワードが多かったので, (32ビット CPU では) 構造体のアラインメントは常に4バイトだと思い込んでいる人が多そうで心配になる. さらに悪いことに,冒頭の図1を見て「そうか,だから4バイトなのか!」と早トチリした人もいそうだ.(苦笑)
実際,(このページを読んでもまだ)
この誤解をしている人をネット上で2,3人見かけた.
(ということは全国に少なくとも2〜3億人は隠れてるってことですね,わかります.)
「32 (64) ビット CPU だから構造体のアラインメントは 4 (8) バイト」などと, CPU のビット数やデータバス幅がそのまま構造体のアラインメントになると考えるのはワードマシンの発想であって,バイトマシンのアラインメント規則ではない. ワードマシンでは,ワード単位でしかメモリにアクセスできないのだから, (構造体に限らず) すべての変数のアラインメントがワード単位になるのはむしろ当然. それどころか,ワードマシンはバイトという概念ができる前から存在していた.
昔のコンピュータはワードマシンが多かった (らしい).
バイトの概念ができる前は当然すべてワードマシン.
ミニコン (1960年代 〜 1980年代前半) とか,
1980年代の AI (人工知能) 研究ブームでもてはやされた
Lisp マシンとか.
(Symbolics の Lisp マシン,使ってみたかった….
ちなみにこれは36ビット (32ビットではない) のワードマシン.)
しかし今時の汎用 CPU のほとんどはバイトマシンであって,ワードマシンはごく一部のメインフレームや特殊な専用プロセッサぐらいしか残っていないだろう. 最近の GPU は (実際のハードウェアはどうか知らないが,Direct3D という API を通して見る限りは) 32ビットワードマシンである. (DSP もワードマシンかもしれないが,使ったことがないのでよく知らない.)
「構造体 32ビット(4バイト)」とか「構造体
64ビット(8バイト)」などで検索して来る人は,
一体どんなワードマシンを使っているんだろう?
(GPU 以外で) 32ビットや64ビットのワードマシンって聞いたことないけど….
そうか,きっと GPU のドライバ開発者か Direct3D プログラマなんだろうね.(笑)
さらに追い討ちをかけると(笑),そもそも「32bit」,
「64bit」と一括りにしている点で最初っからダメ.(64bit の場合について言えば)
自分が使っている処理系が
LP64 なのか,LLP64 なのか,それ以外 (ILP64,SILP64 など)
なのか,理解してますか?
(もっともこれらは基本データ型のサイズによる分類なので,アラインメントについては結局各処理系のマニュアルで確認する必要がある.)
バイトマシンを使っていて,ここに書いた誤解について身に覚えのある人は, 下記の構造体のサイズとアラインメントがいくつになるか, 自分が使っている処理系で確認して反省してください.(笑)
// ●前提 (char 以外については処理系依存) // ・char のサイズおよびアラインメントは1バイト (Cの仕様). // ・short のサイズおよびアラインメントは2バイトとする. // ・int,long,float のサイズおよびアラインメントは4バイトとする. // ・long long,double のサイズおよびアラインメントは8バイトとする. // x86 用 gcc では -malign-double オプションを指定すること. // (デフォルトでは,double および long long のアラインメントが4バイトになっている. // これはデータバスが32ビットだった 386,486 との互換性のためだろう.) // ●構造体/共用体の定義 struct S1 { short s; unsigned char uc[3]; }; struct S2 { char string[10]; unsigned char uc; }; struct S3 { double d; long long ll; char c; }; // sizeof(共用体) で説明した例 typedef union U { char string[17]; double d[2]; } U_t; // ●型のサイズとアラインメントを出力するマクロ // sizeof の値を printf("%d") してる人が多いけど, // size_t 型 (無符号) であって絶対 int 型じゃないよ! // それに unsigned int と同じとも限らない. // size_t 型とは,「メモリ上の任意のデータのサイズ」を表すための移植性のある型. // (MS-DOS 用の Microsoft C コンパイラでは 64KB までしか表せなかったけど….(昔話))) #if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) // C99 以後 // size_t の printf 書式は %zu. static const char format[] = "sizeof(%s)=%zu, AlignmentOf(%s)=%zu.\n"; #define PrintSizeAndAlignment(type) \ printf(format, #type, sizeof(type), #type, AlignmentOf(type)) #elif defined(_MSC_VER) // Visual C/C++ // size_t の printf 書式は %Iu. static const char format[] = "sizeof(%s)=%Iu, AlignmentOf(%s)=%Iu.\n"; #define PrintSizeAndAlignment(type) \ printf(format, #type, sizeof(type), #type, AlignmentOf(type)) #else // その他の処理系 // size_t の printf 書式が不明なので unsigned long にキャストして出力する. static const char format[] = "sizeof(%s)=%lu, AlignmentOf(%s)=%lu.\n"; #define PrintSizeAndAlignment(type) \ printf(format, \ #type, (unsigned long)sizeof(type), \ #type, (unsigned long)AlignmentOf(type)) #endif // ●型のサイズとアラインメントを出力する. int main(void) { // 基本データ型 (整数型) PrintSizeAndAlignment(char); PrintSizeAndAlignment(unsigned char); PrintSizeAndAlignment(short); PrintSizeAndAlignment(unsigned short); PrintSizeAndAlignment(int); PrintSizeAndAlignment(unsigned int); PrintSizeAndAlignment(long); PrintSizeAndAlignment(unsigned long); PrintSizeAndAlignment(long long); PrintSizeAndAlignment(unsigned long long); printf("\n"); // 基本データ型 (浮動小数型) PrintSizeAndAlignment(float); PrintSizeAndAlignment(double); PrintSizeAndAlignment(long double); printf("\n"); // 複合データ型 (ユーザ定義) PrintSizeAndAlignment(struct S1); PrintSizeAndAlignment(struct S2); PrintSizeAndAlignment(struct S3); PrintSizeAndAlignment(union U); return EXIT_SUCCESS; }
上記の前提を満たす処理系ならば,結果は次のようになるべき.
sizeof(struct S1)=6,AlignmentOf(struct S1)=2.(2バイト境界に配置される.サイズは2の倍数.) sizeof(struct S2)=11,AlignmentOf(struct S2)=1.(任意のアドレスに配置される.サイズはメンバのサイズの単純合計.) sizeof(struct S3)=24,AlignmentOf(struct S3)=8.(8バイト境界.サイズは8の倍数.) sizeof(union U)=24, AlignmentOf(union U)=8. (8バイト境界.サイズは8の倍数.)
そうなる理由は「複合データ型のアラインメントのまとめ」を読めばわかるはず.
VC2008 (Target:x86) による実行結果は次のとおり.
sizeof(char)=1, AlignmentOf(char)=1. sizeof(unsigned char)=1, AlignmentOf(unsigned char)=1. sizeof(short)=2, AlignmentOf(short)=2. sizeof(unsigned short)=2, AlignmentOf(unsigned short)=2. sizeof(int)=4, AlignmentOf(int)=4. sizeof(unsigned int)=4, AlignmentOf(unsigned int)=4. sizeof(long)=4, AlignmentOf(long)=4. sizeof(unsigned long)=4, AlignmentOf(unsigned long)=4. sizeof(long long)=8, AlignmentOf(long long)=8. sizeof(unsigned long long)=8, AlignmentOf(unsigned long long)=8. sizeof(float)=4, AlignmentOf(float)=4. sizeof(double)=8, AlignmentOf(double)=8. sizeof(long double)=8, AlignmentOf(long double)=8. sizeof(struct S1)=6, AlignmentOf(struct S1)=2. sizeof(struct S2)=11, AlignmentOf(struct S2)=1. sizeof(struct S3)=24, AlignmentOf(struct S3)=8. sizeof(union U)=24, AlignmentOf(union U)=8.
この項目は該当する人が多そうなので以前から追加しようと思っていたんだけど,
忙しいので後回しにしていた.
今回追加したきっかけは,同じ誤解をしているコンパイラ(!)を見つけたから.(↓)
[Armadillo:04936] Armadillo-210の構造体について
構造体の配列を共有メモリに展開するプログラムのデバック中に、実サイズより多くの領域を使用している事が解かり
以下のサンプル作りを実行すると、4バイトと表示しました。struct test{ char ChNo[1]; }w_mem;
アラインメントを変更するコンパイル・オプションや #pragma などを指定せずにこうなっているのだとしたら,このコンパイラもまた, 構造体のアラインメントは常に4バイトだと誤解している可能性がある. double 型や long long 型のメンバが含まれている構造体のアラインメントを4バイトとして扱うと, 不正アラインメントエラーが発生する. こういう災厄を招く勘違いコンパイラは,即刻クビにするのが吉.
正解はいずれも,「メンバのサイズ」ではなく「メンバのアラインメント」である. したがってこれらの誤解が「正しい」のは,「サイズ=アラインメント」である場合, つまりメンバが (普通の) 基本データ型の場合に限られる. メンバが複合データ型や,普通でない (サイズが2の冪乗でない, あるいはアラインメントとサイズが一致しない) 基本データ型の場合には通用しない. (アラインメントについて解説している他のサイトでも,上記のような記述を見かけた.)
確かにCコンパイラは,アラインメントの「調整」はしてくれるが, 普通は「最適化」はしない … というより,C言語の仕様上,してはいけない. ここでいう「調整」とは,上で書いたように 「メンバのオフセットをそのメンバのアラインメントの倍数に切り上げる」 ことで,Cコンパイラが普通に行っている.
一方「最適化」となると, 「構造体メンバの順序を入れ替えて,できるだけ隙間ができないようにする」 としか解釈できないが, C言語の仕様上,コンパイラが勝手にメンバの順序を入れ替えることは許されない.
参考:移植性のあるCプログラミング | 構造体メンバの配置
しかし,上の参考ページにも書いてあるが, Cコンパイラの中にはそういう最適化をしてくれる非標準拡張機能を持つものもあるらしい. その機能を ON にすると,Cの規格に違反することになるので普通じゃありません!
■構造体のパディングをできるだけ避ける方法 (2014/05/03(土) 改定)
パディングを避けるために #pragma pack や gcc 拡張の __attribute__((packed)),アラインメントを (小さくする方向に) 変更するコンパイル・オプションなどを使用するのは,CPU のハードウェアの制約に違反して構造体を強引に力で押しつぶす野蛮な方法なので安易に用いるべきではない.CPU の回路について初歩的な知識があれば,すぐに「タダでまともに動くわけがない」とわかるだろう.つまり #pragma pack 等は,文字どおり「構造体のメンバ配置を詰める (pack)」だけで,普通の方法でメンバにアクセスできることを保証していない場合がある (というより,その方が普通?).
If you change the alignment of a structure, it may not use as much space in memory, but you may see a decrease in performance or even get a hardware-generated exception for unaligned access. You can modify this exception behavior by using SetErrorMode.
訳:構造体のアラインメントを変更した場合,メモリの利用効率は上がるかもしれないが,アラインメントが合っていないメンバにアクセスすると性能低下を引き起こしたり,ハードウェア例外が発生したりする可能性がある.(以下略)
x86/x64 で (SSE 以外の) 非整列メンバにアクセスする場合は遅くなるだけだが,ARM や Itanium の場合,あるいは x86/x64 でも SSE データにアクセスする場合はハードウェア例外が発生する.
注 -
#pragma pack を使用して構造体または共用体のメンバーを自然境界以外の境界で整列させると、 通常、これらのフィールドへのアクセスが発生した場合に SPARC 上でバスエラーが起きます。 このエラーを避けるには (以下略)
ちなみにこのマニュアル内の「厳密な」の原文は "strict" のはずなので, (アラインメントが)「厳しい (つまり大きい)」と訳すべき.
たとえアラインされていないメンバにアクセスできるとしても,
それどころか,そもそもコンパイラが #pragma pack 相当の機能をサポートしていないかもしれない.
パディングができるだけ入らないようにしたければ, アラインメントを考慮して自分で構造体メンバの順序を決めるのが正統なやり方. #pragma pack などを使わなくても, 構造体メンバを適切な順序で並べればすべてのメンバ間のパディングを必ず0にできる (問題:数学的に証明せよ).ただし最後のメンバの後のパディングは sizeof の仕様のため0にできない場合がある. 順序の決め方は中学1年レベルの数学 (倍数と冪乗) が理解できていればわかるはず.
注6: 更新はもちろんだが, 読むだけ・書くだけの場合も複数回に分けてアクセスするのでアトミック性は全く保証されない. このようなメンバを割込み (シグナル) ハンドラや他のスレッドなどが変更する可能性がある場合に排他制御もせずに読み出すと, 変更前の値でも変更後の値でもなく,両者が混ざったデタラメな値 (例えば下位バイトが変更後の値で上位バイトが変更前の値,など) が得られるおそれがある. (2011/08/07(日) 追記)
実例:LONG 型変数についてアトミック操作を行う Win32 API 関数 InterlockedIncrement() などは,CPU が x86 以外の場合や,たとえ x86 であってもマルチプロセッサの場合, 変数が適切にアラインされていないと予期せぬ結果を引き起こす.(2014/03/22(土) 追記)
RemarksThe variable pointed to by the Addend parameter must be aligned on a 32-bit boundary; otherwise, this function will behave unpredictably on multiprocessor x86 systems and any non-x86 systems.
参考
x86 (IA-32 アーキテクチャ) の場合については、32 ビット境界にアライメントが調整されたアドレスへの書き込みはアトミックに行われることが保証されています (インテル、『ソフトウェアデベロッパーズマニュアル下巻』)
それから「(アラインメントを)
気にしなくていい」という点については「あなたが気にしなくても,(大抵の)
CPU は厳しく気にします!」
それに,次のような点も気にしないといけない.(とりあえず,今思いついたものだけ.)
最初の2つの場合に上記の「最適化オプション」を使うと, メンバの順序が勝手に変えられてとんでもないことになる. Memory Mapped I/O において, ハードウェアレジスタを構造体として記述している場合も同様.
構造体メンバの順序を入れ替えて隙間を最小にする最適化アルゴリズムは昔趣味で考えたことがあり,
仕事でも10年ほど前に使った.いずれこのサイトで公開しようと思って,
トップページには開設当初から目次に
(淡色表示で)
書いてあるが,いまだに公開していない….(^^;)
このページへのアクセスが最近急増したので,そろそろ公開しようかな….
「"構造体" "メンバの順序" "C言語"」で Google 検索してみたところ, ビットフィールドのメンバの順序を入れ替えて最適化を行うというコンパイラを見つけた. ↓これは違反じゃない … よね?
ルネサス テクノロジ - よくあるお問い合わせ検索結果
「複合データ型の各要素のオフセット」でも書いたとおり, sizeof(X) の値は,正味のXのサイズ (バイト数) ではなく, それをXのアラインメントの倍数に切り上げた値である.
なぜ sizeof がそういう仕様なのか,わかりますか?
C言語の規格では,n個の要素からなる配列 array[] に関して,
n == (sizeof(array) / sizeof(array[0]))となることを要求している.この仕様は事実上次のことを意味する.
sizeof(X) が返す値は,X の正味のサイズではなく, X を配列要素にした場合の配置間隔 (stride) でなければならない.
両者が異なるのは,構造体型ではよくあることだが, サイズが2の冪乗でない基本データ型でも起こりうるので注意が必要.
余談:C言語の中・上級者向けのサイトのいくつかで, 著者が明らかに上記のことを理解して書いているとわかる記述を見たことがある. しかしなぜか上記のことを明言しているのを見たことがない.なんでだろう?
Cの標準ライブラリに含まれる malloc() (およびそのファミリーである calloc(),realloc()) が返すアドレスは, すべてのデータ型に適合するようにアラインされている (malloc() の仕様).また,コンパイラが alloca() をサポートしていれば,それも同様のアドレスを返すはずである.
多くの32ビット CPU では,(標準的なCがサポートするデータ型の中で) 最もアラインメントの厳しいデータ型は double および long long 型 (8バイト・アラインメント) である (たぶん). したがって多くの32ビット CPU 用の malloc() は, 8バイトの倍数のアドレスを返す.(実例:VC の malloc)
この話題については,こちらのページもご覧ください.
ポインタの下位数ビットは常に0なので, これらをポインタが参照するデータの型を表すタグ等に転用することができる. これをタグ付きポインタ (tagged pointer) という. 1970〜1980年代の LISP マシンは高速化のためタグ付きポインタをハードウェアで処理していた (タグ・アーキテクチャ tagged architecture).
標準的なC言語では,データ型のサイズを取得する sizeof 演算子はあるものの, アラインメントを取得する演算子はない. そこで私は昔 (1993年),アラインメントを取得するための処理系・OS 独立なマクロ AlignmentOf() を考案し, 今でもメモリ管理によく使っている.
Microsoft C では,同様の機能を持つ演算子 __alignof() という演算子があるが, これは実際のアラインメントと一致しない場合があるようである (現在調査中,バグというわけではなくそういう仕様らしい).
GCC には __alignof__ という演算子があるが,これについては未調査.
(そのうち暇ができれば書くつもり.)
「複合データ型の各要素のオフセット」で 「共用体の場合はメンバのオフセットは常に0」と書いた点について, 「C/C++ の規格で保証されているのか?」という質問を受けたので追記. 私は C/C++ の規格には全然詳しくないし, 規格書も持っていないので, 規格に関する質問には直接回答できないことを最初にお断りしておきます. (詳しい方,教えてください. ところで IS って何? Defect Report って何? おいしいの?)
とりあえず手持ちの本などを調べてみると,次のような記述があった.
実際には, 共用体は全メンバーの起点からのオフセットが0である構造体である。(p.180)
5.7.1 共用体構成要素の配置 (p.157)
共用体型のオブジェクトは、そこに含まれるどの 構成要素にも合った記憶域整列境界から始まる。(中略)static union U { ... int C; ... } object, *P = &object;この共用体では、次の2つの等値式が成立する。(union U *) &(P->C) == P &(P->C) == (int *) Pまたこれらの等値式は、構成要素 C の型や、 C の前後の構成要素が何であるかに関係なく成立する。
Unions in various programming languages
C/C++
In C and C++, untagged unions are expressed nearly exactly like structures (structs), except that each data member begins at the same location in memory.
なお,C/C++ の規格書の文言に頼らず自分の頭で考えてみると次のようになる.
まず,共用体を本来の目的である「複数の変数が同じメモリ領域を共用する」 ためだけに使うのであれば, すべてのメンバのオフセットが同じである必要はない. 例えば次の共用体 (各メンバのアラインメントはサイズと同じとする) では, u32 のオフセットは0だが,u16 のオフセットは0または2,u8 のオフセットは0〜3のいずれでもよいはずである.
union { uint32_t u32; uint16_t u16; uint8_t u8; };
しかし現実には,共用体は昔から type punning (あるデータ型のバイト列を別の型として解釈する) のために多用されている. 例えば次のような共用体を用いて float 型のエンディアン変換を行ったり, 浮動小数演算ではなくビット演算だけで符号を高速判定したりすることができる.
union { float f; uint32_t u32; uint8_t u8[4]; };
このような使い方を規格が容認しているのであれば, すべてのメンバを同じオフセットに配置する必要がある. そしてそれは0以外にはない (共用体のサイズを無駄に大きくしない限り). もし容認していないとすると, 低レベルプログラミング言語として必須の type punning ができなくなってしまう (もっとも,変数のアドレスを別の型のポインタにキャストするという代替手段もあるが).
|
省メモリプログラミング―メモリ制限のあるシステムのためのソフトウェアパターン集 (Software patterns series) posted with amazlet at 10.06.12 ジェイムズ ノーブル チャールズ ウィアー ピアソンエデュケーション 売り上げランキング: 80302 おすすめ度の平均: メモリ制限のあるシステム分類が上手い 組み込み向けのデザインパターンとしてはまともです。 すべての設計者・プログラマに必須 |
「省メモリ」とあるが,メモリ管理の高速化についても参考になる技法が解説されている. 時々「malloc 高速(化)」などで検索して来る人がいるが, malloc の速度をこれ以上大きく改善する余地はあまりないと思う (あるとしても非常に難しいだろう).その理由は,
それでもなお malloc の高速化それ自体を目指したい人には「(悲愴な顔で) 頑張ってください」としか言えないが, 「アプリケーションのメモリ管理を高速化したいから高速な malloc が欲しい」というのならあまりにも芸がなさすぎる. そういう人はこの本の「第5章 Memory Allocation:メモリ割当て」を読んで反省してください.(笑)
アプリを高速化したいなら,できるだけ malloc/free を呼び出す頻度を減らすこと. そのためには1回の malloc で確保した大きな領域 (メモリプール) に多数のオブジェクトを詰め込む必要がある (これは省メモリにもなる) が, どのオブジェクトを同じ領域に入れるべきかはオブジェクトの寿命 (extent),サイズ, アラインメントなどを考慮して決める必要がある. 特に,寿命を知っているのはアプリケーションだけだ. 目的に合ったメモリプールならば,malloc/free をそのまま使用する場合に比べて数十倍以上速くなることもある.
■参考
メモリー管理の内側 動的アロケーションの選択肢とトレードオフ、そして実装 (原文)ところで,省メモリが高速化につながる場合も多い.昔からメモリと速度のトレードオフ (高速でメモリを大量に使用するアルゴリズム (例えばテーブル参照) を使うか, それとも低速でメモリを少ししか使用しないアルゴリズムを使うか) がよく問題になるので,省メモリと高速化は両立しないと思い込んでいる人もいるだろう. しかし最近の CPU は命令実行速度に比べてメモリアクセス速度がはるかに遅いので, 無駄なメモリを削減したり,メモリ上のデータ配置を変える (同時期に頻繁に使用するデータをなるべく少数のキャッシュラインに集中させる) と高速化されることも多い. (1970年代以前の CPU は命令実行とメモリアクセスが同期していたので同程度の速度だった.)
さて,ここで問題.次のコードで大きな2次元配列 (例えば画像データ) をコピーする場合,(1) と (2) のどちらが速いか.またその理由を述べよ. (理由を書かなければ0点)
int src[M][N], dest[M][N]; unsigned i, j; // (1) for(i = 0; i < M; i++) for(j = 0; j < N; j++) dest[i][j] = src[i][j]; // (2) for(j = 0; j < N; j++) for(i = 0; i < M; i++) dest[i][j] = src[i][j];
|
はじめて読む486―32ビットコンピュータをやさしく語る posted with amazlet at 09.11.03 蒲地 輝尚 アスキー 売り上げランキング: 10017 おすすめ度の平均: intel80286で挫折し、486から勉強し始めました素晴らしい486アーキテクチャ本 この本は達者! 本当に傑作の一品です。 OSを勉強をしている人にもお勧めの一冊 |
SPARCアーキテクチャ・マニュアル バージョン 8 posted with amazlet at 09.11.03 SPARC International トッパン 売り上げランキング: 1057910 おすすめ度の平均: RISCといえばSPARC |
|
ARM組み込みソフトウェア入門―記述例で学ぶ組み込み機器設計のためのシステム開発 (Design Wave Advanceシリーズ) posted with amazlet at 12.03.17 Andrew N. Sloss Chris Wright Dominic Symes CQ出版 売り上げランキング: 213165 |
|
ARM Cortex‐M3システム開発ガイド―最新アーキテクチャの理解からソフトウェア開発までを詳解 (Design Wave Advance) posted with amazlet at 12.03.17 Joseph Yiu CQ出版 売り上げランキング: 178355 |
CQ出版社の書籍案内 (内容見本PDFあり,立ち読み可)
|
完全版 世界の定番ARMマイコン 超入門キット STM32ディスカバリ: デバッガ搭載&はんだづけ不要!Cortex-M3をホントに始められる (トライアルシリーズ) posted with amazlet at 12.03.17 島田 義人 永原 柊 CQ出版 売り上げランキング: 21953 |
|
ARM9/11/XScaleハンドブック (TECH I Processor) posted with amazlet at 12.03.17 CQ出版 売り上げランキング: 324832 |
|
ARMでOS超入門 (ARMマイコン) posted with amazlet at 12.03.17 桑野 雅彦 岡田 好一 共著 CQ出版 売り上げランキング: 108107 |
|
アセンブリ言語の教科書 posted with amazlet at 09.11.03 愛甲 健二 データハウス 売り上げランキング: 72168 おすすめ度の平均: アセンブリ言語を広範囲に解説した本冗長感は否めない、そして索引も必要 ゆとり教育の教科書 アセンブリ言語の良書 著者の熱意が伝わります |
著者サイト
|
ハッカーのたのしみ―本物のプログラマはいかにして問題を解くか posted with amazlet at 10.06.12 ジュニア,ヘンリー・S. ウォーレン エスアイビーアクセス 売り上げランキング: 27265 おすすめ度の平均: ビットの楽しみたのしみ? たしなみ? ちゃんと読むと得した気分になれます 最後の頑張りに効きます Hackっていうのは、こういうコトさ |
主に2進整数やビットパターンのさまざまな演算技法について解説している. 基本的には (特定のプログラミング言語に依存しない) 数学的な解説が中心だが,C言語によるサンプルコードも示している.
アラインメントやオフセットの計算に使える「2の冪乗の倍数への切り上げ/切り下げ」や, メモリブロックの管理に使える「次の2の冪乗への切り上げ/切り下げ」, (ビット/バイト) エンディアン変換や FFT (高速フーリエ変換) で使われるビットリバース (ビット逆順) などを含む「ビットやバイト単位の並べ替え」など.
|
珠玉のプログラミング―本質を見抜いたアルゴリズムとデータ構造 posted with amazlet at 10.06.20 ジョン ベントリー ピアソンエデュケーション 売り上げランキング: 7857 おすすめ度の平均: アルゴリズムについて勉強したい人には必読の本です楽しく読めるプログラミングの本 「プログラミング」と言う作業を見つめなおすのに最適。「設計する」と言う概念がよく分からない初級プログラマにも 納得!アルゴリズムは重要 プログラマなら読むべき本 |
|
ブライアン カーニハン ロブ パイク アスキー 売り上げランキング: 20088 おすすめ度の平均: 良著です。入門書の次の次の次くらいに! 繰り返し読む必要あり 絶対にお勧めの本です 良いプログラマになりたいあなたに |
|
ガベージコレクションのアルゴリズムと実装 posted with amazlet at 10.06.17 中村 成洋 相川 光 秀和システム 売り上げランキング: 4312 おすすめ度の平均: 擬似コードのバグは見て見ぬふりGCの入門書として今のところ最強! |
|
アルゴリズムクイックリファレンス posted with amazlet at 10.12.31 George T. Heineman Gary Pollice Stanley Selkow オライリージャパン 売り上げランキング: 28776 |
|
Binary Hacks ―ハッカー秘伝のテクニック100選 posted with amazlet at 10.06.12 高林 哲 鵜飼 文敏 佐藤 祐介 浜地 慎一郎 首藤 一幸 オライリー・ジャパン 売り上げランキング: 35295 おすすめ度の平均: 組み込み系の開発者は必携ですハードコア?なソフトウエア 大工さんにおける電動工具の紹介本 当然教科書ではない。でも、とても参考になります。 バイナリアンの基本 |
|
リンカ・ローダ実践開発テクニック―実行ファイルを作成するために必須の技術 (COMPUTER TECHNOLOGY) posted with amazlet at 10.12.18 坂井 弘亮 CQ出版 売り上げランキング: 26556 |
|
Linkers & Loaders posted with amazlet at 10.06.17 John R. Levine オーム社 売り上げランキング: 139529 おすすめ度の平均: プログラムが実行される仕組みが良く分かる概要が書かれた本 ひどい訳 dllのしくみがわかる! パッケージソフト開発者の必読書 |
|
実践 デバッグ技法 ―GDB、DDD、Eclipseによるデバッギング posted with amazlet at 10.06.12 Norman Matloff Peter Salzman オライリージャパン 売り上げランキング: 79214 |
デバッガの理論と実装 (ASCII SOFTWARE SCIENCE Language) posted with amazlet at 10.06.17 ジョナサン・B. ローゼンバーグ アスキー 売り上げランキング: 295384 おすすめ度の平均: デバッグできないとき普通に読んでいくだけでも面白い デバッカの理論には必読 |
|
|
SSE 等の SIMD 命令やキャッシュを意識したプログラムではメモリのアライメントを考慮する必要があります。
これまではコンパイラ毎の拡張命令に頼っていましたが、C++11 では言語仕様に含まれるようになりました。
メモリアクセスの同期命令も含めて、低レベルなメモリ命令を積極的に取り込んでいる印象です。
ここまでは、メモリは1サイクルか2サイクルでアクセスができるという想定でパイプラインを考えてきたが、この想定がある程度成り立っていたのは30年以上も昔のことである。それ以降の30年で、プロセサのクロックは1000倍程度速くなったが、メモリのアクセスタイムは10倍程度しか向上しておらず、結果として、現在のマイクロプロセサがDRAMで構成されたメモリをアクセスするには、100サイクル以上を必要とするようになってきている。
Visual Studio 2005のARMv4I向けコンパイラが構造体アライメントをどのように扱うか実験。
このページの主な更新は Blog でお知らせします.
Copyright © 2006-2020 noocyte, All rights reserved. E-mail: relipmoced (a) yahoo.co.jp (" (a) " を半角のアットマークに書き替えてください.) リンクはご自由に. 「noocyte のプログラミング研究室」トップページに戻る. |