2009-08-27 17 views
7

Visual C++ 2008でこのコードを試しましたが、AとBには同じアドレスがないことがわかりました。C++のスタックとスコープ

コンパイラがない理由を私は理解していない
int main() 
{ 
    { 
     int A; 
     printf("%p\n", &A); 
    } 

    int B; 
    printf("%p\n", &B); 
} 

しかし、Bが定義されますとき、Aはもう存在しないため、同じスタックの場所が再利用できるように私には思える...

非常に単純な最適化(より大きな変数や再帰関数などの文脈では問題になる可能性がある)のように見えます。そして、それはCPUやメモリ上で重くなる再利用のようには思われません。誰にもこれについての説明がありますか?

私は答えが "見た目よりもはるかに複雑だから"という行に沿っていると思いますが、正直言って私は分かりません。

edit:以下の回答とコメントに関するいくつかの精度。

このコードの問題は、この関数が呼び出されるたびにスタックが「1つの整数が大きすぎます」ということです。もちろん、これは例では問題ありませんが、大きな変数と再帰呼び出しを考慮すると、簡単に回避できるスタックオーバーフローがあります。

私が示唆しているのはメモリの最適化ですが、パフォーマンスがどのように損なわれるかわかりません。

そして、これはリリースで行われ、ビルドでは、すべての最適化が行われます。

+0

リリースビルドまたはデバッグビルドをコンパイルしていますか? – Michael

+1

あなたが示唆しているものは、**宇宙**の最適化ですが、必ずしも速度の最適化ではありません。 –

+1

すべての地方自治体が大きすぎてキャッシュラインに収まらない場合は、キャッシュミスがないので速度最適化になります。 – Michael

答えて

8

このような地方のためにスタックスペースを再利用することは非常に一般的な最適化です。実際、最適化されたビルドでは、ローカルのアドレスを使用しなかった場合、コンパイラはスタック領域を割り当てなくても変数はレジスタ内にしか存在しません。

この最適化が表示されない場合があります。理由はいくつかあります。

最初に、デバッグビルドのような最適化がオフの場合、コンパイラはデバッグを容易にするためにこれらのいずれも実行しません。関数で使用されなくなってもAの値を表示できます。

最適化を使用してコンパイルしている場合は、ローカルのアドレスを取得して別の関数に渡すので、コンパイラはその関数が何をしているのか不明なのでストアを再利用したくない住所と

関数によって使用されるスタックスペースがあるしきい値を超えない限り、この最適化を使用しないコンパイラも想像することができます。これを行うコンパイラについてはわかりません。使用されなくなったローカル変数の領域を再利用することはコストがゼロであり、ボード全体に適用できるためです。

スタックの増加がアプリケーションにとって重大な懸案事項である場合、つまりスタックオーバーフローが発生するシナリオによっては、コンパイラのスタックスペースの最適化に頼るべきではありません。スタック上の大きなバッファをヒープに移動して、非常に深い再帰を排除することを検討する必要があります。たとえば、Windowsスレッドの場合、デフォルトで1 MBのスタックがあります。各スタックフレームに1kのメモリを割り当て、1000回の再帰呼び出しを深くするので、スタックフレームのスペースを節約するために、コンパイラを同軸化しようとするのではなく、オーバーフローが懸念されます。

+0

コンパイラが上記の例でレジスタにAとBを入れることは許されません。 &Bが必要です。 – AraK

+0

@Arak - そうですね、私は "あなたが住所を取っていないなら"と言ったのです。 – Michael

+0

@Michael、申し訳ありませんが、私はそれを見ませんでした:) – AraK

1

の両方が同じスタックフレームにを入れている可能性があります。だから、たとえAがスコープの外にアクセスできないとしても、コンパイラはコードのセマンティクスを損なわない限り、メモリ内の場所に自由にバインドすることができます。要するに、彼らはあなたがメインを実行すると同時にスタックに置かれます。

+2

また、知っているとおりに:あなたのコンパイラよりもスマートにしようとしないでください。 – ebo

+0

私はコンパイラがそれを許可されていることを知っていますが、なぜ彼はそれをやっているのだろうと思っています:) – Drealmer

