ループ内で変数を宣言するためのオーバーヘッドはありますか?(C ++)


158

次のようなことをすると、速度や効率が低下するのではないかと思っています。

int i = 0;
while(i < 100)
{
    int var = 4;
    i++;
}

これはint var100回宣言します。あるように思えますが、よくわかりません。代わりにこれを行う方がより実用的/高速でしょうか?

int i = 0;
int var;
while(i < 100)
{
    var = 4;
    i++;
}

それとも、速度的にも効率的にも同じですか?


7
明確にするために、上記のコードはvarを100回「宣言」していません。
ジェイソン

1
@Rabarberski:参照されている質問は、言語を指定していないため、完全に重複しているわけではありません。この質問はC ++に固有のものです。しかし、参照された質問に投稿された回答によると、回答は言語と、場合によってはコンパイラによって異なります。
DavidRR 2013

2
@jasonコードの最初のスニペットが変数 'var'を100回宣言していない場合、何が起こっているのか説明できますか?変数を1回宣言し、100回初期化するだけですか?ループ内のすべてが100回実行されるため、コードは変数を100回宣言して初期化すると思います。ありがとう。
randomUser47534 2015年

回答:


194

ローカル変数のスタックスペースは通常、関数スコープで割り当てられます。したがって、スタックポインタの調整はループ内では発生せず、4をに割り当てるだけvarです。したがって、これら2つのスニペットのオーバーヘッドは同じです。


50
大学で教えている人たちが少なくともこの基本的なことを知っていたらいいのにと思います。彼がループ内で変数を宣言することを私に笑ったとき、彼がそうしない理由としてパフォーマンスを引用するまで、私は何が悪いのか疑問に思いました、そして私は「WTF !?」のようでした。
mmx 2009年

18
スタックスペースについてすぐに話し合う必要がありますか?このような変数もレジスターにある可能性があります。
toto

3
@totoこのような変数もどこにもない可能性があります。var変数は初期化されますが使用されることはないため、妥当なオプティマイザーは変数を完全に削除できます(変数がループのどこかで使用された場合の2番目のスニペットを除く)。
CiaPan 2016

@Mehrdad Afshariループ内の変数は、反復ごとに1回呼び出されるコンストラクターを取得します。編集-あなたがこれについて以下で言及したようですが、受け入れられた回答でも言及する価値があると思います。
hoodaticus 2017

106

プリミティブ型とPOD型の場合、違いはありません。コンパイラーは、関数の先頭で変数にスタックスペースを割り当て、どちらの場合も関数が戻ったときに変数の割り当てを解除します。

自明でないコンストラクターを持つ非PODクラスタイプの場合、違いが生じます。その場合、変数をループの外側に配置すると、コンストラクターとデストラクタが1回だけ呼び出され、代入演算子が反復ごとに呼び出されますが、ループは、ループが繰り返されるたびにコンストラクタとデストラクタを呼び出します。クラスのコンストラクタ、デストラクタ、および代入演算子の動作に応じて、これが望ましい場合と望ましくない場合があります。


42
正しい考えの間違った理由。ループ外の変数。一度構築され、一度破棄されますが、割り当て演算子はすべての反復に適用されます。ループ内の変数。Constructe / Desatructorは、すべての反復を適用しましたが、代入操作はゼロでした。
マーティンヨーク

8
これが最良の答えですが、これらのコメントは紛らわしいです。コンストラクターの呼び出しと代入演算子の呼び出しには大きな違いがあります。
アンドリューグラント

1
これは、あるループ本体だけではなく、初期化のために、とにかく割り当てをしている場合はtrue。また、本体に依存しない/一定の初期化がある場合、オプティマイザーはそれを引き上げることができます。
peterchen 2009年

7
@アンドリュー・グラント:なぜ。代入演算子は通常、tmpへのコピーコンストラクト、それに続くスワップ(例外安全のため)、そしてtmpの破棄として定義されます。したがって、代入演算子は、上記の構築/破棄サイクルとそれほど違いはありません。典型的な代入演算子の例については、stackoverflow.com / questions / 255612 /…を参照してください。
マーティンヨーク

1
構築/破棄が高価な場合、それらの総コストは、operator =のコストの妥当な上限です。しかし、割り当ては確かに安くなる可能性があります。また、この説明をint型からC ++型に拡張すると、「同じ型の値から変数を代入する」以外の操作として「var = 4」を一般化できます。
greggo 2014年

69

これらは両方とも同じであり、コンパイラーの機能を確認することで、次のように確認できます(最適化を高く設定しなくても)。

コンパイラ(gcc 4.0)が単純な例に対して何をするかを見てください。

1.c:

main(){ int var; while(int i < 100) { var = 4; } }

