ループ前またはループ内の変数の宣言の違いは?
-
03-07-2019 - |
質問
一般的に、ループ内で繰り返し変数を宣言するのではなく、ループの前にスローアウェイ変数を宣言すると、(パフォーマンス)の違いが生じるのではないかといつも疑問に思っています。 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 に従ってコーディングする傾向があります。
編集: 特に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);
}
これは、 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;
}
vs
for (int i = 0; i < 10; i++) {
int b = i;
}
これらのコードをgcc-5.2.0でコンパイルしました。そして、私はメイン()を分解しました これら2つのコードのうち、結果は次のとおりです。
1&#186;:
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
vs
2&#186;
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(たとえば)は毎回メモリ割り当て全体を行います。
私の意見では、bはより良い構造です。 aでは、ループが終了した後、intermediateResultの最後の値が保持されます。
編集: これは値の型と大きな違いはありませんが、参照型はやや重くなります。個人的には、クリーンアップのためにできるだけ早く変数を逆参照するのが好きです。bはそれをあなたのために行います
いくつかのコンパイラが両方を同じコードになるように最適化できると思いますが、すべてではありません。したがって、前者の方が良いと思います。後者の唯一の理由は、宣言された変数がループ内でのみ使用されるようにする場合です。
一般的なルールとして、可能な限り最も内側のスコープで変数を宣言します。したがって、ループの外でIntermediateResultを使用していない場合は、Bを使用します。
同僚は最初の形式を好み、それが最適化であることを伝え、宣言を再利用することを好みます。
私は2番目のものを好む(そして同僚を説得しようとする!-))、それを読んで:
- 変数のスコープを必要な場所に限定します。これは良いことです。
- Javaは、パフォーマンスに大きな違いをもたらさないように十分に最適化します。 IIRC、おそらく2番目の形式はさらに高速です。
とにかく、コンパイラーやJVMの品質に依存する時期尚早な最適化のカテゴリーに分類されます。
ラムダなどで変数を使用している場合、C#には違いがあります。しかし、一般的にコンパイラは変数がループ内でのみ使用されると仮定して、基本的に同じことを行います。
これらは基本的に同じであることに注意してください。バージョンbでは、ループ後に変数が使用されないこと、使用できないことが読者に明らかになることに注意してください。さらに、バージョンbははるかに簡単にリファクタリングされます。バージョンaでループ本体を独自のメソッドに抽出することはより困難です。さらに、バージョンbでは、このようなリファクタリングに副作用がないことを保証します。
したがって、バージョンaは私に終わりを告げません。何の利点もありませんし、コードについて推論するのがずっと難しくなるからです...
まあ、いつでもスコープを作成できます:
{ //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);
}
}
この方法で変数を宣言するのは一度だけで、ループを抜けると死にます。
私は、ループ内で変数を宣言すると、メモリを浪費していると常に考えてきました。このようなものがある場合:
for(;;) {
Object o = new Object();
}
次に、オブジェクトを反復ごとに作成する必要があるだけでなく、オブジェクトごとに新しい参照を割り当てる必要があります。ガベージコレクターが遅い場合は、クリーンアップする必要があるダングリングリファレンスがたくさんあるようです。
ただし、これがある場合:
Object o;
for(;;) {
o = new Object();
}
その後、単一の参照を作成し、そのたびに新しいオブジェクトを割り当てます。確かに、スコープから外れるまで少し時間がかかるかもしれませんが、処理するぶら下がり参照は1つだけです。
コンパイラに依存するため、一般的な答えを出すのは難しいと思います。
私の練習は次のとおりです:
-
変数のタイプが単純な場合(int、double、...)バリアント b (内側)が好ましい。
理由:変数のスコープを縮小しています。 -
変数のタイプが単純でない場合(
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秒かかりました。
A)はB)よりも安全な賭けです.........「int」または「float」ではなく、ループで構造を初期化する場合を想像してください。
like
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」はメモリの蓄積に対して脆弱であり、特にソースライブラリを使用すると脆弱であると考えられます。
これは興味深い質問です。私の経験から、コードについてこの問題を議論する際に考慮すべき究極の質問があります:
変数をグローバルにする必要がある理由はありますか?
ローカルで何度も変数を宣言するのではなく、グローバルに1回だけ変数を宣言するのが理にかなっています。コードを整理するのに適していて、必要なコード行が少ないからです。ただし、1つのメソッド内でローカルに宣言するだけでよい場合は、そのメソッドで初期化して、変数がそのメソッドにのみ関連していることを明確にします。後者のオプションを選択した場合、初期化されたメソッドの外部でこの変数を呼び出さないように注意してください。コードは何を話しているのかわからず、エラーを報告します。
また、補足として、目的がほぼ同じであっても、異なるメソッド間でローカル変数名を複製しないでください。混乱するだけです。
もし興味があるなら、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)
これはより良い形式です
double intermediateResult;
int i = byte.MinValue;
for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}
1)このようにして、サイクルごとにではなく、両方の変数を一度宣言しました。 2)他のすべてのオプションの割り当て。 3)したがって、ベストプラクティスルールは、反復の外の宣言です。
Goで同じことを試し、 go tool compile -S
とgo 1.9.4
アセンブラー出力によるゼロ差。
これと同じ質問が長い間ありました。そこで、さらに単純なコードをテストしました。
結論: そのような場合の場合、 NO のパフォーマンスの違いはありません。
ループ外のケース
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のデコンパイラでコンパイルされたファイルをチェックしましたが、どちらの場合も同じ Test.class
for(int i = 0; i < 1000; ++i) {
int intermediateResult = i + 2;
System.out.println(intermediateResult);
}
この回答に記載されている方法を使用して、両方のケースのコードを逆アセンブルしました。回答に関連する部分のみを表示します
ループ外のケース
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;
細心の注意を払うと、 LocalVariableTable
の i
および intermediateResult
に割り当てられた Slot
のみが出現順序の製品。スロットの同じ違いは、他のコード行にも反映されます。
- 余分な操作は実行されていません
-
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);
}
コンパイラーが十分にスマートであることを知っていても、それに依存するのは嫌で、a)バリアントを使用します。
b)バリアントは、ループ本体の後で intermediateResult を使用不可にする必要がある場合にのみ意味があります。しかし、とにかくこのような絶望的な状況を想像することはできません。...
編集: Jon Skeet は非常に良い点を指摘し、ループ内の変数宣言が実際の意味の違いを生むことができることを示しました。