Yoh2の日記: アライメント揃ってなくても大丈夫だというのは思い込み? 12
x86系って、SIMDを明示的に使わない限り、アライメント境界が揃っていないデータ転送プログラムを書いても、パフォーマンスさえ気にしなければ特に問題ないという認識でいた。
が、実はそんな保証がないのではないかという現象に遭遇した。
それはx86_64でuint64_tの配列をコピーするコード。gcc -O3でコンパイルし、コピー元を奇数アドレスにしたらプログラムが落ちた。
試したgccは以下の3種類。いずれも現象発生。
- gcc-4.6.x (詳細忘れ。Ubuntu 12.04)
- gcc-4.6.3 (Gentoo)
- gcc-4.7.2 (Gentoo)
以下、再現コード。上記コンパイラで-O2までは問題ないが-O3で落ちる。
// foo.c -- コピー関数。最適化で呼び出しが消されないようにファイルを分けた。
#include <stddef.h>
#include <stdint.h>
void copy_uint64(uint64_t *restrict dst, const uint64_t *restrict src, size_t n)
{
for(size_t i = 0; i < n; i++)
{
dst[i] = src[i];
}
}
// bar.c -- srcに奇数アドレスを設定してコピー関数呼び出し
#include <stdlib.h>
#include <stdint.h>
void copy_uint64(uint64_t *restrict dst, const uint64_t *restrict src, size_t n);
int main(void)
{
uint64_t *dst = (uint64_t *)malloc(sizeof(uint64_t) * 64);
// アライメントされていないアドレスにする。
uint64_t *src = (uint64_t *)((char *)malloc(sizeof(uint64_t) * 64 + 1) + 1);
copy_uint64(dst, src, 64);
return 0;
}
foo.cをgcc -O3 -Sしてみると、コピーしている部分と思われる箇所のアセンブリコードはこうなっていた。
movdqa (%r11,%rcx), %xmm0
addq $1, %r8
movdqu %xmm0, (%r10,%rcx)
addq $16, %rcx
srcから読み込んでいる部分がSSE命令の movdqa... 、dstに書き込んでいる部分が movdqu。
ここで曲者なのが movdqa。これは指定するアドレスが16バイト境界に揃っていなければならない命令。最適化の結果、これが使われてしまったために落ちていると思われる。
ちなみに、同じ効果を持ち、境界に揃っていなくてもよい movdqu という命令もある。このソースではdstへの書き込みで movdqu が使われている。
そのため、srcを境界整列させ、dstを奇数アドレスにした場合は問題なく実行が完了した。また、-Sで出力させたソースのmovdqaをmovdquに変更したものを使うと、srcが奇数アドレスでも問題なく実行が完了した。
最適化でSSE命令を使ってくれるのは歓迎なんだけど、アライメントなんて無視して横着したい身としてはこの最適化は厳しい。
んで結局これは (コンパイラの) バグなの? それとも (プログラムの) バグなの?
(uint64_t *)((char *)malloc(... が規格上未定義 (スコア:0)
なのでプログラムのバグ。
Re:(uint64_t *)((char *)malloc(... が規格上未定義 (スコア:1)
どの辺がダメなんでしょう?
void * → char * : 言うまでもなくOKだと思います。
char *型に +1 : これもOKだと思います。
char * → uint64_t * : 異なるオブジェクト型のポインタの相互変換はOKだったと思いますがダメでしたっけ?
まあ、これが未定義だとしても別の方法で奇数アドレスを持つ、十分な大きさの有効なオブジェクトをsrcに代入すればいいわけで。
そもそも未定義とならない方法でそれを実現できる方法はなかったりします?
巧妙に潜伏したバグは心霊現象と区別が付かない。
Re:(uint64_t *)((char *)malloc(... が規格上未定義 (スコア:3, 参考になる)
元ACとは別人ですが。
6.3.2.3 Pointers の 7 に
とありますので、 char * を uint64_t * に変換した時点で C99 的には undefined だと思います。
何が correctly aligned かは規格書中では述べられていない(と思う)ので
コンパイラのバグかどうかはちょっとわかりませんが…。
あと、 -O3 に -ftree-vectorize が含まれているので SIMD の使用は明示されてしまっていると思います。
Re:(uint64_t *)((char *)malloc(... が規格上未定義 (スコア:1)
逆参照した場合の挙動が処理系定義 or 未定義と覚え違いをしていました。ポインタの時点でそうなんですね。
が、同じ 6.3.2.3 Pointers の 5 で
とありますので、char *経由ではなくintptr_t経由ならOKのように思えました。またはポインタ経由でもgccで保証があればいいわけですが未調査です。
※ 「previously specified」は、nullポインタと0についての話 (2で規定) のことだと思います。
また、アライメントについての話は、C11で 6.2.8 Alignment of objects という節が追加されています。
ただ、アライメントのサイズがどうなるかという話のみで、アライメントされていない場合のアクセスはどうなるかまでは書かれていません。
なお、C11で追加された _Alignof の結果は、char: 1、short: 2、int: 4、long: 8、float: 4、double: 8 (gcc-4.7.2, x86_64 -- 4.6.3では_Alignof未サポート) でした。sizeofと一致しますね。
調べるべきことが残っている上に増えてもいますが、とりあえずはここまで。
巧妙に潜伏したバグは心霊現象と区別が付かない。
Re: (スコア:0)
7.20.3 Memory management functions の 1 に
The pointer returned if the allocation succeeds is suitably aligned so that it may be assigned to a pointer to any type of object and then used to access such an object or an array of such objects in the space allocated (until the space is explicitly deallocated).
とmalloc系による動的メモリ確保したアドレスは、「任意の」型として利用可能なアライメントが取られるように思えます。
あと、 -O3 に -ftree-vectorize が含まれているので SIMD の使用は明示されて
それ以前に (スコア:0)
SIMD使わなくても、アラインメント狂っていたら例外出されておかしくない。
alignment-tolerant processorsの場合は速度低下だけだが、alignment-strict processorsの場合は、不正アラインメント例外だ。
Re: (スコア:0)
そんなことは承知の上でx86の話をしているというのに、トンチキな奴だな
Re: (スコア:0)
ACフラグも知らんのかにわかが
Re: (スコア:0)
x86は比較的緩かったとは思うが何でもありなぐらいにフリーダムだったっけ?
Re:それ以前に (スコア:1)
フリーダムだという保証があるのかフリーダムだと思い込んでいただけなのか、そこが問題なわけで。
ロクに調べないうちに日記を書いたので、もう少し調べてからにした方がよかったかもしれませんが。
といっても 、過去にvoid * と関数へのポインタ の相互変換を認める追加ルール (※) を探しても見付けられなかった程度の調査力なので、きちんと調べても調べ切れるかどうか。
※ C99以降 (それ以前は知らん) では、関数へのポインタは他の関数へのポインタへの変換以外は規定されていないので、追加ルールがないとvoid *との相互変換は未定義。そしてこれが未定義になるとdlsymの存在意義に関わってくるのでどこかで追加ルールを設定しているはず。
しかし、もしアライメントが揃っていないアドレスへのアクセスがダメとの結果が出たら過去に書いたIPヘッダとかISO9660とかのパーサが……
奇数アドレスからの複数バイトフィールドとか、4で割り切れないアドレスからの4バイトフィールドとかあるし。
巧妙に潜伏したバグは心霊現象と区別が付かない。
Re:それ以前に (スコア:2)
> 奇数アドレスからの複数バイトフィールドとか、4で割り切れないアドレスからの4バイトフィールド
そういうのは、バイト単位でアクセスしてプログラムコード的にシフト等駆使して16bitなり32bitとの相互変換するのがまっとうなやり方でしょう。
アライメント以前に、エンディアンはどうなってるの?と言う点で動作保証がないし。
Re:それ以前に (スコア:1)
自分のところ(x86とppc)で動けばいいや、というプログラムの場合、アライメントを気にせず読み込んでhtonlなどでバイトオーダー調整なんぞやってたもんで。memcpyすらタイプを面倒がってました。
巧妙に潜伏したバグは心霊現象と区別が付かない。