一般的に、ループ内で繰り返し変数を宣言するのではなく、ループの前にスローアウェイ変数を宣言すると、(パフォーマンス)の違いが生じるのではないかといつも思っていました。 Javaの(まったく無意味)の例:
a)ループ前の宣言:
double intermediateResult;
for(int i=0; i < 1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}
b)ループ内の宣言(繰り返し):
for(int i=0; i < 1000; i++){
double intermediateResult = i;
System.out.println(intermediateResult);
}
どちらが良いですか、aまたはb?
繰り返される変数宣言(例b)は理論上より多くのオーバーヘッドを作成すると思われますが、コンパイラは十分にスマートなので、関係ありません。例bには、よりコンパクトで、変数のスコープが使用される場所に制限されるという利点があります。それでも、例aに従ってコーディングする傾向があります。
Edit:特にJavaの場合に興味があります。
どちらが良いですか、aまたはb?
パフォーマンスの観点からは、測定する必要があります。 (そして、私の意見では、差を測定できる場合、コンパイラーはあまり良くありません)。
保守の観点からは、bの方が優れています。可能な限り狭い範囲で、同じ場所で変数を宣言して初期化します。宣言と初期化の間に大きな穴を開けたり、必要のない名前空間を汚したりしないでください。
さて、AとBの例をそれぞれ20回実行し、1億回ループしました(JVM-1.5.0)
A:平均実行時間:.074秒
B:平均実行時間:.067秒
驚いたことに、Bは少し速かった。これを正確に測定できるかどうかは、コンピューターと同じくらい速いのです。私もそれをA方法でコーディングしますが、私はそれは本当に重要ではないと言うでしょう。
それは言語と正確な使用法に依存します。たとえば、C#1では違いはありません。 C#2では、ローカル変数が匿名メソッド(またはC#3のラムダ式)によってキャプチャされた場合、非常に大きな違いが生じる可能性があります。
例:
using System;
using System.Collections.Generic;
class Test
{
static void Main()
{
List<Action> actions = new List<Action>();
int outer;
for (int i=0; i < 10; i++)
{
outer = i;
int inner = i;
actions.Add(() => Console.WriteLine("Inner={0}, Outer={1}", inner, outer));
}
foreach (Action action in actions)
{
action();
}
}
}
出力:
Inner=0, Outer=9
Inner=1, Outer=9
Inner=2, Outer=9
Inner=3, Outer=9
Inner=4, Outer=9
Inner=5, Outer=9
Inner=6, Outer=9
Inner=7, Outer=9
Inner=8, Outer=9
Inner=9, Outer=9
違いは、すべてのアクションが同じouter
変数をキャプチャしますが、それぞれに独自の個別のinner
変数があることです。
以下は、私が.NETで作成およびコンパイルしたものです。
double r0;
for (int i = 0; i < 1000; i++) {
r0 = i*i;
Console.WriteLine(r0);
}
for (int j = 0; j < 1000; j++) {
double r1 = j*j;
Console.WriteLine(r1);
}
これは、 。NETリフレクター から得られるものです- CIL がコードに戻されます。
for (int i = 0; i < 0x3e8; i++)
{
double r0 = i * i;
Console.WriteLine(r0);
}
for (int j = 0; j < 0x3e8; j++)
{
double r1 = j * j;
Console.WriteLine(r1);
}
したがって、コンパイル後は両方ともまったく同じに見えます。管理言語では、コードはCL /バイトコードに変換され、実行時に機械語に変換されます。そのため、機械語では、スタック上にdoubleが作成されない場合があります。 WriteLine
関数の一時変数であることをコードが反映しているため、単なるレジスタである場合があります。ループ専用の最適化ルールが設定されています。そのため、特にマネージド言語では、平均的な人は心配するべきではありません。たとえば、string a; a+=anotherstring[i]
とStringBuilder
を使用して多数の文字列を連結する必要がある場合など、管理コードを最適化できる場合があります。両方のパフォーマンスには非常に大きな違いがあります。コンパイラーがコードを最適化できないようなケースが多くあります。これは、より大きなスコープで何が意図されているかを把握できないためです。ただし、基本的なことはほとんど最適化できます。
これはVB.NETの落とし穴です。この例では、Visual Basicの結果は変数を再初期化しません。
For i as Integer = 1 to 100
Dim j as Integer
Console.WriteLine(j)
j = i
Next
' Output: 0 1 2 3 4...
これにより、最初に0が出力されます(Visual Basic変数は宣言時にデフォルト値になります!)が、その後は毎回i
が出力されます。
ただし、= 0
を追加すると、期待どおりの結果が得られます。
For i as Integer = 1 to 100
Dim j as Integer = 0
Console.WriteLine(j)
j = i
Next
'Output: 0 0 0 0 0...
私は簡単なテストを行いました:
int b;
for (int i = 0; i < 10; i++) {
b = i;
}
対
for (int i = 0; i < 10; i++) {
int b = i;
}
これらのコードをgcc-5.2.0でコンパイルしました。そして、私はこれらの2つのコードのメイン()を分解しました、そしてそれが結果です:
1º:
0x00000000004004b6 <+0>: Push rbp
0x00000000004004b7 <+1>: mov rbp,rsp
0x00000000004004ba <+4>: mov DWORD PTR [rbp-0x4],0x0
0x00000000004004c1 <+11>: jmp 0x4004cd <main+23>
0x00000000004004c3 <+13>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004004c6 <+16>: mov DWORD PTR [rbp-0x8],eax
0x00000000004004c9 <+19>: add DWORD PTR [rbp-0x4],0x1
0x00000000004004cd <+23>: cmp DWORD PTR [rbp-0x4],0x9
0x00000000004004d1 <+27>: jle 0x4004c3 <main+13>
0x00000000004004d3 <+29>: mov eax,0x0
0x00000000004004d8 <+34>: pop rbp
0x00000000004004d9 <+35>: ret
対
2º
0x00000000004004b6 <+0>: Push rbp
0x00000000004004b7 <+1>: mov rbp,rsp
0x00000000004004ba <+4>: mov DWORD PTR [rbp-0x4],0x0
0x00000000004004c1 <+11>: jmp 0x4004cd <main+23>
0x00000000004004c3 <+13>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004004c6 <+16>: mov DWORD PTR [rbp-0x8],eax
0x00000000004004c9 <+19>: add DWORD PTR [rbp-0x4],0x1
0x00000000004004cd <+23>: cmp DWORD PTR [rbp-0x4],0x9
0x00000000004004d1 <+27>: jle 0x4004c3 <main+13>
0x00000000004004d3 <+29>: mov eax,0x0
0x00000000004004d8 <+34>: pop rbp
0x00000000004004d9 <+35>: ret
これはまったく同じ結果です。 2つのコードが同じものを生成するという証拠ではありませんか?
私は常に(コンパイラに依存するのではなく)Aを使用し、次のように書き換えることもあります。
for(int i=0, double intermediateResult=0; i<1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}
これでもintermediateResult
がループのスコープに制限されますが、各反復中に再宣言されません。
言語に依存します-IIRC C#はこれを最適化するため、違いはありませんが、JavaScript(たとえば)は毎回Shebang全体を割り当てます。
私の意見では、bはより良い構造です。 aでは、ループが終了した後、intermediateResultの最後の値が保持されます。
編集:これは値の型とそれほど大きな違いはありませんが、参照型はやや重要です。個人的には、クリーンアップのためにできるだけ早く変数を逆参照するのが好きです。bはそれをあなたのために行います。
まあ、あなたは常にそのためのスコープを作ることができます:
{ //Or if(true) if the language doesn't support making scopes like this
double intermediateResult;
for (int i=0; i<1000; i++) {
intermediateResult = i;
System.out.println(intermediateResult);
}
}
この方法では、変数を1回宣言するだけで、ループを抜けると消滅します。
ラムダなどで変数を使用している場合、C#には違いがあります。しかし、一般に、コンパイラーは、変数がループ内でのみ使用されると仮定して、基本的に同じことを行います。
基本的に同じであることに注意してください。バージョンbを使用すると、ループの後で変数が使用されないこと、および使用できないことが読者に明らかになります。さらに、バージョンbははるかに簡単にリファクタリングされます。バージョンaでループ本体を独自のメソッドに抽出することはより困難です。さらに、バージョンbは、そのようなリファクタリングに副作用がないことを保証します。
それゆえ、バージョンaは私に終わりを告げません。何の利点もありませんし、コードについて推論するのがずっと難しくなるからです...
同僚は最初のフォームを好み、それが最適化であることを伝え、宣言を再利用することを好みます。
私は2番目のものを好む(そして私の同僚を説得しようとする!-))、それを読んで:
とにかく、コンパイラやJVMの品質に依存する時期尚早な最適化のカテゴリに分類されます。
いくつかのコンパイラが両方を同じコードになるように最適化できると思いますが、すべてではありません。したがって、前者の方が良いと思います。後者の唯一の理由は、宣言された変数がループ内でonlyを使用することを保証したい場合です。
原則として、可能な限り最も内側のスコープで変数を宣言します。したがって、ループの外でIntermediateResultを使用していない場合は、Bを使用します。
ループ内で変数を宣言すると、メモリが無駄になるといつも思っていました。このようなものがある場合:
for(;;) {
Object o = new Object();
}
その場合、各反復でオブジェクトを作成する必要があるだけでなく、各オブジェクトに新しい参照を割り当てる必要があります。ガベージコレクターが遅い場合、クリーンアップする必要のあるぶら下がり参照がたくさんあるようです。
ただし、これがある場合:
Object o;
for(;;) {
o = new Object();
}
次に、単一の参照を作成し、そのたびに新しいオブジェクトを割り当てます。確かに、スコープから外れるまで少し時間がかかるかもしれませんが、処理するぶら下がり参照は1つしかありません。
私の練習は次のとおりです。
変数のタイプが単純な場合(int、double、...)私はバリアントb(inside)を好みます。
理由:変数のスコープを縮小します。
変数の型が単純でない場合(何らかのclass
またはstruct
)私はバリアントa(外部)を好みます。
理由:ctor-dtor呼び出しの回数を減らす。
それはコンパイラに依存しており、一般的な答えを出すのは難しいと思います。
パフォーマンスの観点からは、外部の方が(はるかに)優れています。
public static void outside() {
double intermediateResult;
for(int i=0; i < Integer.MAX_VALUE; i++){
intermediateResult = i;
}
}
public static void inside() {
for(int i=0; i < Integer.MAX_VALUE; i++){
double intermediateResult = i;
}
}
両方の機能をそれぞれ10億回実行しました。 outside()は65ミリ秒かかりました。 inside()には1.5秒かかりました。
興味深い質問です。私の経験から、この問題をコードについて議論する際に考慮すべき究極の質問があります:
変数がグローバルである必要がある理由はありますか?
ローカルで何度も変数を宣言するのではなく、グローバルに1回だけ変数を宣言するのは理にかなっています。コードを整理するのに適していて、必要なコード行が少ないためです。ただし、1つのメソッド内でローカルに宣言するだけでよい場合は、そのメソッドで初期化して、変数がそのメソッドにのみ関連していることを明確にします。後者のオプションを選択した場合、初期化されたメソッドの外部でこの変数を呼び出さないように注意してください。コードは何を話しているかわからず、エラーを報告します。
また、補足として、目的がほぼ同じであっても、異なるメソッド間でローカル変数名を重複させないでください。混乱するだけです。
Goで同じことを試し、go 1.9.4でgo tool compile -S
を使用してコンパイラ出力を比較しました
アセンブラー出力によるゼロ差。
私はこれと同じ質問を長い間持っていました。そこで、さらに単純なコードをテストしました。
結論:そのような場合にはいいえパフォーマンスの違い。
ループ外の場合
int intermediateResult;
for(int i=0; i < 1000; i++){
intermediateResult = i+2;
System.out.println(intermediateResult);
}
ループ内の場合
for(int i=0; i < 1000; i++){
int intermediateResult = i+2;
System.out.println(intermediateResult);
}
IntelliJのデコンパイラでコンパイルされたファイルを確認しましたが、どちらの場合もsameTest.class
for(int i = 0; i < 1000; ++i) {
int intermediateResult = i + 2;
System.out.println(intermediateResult);
}
また、この answer で指定された方法を使用して、両方のケースのコードを逆アセンブルしました。答えに関連する部分のみを表示します
ループ外の場合
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_2
2: iload_2
3: sipush 1000
6: if_icmpge 26
9: iload_2
10: iconst_2
11: iadd
12: istore_1
13: getstatic #2 // Field Java/lang/System.out:Ljava/io/PrintStream;
16: iload_1
17: invokevirtual #3 // Method Java/io/PrintStream.println:(I)V
20: iinc 2, 1
23: goto 2
26: return
LocalVariableTable:
Start Length Slot Name Signature
13 13 1 intermediateResult I
2 24 2 i I
0 27 0 args [Ljava/lang/String;
ループ内の場合
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: sipush 1000
6: if_icmpge 26
9: iload_1
10: iconst_2
11: iadd
12: istore_2
13: getstatic #2 // Field Java/lang/System.out:Ljava/io/PrintStream;
16: iload_2
17: invokevirtual #3 // Method Java/io/PrintStream.println:(I)V
20: iinc 1, 1
23: goto 2
26: return
LocalVariableTable:
Start Length Slot Name Signature
13 7 2 intermediateResult I
2 24 1 i I
0 27 0 args [Ljava/lang/String;
細心の注意を払うと、Slot
内のi
およびintermediateResult
に割り当てられたLocalVariableTable
のみが、出現順序の積としてスワップされます。スロットの同じ違いは、他のコード行にも反映されます。
intermediateResult
はどちらの場合もローカル変数なので、アクセス時間に違いはありません。ボーナス
コンパイラは大量の最適化を行い、この場合に何が起こるかを見てみましょう。
ゼロワークケース
for(int i=0; i < 1000; i++){
int intermediateResult = i;
System.out.println(intermediateResult);
}
ゼロワークの逆コンパイル
for(int i = 0; i < 1000; ++i) {
System.out.println(i);
}
これは良い形です
double intermediateResult;
int i = byte.MinValue;
for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}
1)このようにして、サイクルごとにではなく、両方の変数を一度宣言します。 2)他のすべてのオプションの割り当て。 3)したがって、ベストプラクティスのルールは、反復の外の宣言です。
A)B)よりも安全な賭けです.........「int」または「float」ではなく、ループで構造を初期化する場合を想像してください。
のような
typedef struct loop_example{
JXTZ hi; // where JXTZ could be another type...say closed source lib
// you include in Makefile
}loop_example_struct;
//then....
int j = 0; // declare here or face c99 error if in loop - depends on compiler setting
for ( ;j++; )
{
loop_example loop_object; // guess the result in memory heap?
}
メモリリークの問題に直面することは確かです!したがって、「A」の方がより安全な賭けであり、「B」は、特にソースライブラリを操作するメモリ蓄積に対して脆弱であると考えています。
誰かが興味があるなら、Node 4.0.0でJSをテストしました。ループ外で宣言すると、1回の試行あたり1億回のループ反復を行う1000回の試行で、平均で〜.5ミリ秒のパフォーマンスの改善がもたらされました。だから私は先に行くと言って、最も読みやすい/保守可能な方法でそれを書くつもりです、それはB、imoです。私は自分のコードをいじくりますが、今はパフォーマンスのNodeモジュールを使用しました。コードは次のとおりです。
var now = require("../node_modules/performance-now")
// declare vars inside loop
function varInside(){
for(var i = 0; i < 100000000; i++){
var temp = i;
var temp2 = i + 1;
var temp3 = i + 2;
}
}
// declare vars outside loop
function varOutside(){
var temp;
var temp2;
var temp3;
for(var i = 0; i < 100000000; i++){
temp = i
temp2 = i + 1
temp3 = i + 2
}
}
// for computing average execution times
var insideAvg = 0;
var outsideAvg = 0;
// run varInside a million times and average execution times
for(var i = 0; i < 1000; i++){
var start = now()
varInside()
var end = now()
insideAvg = (insideAvg + (end-start)) / 2
}
// run varOutside a million times and average execution times
for(var i = 0; i < 1000; i++){
var start = now()
varOutside()
var end = now()
outsideAvg = (outsideAvg + (end-start)) / 2
}
console.log('declared inside loop', insideAvg)
console.log('declared outside loop', outsideAvg)