1

Aはの後にスタックに割り当てられます。Bはコード内でAの後に宣言されますが(これはC90では許可されません)、まだmain関数のトップスコープにあります。メインティルの終わりまで。メイン開始時にはBが押され、インナースコープに入るとAが押され、残っているとポップされ、メイン機能が残っているとBがポップされます。

+1

コンストラクタ/デストラクタが正しい順序と場所で実行されている限り、コンパイラは任意の順序でスタックにAとBを自由に割り当てることができます。 –

+0

スタック上にある必要はありません。上記の例でアドレスが取られなかった場合、コンパイラはそれらをレジスタに残すだけで、関数はスタック・スペースをゼロにすることができます。 – Michael

3

アセンブリをチェックアウトしないのはなぜですか?

コードを少し変更してint A = 1にしました。 int B = 2;少し解読しやすくするためです。デフォルト設定でグラムから

++コンパイラは、ちょうど同じアドレスでそれらを置くことを気にしませんでしたよう

.globl main 
    .type main, @function 
main: 
.LFB2: 
    leal 4(%esp), %ecx 
.LCFI0: 
    andl $-16, %esp 
    pushl -4(%ecx) 
.LCFI1: 
    pushl %ebp 
.LCFI2: 
    movl %esp, %ebp 
.LCFI3: 
    pushl %ecx 
.LCFI4: 
    subl $36, %esp 
.LCFI5: 
    movl $1, -8(%ebp) 
    leal -8(%ebp), %eax 
    movl %eax, 4(%esp) 
    movl $.LC0, (%esp) 
    call printf 
    movl $2, -12(%ebp) 
    leal -12(%ebp), %eax 
    movl %eax, 4(%esp) 
    movl $.LC0, (%esp) 
    call printf 
    movl $0, %eax 
    addl $36, %esp 
    popl %ecx 
    popl %ebp 
    leal -4(%ecx), %esp 
    ret 
.LFE2: 

は、最終的にはそれが見えます。ファンシーな先読みの最適化は必要ありませんでした。それが最適化しようとしていなかったか、それとも利益がないと決めましたか。

注意Aが割り当てられ、印刷されます。その後、元のソースと同様に、Bが割り当てられて印刷されます。もちろん、別のコンパイラ設定を使用すると、これはまったく違って見える可能性があります。私の知識Bのためのスペースで

2

は、あなたがその行の前にデバッガに壊れた場合、あなたはそれにもかかわらずのアドレスを取得することができますライン

int B; 

でメインに入るときに確保されず、 B.スタックポインタもこの行の後に変更されません。この行で唯一起こることは、Bのコンストラクタが呼び出されることです。

+0

これを禁止または許可するものはC++標準には何もありません。したがって、コンパイラが実際にBのメモリをすぐに割り当てることは可能です。他のコンパイラではそうではありません。 – MSalters

-1

この場合、コンパイラは実際には選択肢がありません。 printf()という特定の動作を想定することはできません。その結果、A自体が存在する限り、printf()&Aにハングアップする可能性があると想定する必要があります。したがって、A自体は定義されているスコープ全体に存在します。

+2

-1:* 'A'自体は定義されているスコープ全体に存在します*。それはまさに '{...} '構造体にある理由なので、コンパイラが' B'に遭遇した時点では外部では定義されていません。したがって、コンパイラ**には**選択肢があります。 –

1

私の仕事の大部分はコンパイラと戦っているところで、私たち人間がいつもやっていることを期待するわけではありません。コンパイラをプログラムしたとしても、結果に驚くことがあります。入力行列は100%予測できません。

コンパイラの最適化部分は非常に複雑で、他の回答に記載されているように、あなたが観察したことは設定に対する任意の応答によるものかもしれませんが、周囲のコードの影響を受けている可能性があります。この最適化がロジックにない場合でも

いずれにしても、マイケルさんはスタックオーバーフローを防ぐためにコンパイラに頼ってはいけません。通常のコードメンテナンスや別の入力セットを使用した場合に問題を後で押し込むかもしれないからです。それはパイプラインの中で、ユーザーの手の中で、さらにずっとクラッシュします。