gcc -S 1.c

1.s:

_main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $24, %esp
    movl    $0, -16(%ebp)
    jmp L2
L3:
    movl    $4, -12(%ebp)
L2:
    cmpl    $99, -16(%ebp)
    jle L3
    leave
    ret

2.c

main() { while(int i < 100) { int var = 4; } }

gcc -S 2.c

2.s:

_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $24, %esp
        movl    $0, -16(%ebp)
        jmp     L2
L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3
        leave
        ret

これらから、2つのことがわかります。1つは、コードが両方で同じであるということです。

次に、varのストレージがループの外側に割り当てられます。

         subl    $24, %esp

そして最後に、ループ内の唯一のものは、割り当てと条件のチェックです。

L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3

これは、ループを完全に削除せずにできる限り効率的です。


2
「これは、ループを完全に削除せずにできる限り効率的です」完全ではありません。ループを部分的に展開すると(パスごとに4回と言う)、劇的に高速化されます。おそらく、最適化する方法は他にもたくさんあります...最近のコンパイラのほとんどは、ループしてもまったく意味がないことに気付くでしょう。iは、後に使用された場合、それは単純に設定したい「I」= 100
ダロン

これは、コードがインクリメントされた「i」に変更されたことを前提としています...それは永久ループです。
darron 2009

元の投稿と同じように!
アレックスブラウン

2
私は証明で理論を裏付ける答えが好きです!ASMダンプが等しいコードであるという理論を裏付けているのを見るのはうれしいです。+1
シャビモンテロ2014

1
実際に、バージョンごとにマシンコードを生成して結果を出しました。実行する必要はありません。
アレックスブラウン

14

最近では、コンパイラーがコードをより適切に最適化(変数スコープを縮小)できるため、定数でない限り、ループ内で宣言することをお勧めします。

編集:この答えは現在ほとんど時代遅れです。ポストクラシックコンパイラの台頭により、コンパイラがそれを理解できないケースはまれになっています。私はまだそれらを構築することができますが、ほとんどの人は構築を悪いコードとして分類します。


4
最適化に影響するかどうかは疑わしいです。コンパイラが何らかのデータフロー分析を実行すると、ループの外部で変更されていないことがわかるため、どちらの場合も同じ最適化されたコードが生成されます。
Adam Rosenfield

3
ただし、同じ一時変数名を使用する2つの異なるループがあるかどうかはわかりません。
ジョシュア

11

最新のコンパイラのほとんどは、これを最適化します。そうは言っても、私はあなたの最初の例をもっと読みやすいと思うので使用します。


3
私はそれを最適化として実際には数えません。これらはローカル変数であるため、スタックスペースは関数の先頭に割り当てられます。パフォーマンスを損なうような実際の「作成」はありません(コンストラクターが呼び出されていない限り、これはまったく別の話です)。
mmx 2009年

あなたは正しいです、「最適化」は間違った言葉ですが、私はより良いものを求めて途方に暮れています。
Andrew Hare

問題は、そのようなオプティマイザーがライブ範囲分析を使用し、両方の変数がかなり死んでいることです。
MSalters 2009年

「コンパイラは、データフロー分析を実行すると、それらの間に違いは見られません」はどうでしょうか。個人的には、変数のスコープは、効率ではなく明確にするために、使用される場所に限定することをお勧めします。
greggo 2014年

9

組み込み型の場合、2つのスタイルの間に違いはない可能性があります(おそらく、生成されたコードに至るまで)。

ただし、変数が重要なコンストラクタ/デストラクタを持つクラスである場合、実行時コストに大きな違いが生じる可能性があります。私は通常、変数をループの内側にスコープします(スコープをできるだけ小さく保つため)が、それがパフォーマンスに影響を与えることが判明した場合は、クラス変数をループのスコープの外側に移動することを検討します。ただし、これを行うには、odeパスのセマンティクスが変更される可能性があるため、追加の分析が必要です。したがって、これは、セマンティクスで許可されている場合にのみ実行できます。

RAIIクラスはこの動作を必要とする場合があります。たとえば、ファイルアクセスの有効期間を管理するクラスは、ファイルアクセスを適切に管理するために、ループの反復ごとに作成および破棄する必要がある場合があります。

LockMgr構築時にクリティカルセクションを取得し、破棄すると解放するクラスがあるとします。

while (i< 100) {
    LockMgr lock( myCriticalSection); // acquires a critical section at start of
                                      //    each loop iteration

    // do stuff...

}   // critical section is released at end of each loop iteration

とはかなり異なります:

LockMgr lock( myCriticalSection);
while (i< 100) {

    // do stuff...

}

6

両方のループの効率は同じです。どちらも無限の時間がかかります:)ループ内でiをインクリメントすることをお勧めします。


ああ、そうだ、私はスペース効率に取り組むのを忘れた-それは大丈夫だ-両方とも2int。プログラマーがツリーのフォレストを見逃しているのは奇妙に思えます。終了しないコードに関するこれらすべての提案です。
ラリー渡辺

終了しなくても大丈夫です。どちらも呼び出されません。:-)
Nosredna 2009年

2

私はかつていくつかのパフォーマンステストを実行しましたが、驚いたことに、ケース1の方が実際に高速であることがわかりました。これは、ループ内で変数を宣言するとスコープが縮小されるため、早く解放されるためだと思います。しかし、それはかなり昔のことで、非常に古いコンパイラでした。最新のコンパイラーは違いを最適化するためのより良い仕事をしていると確信していますが、それでも変数スコープをできるだけ短くしておくことは害にはなりません。


違いはおそらくスコープの違いによるものです。スコープが小さいほど、コンパイラーは変数のシリアル化を排除できる可能性が高くなります。スモールループスコープでは、変数はレジスタに配置され、スタックフレームに保存されなかった可能性があります。ループ内で関数を呼び出すか、コンパイラが実際にどこを指しているのかわからないポインタを逆参照すると、関数スコープ内にある場合はループ変数がスピルされます(ポインタに含まれている可能性があります&i)。
パトリックSchlüter

設定と結果を投稿してください。
jxramos 2016年

2
#include <stdio.h>
int main()
{
    for(int i = 0; i < 10; i++)
    {
        int test;
        if(i == 0)
            test = 100;
        printf("%d\n", test);
    }
}

上記のコードは常に100を10回出力します。つまり、ループ内のローカル変数は、関数呼び出しごとに1回だけ割り当てられます。


0

確実にする唯一の方法は、それらの時間を計ることです。ただし、違いがある場合は微視的であるため、非常に大きなタイミングループが必要になります。

さらに重要なことに、最初のものは変数varを初期化するのでより良いスタイルですが、もう1つは初期化されないままにします。これと、変数を使用場所にできるだけ近いものとして定義する必要があるというガイドラインは、通常、最初の形式が優先されることを意味します。


「確実にする唯一の方法は、それらの時間を計ることです。」-1は正しくありません。申し訳ありませんが、別の投稿では、生成された機械語を比較して本質的に同一であることがわかったため、これが間違っていることが証明されました。私はあなたの答えに一般的に問題はありませんが、-1が何のためにあるのか間違っていませんか?
ビルK

放出されたコードを調べることは確かに有用であり、このような単純なケースでは十分かもしれません。ただし、より複雑なケースでは、参照の局所性などの問題が頭の後ろにあり、これらは実行のタイミングによってのみテストできます。

-1

変数が2つしかない場合、コンパイラーは両方にレジスターを割り当てる可能性があります。これらのレジスタはとにかくそこにあるので、これは時間がかかりません。いずれの場合も、2つのレジスタ書き込み命令と1つのレジスタ読み取り命令があります。


-2

私は、ほとんどの答えが考慮すべき主要なポイントを欠いていると思います。それは「それは明確ですか」そして明らかにすべての議論によって事実です。いいえそうではありません。ほとんどのループコードでは、効率はほとんど問題にならないことをお勧めします(火星着陸船を計算しない限り)。したがって、実際に唯一の問題は、何がより賢明で読みやすく、保守しやすいように見えるかです。この場合、宣言することをお勧めします。ループの前と外側の変数-これは単にそれをより明確にします。そうすれば、あなたや私のような人々は、それが有効かどうかをオンラインでチェックする時間を無駄にすることさえしません。


-6

それは真実ではありませんが、オーバーヘッドはありますが、無視できるオーバーヘッドがあります。

おそらくそれらはスタック上の同じ場所に配置されるでしょうが、それでも割り当てられます。そのintのスタック上のメモリ位置を割り当て、}の終わりにそれを解放します。ヒープフリーの意味ではなく、sp(スタックポインタ)を1だけ移動します。ローカル変数が1つしかないことを考えると、fp(フレームポインタ)とspを単純に等しくします。

簡単な答えは次のようになります。どちらの方法でもほとんど同じように機能することを気にしないでください。

しかし、スタックがどのように編成されているかについてもっと読んでみてください。私の学部はそれについてかなり良い講義をしましたもっと読みたい場合はここをチェックして くださいhttp://www.cs.utk.edu/~plank/plank/classes/cs360/360/notes/Assembler1/lecture.html


繰り返しますが、-1は正しくありません。アセンブリを見た投稿を読んでください。
ビルK

いいえ、あなたは間違っています。そのコードで生成されたアセンブラコードを見てください
grobartn 2009年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.