Entry

プログラミングメモ - 静的な配列の初期化の話

2008年10月15日

こちらの話から。配列を定義するときに "= {0}" で初期化する話です。

結局,明示的に初期化されている dest[0] も,明示されていない dest[1]からdest[dest_size-1]もすべて 0 で初期化されるって訳ですね。

構造体なんか(つまり集成体ね)を,初期化を使わず,memsetやらZeroMemoryやらで0埋めしているコードを腐るほど見つけることができますが,へぼいコンパイラでも使っているか,無知か,独特のこだわりをもっているかでしょう。={0}; とだけ書いた方が楽だし,一般的に早いコードになるらしいよ。

文字列リテラルでの初期化 (1) - 危ないRiSKのブログ うつ期ver.

へぇー……知らなかった。つか,処理系依存だと思ってた。

あまり関係ないんですけど,あたしがよく目にするとこのコーディング規約では,定義文に初期化の代入文を入れちゃダメってなルールがあるもんで(「使うところで初期化しなさい」のルールから派生したものと思われる),必然的に memset(3) や ZeroMemory() を使うことになってしまいます。「独特のこだわり」に分類されるんでしょうか。また,構造体なんかの初期化では,動的に確保することも考慮して,NULL や 0 で埋める場合も,初期化用の関数を作るようにしている。

ともあれ,仕様書でそう決められていても,コンパイラが対応してなかったらどうしようもないので,ちゃんと残りの要素を0で初期化するのか,調べてみることにしました。対象は VC6 と cygwin gcc 3.4.4 です。

#include <stdio.h>

int
main(int argc, char *argv[])
{
    char a[10] = {0};
    int i;

    for (i = 0; i < 10; i++) {
        printf("%d ", a[i]);
    }
    puts("");

    return 0;
}

サンプルコードは,なんのこたない,こんなプログラムです。

ちゃんと初期化されるかは,アセンブリのリストで初期化用のコードが埋め込まれているかを見ればよさそうです。つことで,アセンブリにコンパイルしてみる。gcc では -S オプション,VC6 では,cl のオプションで /Fa[filename] を指定すれば,アセンブリリストが出力されます。

まずは,VC6。これだけのコードでもそれなりの長さになっちゃうので,初期化しているところだけ抜粋します。

; Line 6
    mov BYTE PTR _a$[ebp], 0
    xor eax, eax
    mov DWORD PTR _a$[ebp+1], eax
    mov DWORD PTR _a$[ebp+5], eax
    mov BYTE PTR _a$[ebp+9], al

アセンブリになじみのない方はギョッとしちゃうかもしれませんけど,そんなに難しいことにはなっていません。まず,_a$[ebp] というのは,変数 a の先頭アドレスのこと。C で言うところの (char *)a ですね。ここに 0 を入れて(mov して)います。これは先頭要素を初期化しているだけですから,ソースコードに書いてあるとおりです。

問題はここから。"xor eax, eax" というのは,EAX レジスタを 0 にしてるだけです。排他的論理和を自分自身で取ると 0 になるんです。で,この 0 をどうするのかというと,"mov DWORD PTR _a$[ebp+1], eax" してます。これは,a[1] に 0 を入れるということ。ただ,ここではメモリアドレスを DWORD PTR(double word のポインタ)で指定しているので,4 bytes 分のアドレスを指しています。無理矢理 C で書くと (long *)(&a[1]) といったとこでしょうか。こいつに,0 を入れているので,いっぺんに 4 bytes 分 0 で初期化されることになります。同様にして a[5] から a[8] も 0 で初期化されます。

最後の a[9] があまっちゃうんですけれど,これも最後の行で 1 byte 分 0 で埋めています(AL レジスタは EAX レジスタの下位 8 bit を指す)。要するに,VC6 では,ちゃんと 0 で初期化するコードが埋め込まれるみたい。

ちなみに,a[10000] とかいった大きな配列も,こんな感じで 4 bytes ずつ 0 で埋めるのかというと,そういうわけではありません。試してみたところ,"rep stosd" という文字列操作専用の命令に置き換えられるようです。

つづきまして,gcc を見てみます。

    movl    $10, %ecx
    movb    $0, %al
    rep
    stosb

gcc では,大きさ 10 の配列でも stosb を使って初期化しているようです(オペランドの順番が違うので注意)。いずれにしても,ちゃんと初期化されている。

memset(3) なんかを呼び出して初期化するのと比べると,関数呼び出しの場合,オーバーヘッドを無視することができません。ここら辺は最適化との絡みもあるので,あたしの手には負えなくなってくるんですけれど(最適化オプションを高めに設定するとインライン展開されそうな気がする),一般に memset(3) で初期化する方が遅くなるとは言えそうです。

……ということで締めようと思ったら微妙な話も。

a[100000] の場合を,gcc でアセンブリにコンパイルしてみました(最適化無し)。

    movl    $100000, %eax
    movl    %eax, 8(%esp)
    movl    $0, 4(%esp)
    movl    %edx, (%esp)
    call    _memset

えー……。超 memset(3) 呼んでるし……。つことで,改めて結論。

「配列の静的初期化は,初期化する配列の大きさとか,最適化の程度とか,処理系とかによってどうにでも置き換えられるみたいなので,速くなるかはよく分からん。」

よく分からないなら,「先頭だけ初期化」みたいな微妙な書き方はしないで,memset(3) で明示的に初期化する方がコードとして分かりやすい,という考え方もできそうです。まぁ,好みだと思います。こゆのは。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN