なぜGCCは配列の初期化を集約して、最初にゼロ以外の要素を含めて全体をゼロで埋めるのですか?


21

なぜgccは残りの96個の整数だけではなく、配列全体をゼロで埋めるのですか?ゼロ以外の初期化子はすべて配列の先頭にあります。

void *sink;
void bar() {
    int a[100]{1,2,3,4};
    sink = a;             // a escapes the function
    asm("":::"memory");   // and compiler memory barrier
    // forces the compiler to materialize a[] in memory instead of optimizing away
}

MinGW8.1とgcc9.2はどちらもこのようにasmを作成します(Godboltコンパイラエクスプローラー)。

# gcc9.2 -O3 -m32 -mno-sse
bar():
    push    edi                       # save call-preserved EDI which rep stos uses
    xor     eax, eax                  # eax=0
    mov     ecx, 100                  # repeat-count = 100
    sub     esp, 400                  # reserve 400 bytes on the stack
    mov     edi, esp                  # dst for rep stos
        mov     DWORD PTR sink, esp       # sink = a
    rep stosd                         # memset(a, 0, 400) 

    mov     DWORD PTR [esp], 1        # then store the non-zero initializers
    mov     DWORD PTR [esp+4], 2      # over the zeroed part of the array
    mov     DWORD PTR [esp+8], 3
    mov     DWORD PTR [esp+12], 4
 # memory barrier empty asm statement is here.

    add     esp, 400                  # cleanup the stack
    pop     edi                       # and restore caller's EDI
    ret

(SSEを有効にすると、movdqa load / storeで4つすべてのイニシャライザがコピーされます)

Clangのように、GCCが最後の96要素のみをlea edi, [esp+16](および)memsetを実行せず、なぜしないのrep stosdですか? これは逃した最適化ですか、それともこの方法で行う方が効率的ですか?(Clang memsetはインライン化する代わりに実際に呼び出しますrep stos


編集者注:質問には、元々同じように機能する最適化されていないコンパイラー出力がありましたが、非効率的なコードで-O0は何も証明されません。しかし、この最適化はGCCでも見落とされてい-O3ます。

a非インライン関数へのポインターを渡すことは、コンパイラーを強制的に実体化させるもう1つの方法ですa[]が、32ビットコードではasmが大幅に乱雑になります。(スタックの引数は、配列への初期化のためにスタックへのストアと混合されるプッシュをもたらします。)

を使用しvolatile a[100]{1,2,3,4}てGCC を取得し、配列を作成してコピーします。通常volatile、コンパイラーがローカル変数を初期化する方法、またはそれらをスタックに配置する方法を調べるのに適しています。


1
@Damienあなたは私の質問を誤解しました。例えば、A [0]があるかのように値の2倍を割り当てられている理由私が尋ねるa[0] = 0;と、その後a[0] = 1;
ラッシー

1
アセンブリを読み取ることができませんが、配列が完全にゼロで埋められていることをどこで示していますか?
smac89

3
もう1つの興味深い事実:より多くのアイテムが初期化されると、gccとclangの両方が配列全体のコピー元に戻り.rodataます... 400バイトのコピーが8つのアイテムのゼロ化と設定よりも速いとは思えません。
Jester、

2
最適化を無効にしました。非効率的なコードは、同じことが-O3(実際に)発生することを確認するまで、驚くことではありません。 godbolt.org/z/rh_TNF
Peter Cordes

12
あなたはもっと知りたいですか?最適化されていないので、missed-optimizationキーワードを付けてGCCのバグジラに報告してください。
Peter Cordes

回答:


2

理論的には、初期化は次のようになります。

int a[100] = {
  [3] = 1,
  [5] = 42,
  [88] = 1,
};

そのため、最初にメモリブロック全体をゼロにしてから個々の値を設定する方が、キャッシュと最適化の意味でより効果的です。

以下に応じて動作が変わる可能性があります。

  • ターゲットアーキテクチャ
  • 対象OS
  • 配列の長さ
  • 初期化率(明示的に初期化された値/長さ)
  • 初期化された値の位置

もちろん、あなたの場合、初期化は配列の開始時に圧縮され、最適化は簡単です。

したがって、ここではgccが最も一般的なアプローチを行っているようです。最適化が欠けているようです。


はい、このコードの最適な戦略はおそらくすべてをゼロにすることa[6]です。あるいは、イミディエイトまたはゼロの単一のストアで埋められた初期のギャップから始めて、おそらくすべてのものをゼロにすることでしょう。特にx86-64を対象とする場合、qwordストアを使用して一度に2つの要素を実行し、下位の要素をゼロ以外にすることができます。たとえばmov QWORD PTR [rsp+3*4], 1、要素3と4を1つのミスアライメントのqwordストアで実行します。
Peter Cordes、

動作は理論的にはターゲットOSに依存する可能性がありますが、実際のGCCではそうではなく、理由はありません。ターゲットアーキテクチャのみ(およびその中で、-march=skylakevs。と-march=k8vs. などのさまざまなマイクロアーキテクチャのチューニングオプションは-march=knl、一般に非常に異なり、おそらくこれに対する適切な戦略の点で異なります。)
Peter Cordes

これはC ++でも許可されますか?Cだけだと思った
ラッシー

@Lassieあなたはc ++で正しいですこれは許可されていませんが、質問はコンパイラバックエンドに関連しているため、それほど重要ではありません。また、表示されるコードは両方とも可能です
vlad_tepesch

あなたも、簡単にいくつかを宣言することにより、C ++で同じ作業の例を構築することができstruct Bar{ int i; int a[100]; int j;} 、および初期Bar a{1,{2,3,4},4};gccが同じことを行います。ゼロすべてのアウトをして、5つの値を設定
vlad_tepeschを
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.