パスワードを忘れた? アカウント作成
7324358 journal
プログラミング

Yoh2の日記: アライメント揃ってなくても大丈夫だというのは思い込み? 12

日記 by Yoh2

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命令を使ってくれるのは歓迎なんだけど、アライメントなんて無視して横着したい身としてはこの最適化は厳しい。

んで結局これは (コンパイラの) バグなの? それとも (プログラムの) バグなの?

この議論は賞味期限が切れたので、アーカイブ化されています。 新たにコメントを付けることはできません。
  • by Anonymous Coward on 2013年02月01日 22時02分 (#2317643)

    なのでプログラムのバグ。

    • どの辺がダメなんでしょう?
      void * → char * : 言うまでもなくOKだと思います。
      char *型に +1 : これもOKだと思います。
      char * → uint64_t * : 異なるオブジェクト型のポインタの相互変換はOKだったと思いますがダメでしたっけ?

      まあ、これが未定義だとしても別の方法で奇数アドレスを持つ、十分な大きさの有効なオブジェクトをsrcに代入すればいいわけで。
      そもそも未定義とならない方法でそれを実現できる方法はなかったりします?

      --
      巧妙に潜伏したバグは心霊現象と区別が付かない。
      親コメント
      • by takl (14577) on 2013年02月02日 1時16分 (#2317726)

        元ACとは別人ですが。

        6.3.2.3 Pointers の 7 に

        A pointer to an object or incomplete type may be converted to a pointer to a different
        object or incomplete type. If the resulting pointer is not correctly aligned57) for the
        pointed-to type, the behavior is undefined.

        とありますので、 char * を uint64_t * に変換した時点で C99 的には undefined だと思います。
        何が correctly aligned かは規格書中では述べられていない(と思う)ので
        コンパイラのバグかどうかはちょっとわかりませんが…。

        あと、 -O3 に -ftree-vectorize が含まれているので SIMD の使用は明示されてしまっていると思います。

        親コメント
        • 逆参照した場合の挙動が処理系定義 or 未定義と覚え違いをしていました。ポインタの時点でそうなんですね。
          が、同じ 6.3.2.3 Pointers の 5 で

          An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap reprensentation.

          とありますので、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と一致しますね。

          調べるべきことが残っている上に増えてもいますが、とりあえずはここまで。

          --
          巧妙に潜伏したバグは心霊現象と区別が付かない。
          親コメント
          • by Anonymous Coward

            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 の使用は明示されて

  • by Anonymous Coward on 2013年02月02日 2時07分 (#2317737)

    x86系って、SIMDを明示的に使わない限り、アライメント境界が揃っていないデータ転送プログラムを書いても、パフォーマンスさえ気にしなければ特に問題ないという認識でいた。

    SIMD使わなくても、アラインメント狂っていたら例外出されておかしくない。
    alignment-tolerant processorsの場合は速度低下だけだが、alignment-strict processorsの場合は、不正アラインメント例外だ。

    • by Anonymous Coward

      そんなことは承知の上でx86の話をしているというのに、トンチキな奴だな

      • by Anonymous Coward

        ACフラグも知らんのかにわかが

      • by Anonymous Coward

        x86は比較的緩かったとは思うが何でもありなぐらいにフリーダムだったっけ?

        • by Yoh2 (6924) <yoh2@d2.dion.ne.jp> on 2013年02月02日 13時47分 (#2317873) 日記

          フリーダムだという保証があるのかフリーダムだと思い込んでいただけなのか、そこが問題なわけで。
          ロクに調べないうちに日記を書いたので、もう少し調べてからにした方がよかったかもしれませんが。
          といっても 、過去にvoid * と関数へのポインタ の相互変換を認める追加ルール (※) を探しても見付けられなかった程度の調査力なので、きちんと調べても調べ切れるかどうか。

          ※ C99以降 (それ以前は知らん) では、関数へのポインタは他の関数へのポインタへの変換以外は規定されていないので、追加ルールがないとvoid *との相互変換は未定義。そしてこれが未定義になるとdlsymの存在意義に関わってくるのでどこかで追加ルールを設定しているはず。

          しかし、もしアライメントが揃っていないアドレスへのアクセスがダメとの結果が出たら過去に書いたIPヘッダとかISO9660とかのパーサが……
          奇数アドレスからの複数バイトフィールドとか、4で割り切れないアドレスからの4バイトフィールドとかあるし。

          --
          巧妙に潜伏したバグは心霊現象と区別が付かない。
          親コメント
          • > 奇数アドレスからの複数バイトフィールドとか、4で割り切れないアドレスからの4バイトフィールド

            そういうのは、バイト単位でアクセスしてプログラムコード的にシフト等駆使して16bitなり32bitとの相互変換するのがまっとうなやり方でしょう。
            アライメント以前に、エンディアンはどうなってるの?と言う点で動作保証がないし。

            親コメント
            • by Yoh2 (6924) <yoh2@d2.dion.ne.jp> on 2013年02月02日 16時33分 (#2317943) 日記

              自分のところ(x86とppc)で動けばいいや、というプログラムの場合、アライメントを気にせず読み込んでhtonlなどでバイトオーダー調整なんぞやってたもんで。memcpyすらタイプを面倒がってました。

              --
              巧妙に潜伏したバグは心霊現象と区別が付かない。
              親コメント
typodupeerror

吾輩はリファレンスである。名前はまだ無い -- perlの中の人

読み込み中...