自明ではない条件文をループの初期化セクションに移動する必要がありますか?


21

stackoverflow.comのこの質問からこのアイデアを得ました

一般的なパターンは次のとおりです。

final x = 10;//whatever constant value
for(int i = 0; i < Math.floor(Math.sqrt(x)) + 1; i++) {
  //...do something
}

私がやろうとしているのは、条件付きステートメントが複雑なものであり、変わらないということです。

ループの初期化セクションで宣言する方がいいですか?

final x = 10;//whatever constant value
for(int i = 0, j = Math.floor(Math.sqrt(x)) + 1; i < j; i++) {
  //...do something
}

これはもっと明確ですか?

条件式が次のように単純な場合

final x = 10;//whatever constant value
for(int i = 0, j = n*n; i > j; j++) {
  //...do something
}

47
ループのの行移動するだけで、理にかなった名前を付けることができます。
jonrsharpe 16

2
@Mehrdad:それらは同等ではありません。x大きさが大きい場合、Math.floor(Math.sqrt(x))+1に等しいMath.floor(Math.sqrt(x))。:-)
R ..

5
@jonrsharpeそれは変数の範囲を広げるからです。私は必ずしもそれがだとは言わないよ良いそれをしない理由、それは一部の人がいない理由です。
ケビンクルムウィーデ16

3
@KevinKrumwiedeスコープが懸念事項である場合、コードを独自のブロックに配置することで制限します。たとえば{ x=whatever; for (...) {...} }、さらに良いことに、別の関数である必要があるかどうかを検討します。
Blrfl 16

2
@jonrsharpe initセクションでも宣言するときに、わかりやすい名前を付けることができます。そこに置くとは言わない。分離していれば、読みやすくなります。
JollyJoker 16

回答:


62

私がしたいことは次のようなものです:

void doSomeThings() {
    final x = 10;//whatever constant value
    final limit = Math.floor(Math.sqrt(x)) + 1;
    for(int i = 0; i < limit; i++) {
         //...do something
    }
}

正直に言って、ループヘッダーに初期化j(現在limit)を詰め込む唯一の正当な理由は、ループヘッダーのスコープを正しく保つことです。問題にならないようにするために必要なのは、すてきな囲みスコープです。

速くなりたいという欲求は高く評価できますが、本当の正当な理由がなければ読みやすさを犠牲にしないでください。

確かに、コンパイラは最適化でき、複数の変数を初期化することは合法かもしれませんが、ループはそのままではデバッグするのに十分困難です。人間に親切にしてください。これが本当に私たちを遅くすることが判明した場合、それを修正するのに十分それを理解することは素晴らしいことです。


いい視点ね。式が単純なものである場合、別の変数を気にしないと仮定しfor(int i = 0; i < n*n; i++){...}ます。たとえばn*n、変数に割り当てないでしょうか?
セレリタス16

1
私はそうするでしょう。しかし、スピードではありません。読みやすくするため。読み取り可能なコードは、それ自体で高速になる傾向があります。
candied_orange 16

1
スコーピングの問題でさえ、定数としてマークすると消えます(final)。コンパイラーが強制的に変更を禁止している定数が、関数の後半でアクセス可能かどうかは誰が気にしますか?
jpmc26 16

大きなことはあなたが期待することだと思います。この例ではsqrtを使用していることは知っていますが、別の関数である場合はどうなりますか?関数は純粋ですか?常に同じ値を期待していますか?副作用はありますか?副作用は、すべての反復で発生する意図のあるものですか?
ピーターB

38

優れたコンパイラーはどちらの方法でも同じコードを生成します。したがって、パフォーマンスが必要な場合は、クリティカルループ内にあり、実際にプロファイルを作成し、違いがある場合にのみ変更を行います。コンパイラーが最適化できない場合でも、関数呼び出しのケースについてのコメントで指摘されているように、ほとんどの場合、パフォーマンスの差はプログラマーが考慮する価値がないほど小さくなります。

しかしながら...

コードは主に人間間のコミュニケーションの媒体であり、両方のオプションが他の人間とうまくコミュニケーションできないことを忘れてはなりません。1つ目は、反復ごとに式を計算する必要があるという印象を与え、2つ目は、初期化セクションにある式がループ内のどこかで更新されることを意味します。

私は実際にループの上に引き出してfinal、コードを読んでいる人にすぐにそれを明確にするようにします。それは変数の範囲を広げるので理想的でもありませんが、とにかくあなたの囲む関数はそれ以上ループを含んではいけません。


5
関数呼び出しの組み込みを開始すると、コンパイラーが最適化するのが非常に難しくなります。コンパイラには、Math.sqrtには副作用がないという特別な知識が必要です。
ピーターグリーン

@PeterGreenこれがJavaの場合、JVMはそれを把握できますが、しばらく時間がかかる場合があります。
クリリス

質問には、C ++とJavaの両方のタグが付けられています。私はJavaのJVMがどれだけ高度で、いつそれを理解できるかできないのかわかりませんが、C ++コンパイラは一般にそれを理解できないことを知っていますが、コンパイラに伝える非標準の注釈がない限り、関数は純粋ですまたは、関数の本体が表示され、間接的に呼び出されるすべての関数は、同じ基準で純粋として検出できます。注:副作用なしでは、ループ状態から抜け出すのに十分ではありません。ループ本体が状態を変更する可能性がある場合、グローバル状態に依存する関数をループから移動することもできません。
hvd 16

Math.sqrt(x)がMymodule.SomeNonPureMethodWithSideEffects(x)に置き換えられると、興味深いものになります。
ピーターB

9

@Karl Bielefeldtが彼の答えで言ったように、これは通常問題ではありません。

しかし、それはかつてCとC ++の一般的な問題であり、コードの可読性を低下させることなく問題を回避0しようとするトリックが発生しました

final x = 10;//whatever constant value
for(int i = Math.floor(Math.sqrt(x)); i >= 0; i--) {
  //...do something
}

現在、すべての反復の条件は、すべての>= 0コンパイラが1つまたは2つのアセンブリ命令にコンパイルするものです。過去数十年間に作成されたすべてのCPUには、次のような基本的なチェックが必要です。x64マシンでクイックチェックを行うと、これが予想通りcmpl $0x0, -0x14(%rbp)(long-int-compare値0 vsレジスタrbpオフセット-14)およびjl 0x100000f59(前の比較が「2nd-arg」の場合、ループに続く命令にジャンプする<1番目の引数」)

注私が削除したこと+ 1からMath.floor(Math.sqrt(x)) + 1、数学が機能するためには、開始値はである必要がありますint i = «iterationCount» - 1。また、注目に値するのは、イテレータに署名する必要があることです。unsigned int動作せず、おそらくコンパイラ警告が表示されます。

Cベースの言語で約20年間プログラミングした後、forward-index-iterateを行う特別な理由がない限り、逆インデックス反復ループのみを記述します。条件での単純なチェックに加えて、逆反復は、そうでなければ面倒な反復中の配列突然変異を回避します。


1
あなたが書くことはすべて技術的に正しいです。ただし、最新のCPUはすべて、特別な命令があるかどうかに関係なく、順方向反復ループで適切に動作するように設計されているため、命令に関するコメントは誤解を招く可能性があります。いずれにせよ、ほとんどの時間は通常ループ内で費やさ、反復を実行しません。
ヨルゲンフォグ

4
最新のコンパイラ最適化は、「通常の」コードを念頭に置いて設計されていることに注意してください。ほとんどの場合、コンパイラは最適化の「トリック」を使用するかどうかに関係なく、同じくらい高速なコードを生成します。しかし、いくつかのトリックは、それらがどれほど複雑かによって実際に最適化を妨げる場合があります。特定のトリックを使用してコードを読みやすくしたり、一般的なバグを見つけやすくしたりするのは素晴らしいことですが、「このようにコードを書くと速くなる」と思い込まないでください。通常の方法でコードを記述し、最適化する必要がある場所を見つけるためにプロファイルします。
0x5453 16

またunsigned、チェックを変更するとカウンターがここで機能することに注意してください(最も簡単な方法は、両側に同じ値を追加することです)。例えば、任意の減少のためDec、チェックは(i + Dec) >= Decいつものように同じ結果を持っている必要がありsigned、チェックi >= 0の両方で、signedそしてunsigned限り、言語がために、明確に定義されたラップアラウンドルールがあるとして、カウンタunsigned変数(具体的には、-n + n == 0両方のために真である必要がありますsignedunsigned)。ただし、>=0コンパイラに最適化がない場合、これは署名付きチェックよりも効率が悪いことに注意してください。
ジャスティンタイム2モニカの復活

1
@JustinTimeええ、署名された要件は最も厳しい部分です。Dec開始値と終了値の両方に定数を追加すると機能しますが、直観がはるかにi難しくなります。配列インデックスとして使用する場合unsigned int arrayI = i - Dec;は、ループ本体でもaを行う必要があります。符号なしのイテレーターでスタックしているときは、前方反復を使用します。多くの場合i <= count - 1、ロジックを逆反復ループと平行に保つための条件付きです。
スリップD.トンプソン

1
@ SlippD.Thompson Dec開始値と終了値を具体的に追加するつもりはありませんでしたが、条件チェックをDec両側にシフトしました。 for (unsigned i = N - 1; i + 1 >= 1; i--) /*...*/ これによりi、ループ内で正常に使用できるようになり、条件の左側の可能な限り低い値が保証されます0(ラップアラウンドが干渉しないようにするため)。それはだ間違いなく、符号なしのカウンターで作業する場合も、前方の反復を使用することがずっと簡単。
ジャスティンタイム2

3

Math.sqrt(x)がMymodule.SomeNonPureMethodWithSideEffects(x)に置き換えられると、興味深いものになります。

基本的に私の手口は次のとおりです。何かが常に同じ値を与えると予想される場合、一度だけ評価します。たとえば、List.Countは、ループの操作中にリストが変更されない場合、ループ外のカウントを別の変数に取得します。

これらの「カウント」の一部は、特にデータベースを扱う場合、驚くほど高価になる可能性があります。リストの反復中に変更されるはずのないデータセットで作業している場合でも。


カウントが高価な場合は、まったく使用しないでください。代わりに、あなたは相当やるべきことfor( auto it = begin(dataset); !at_end(it); ++it )
ベンフォークト

@BenVoigt反復子を使用することは、これらの操作にとって間違いなく最良の方法です。副作用を伴う非純粋なメソッドの使用についての私のポイントを説明するために、単に言及しました。
ピーターB

0

私の意見では、これは言語固有です。たとえば、C ++ 11を使用している場合、条件チェックがconstexpr関数であれば、コンパイラは毎回同じ値を生成することがわかっているため、複数の実行を最適化する可能性が非常に高いと思われます。

ただし、関数呼び出しがライブラリ関数である場合constexpr、コンパイラーはこれを推定できないため、すべての反復でほぼ確実に実行します(インラインであり、したがって純粋であると推定できる場合を除く)。

私はJavaについてあまり知りませんが、それがJITコンパイルされていることを考えると、コンパイラーは実行時に条件をインライン化して最適化するのに十分な情報を持っていると思います。しかし、これは適切なコンパイラー設計に依存し、このループを決定するコンパイラーは最適化の優先事項であり、推測することしかできません。

個人的には、可能であればforループ内に条件を入れる方が少しエレガントだと感じますが、複雑な場合はconstexpror inline関数に書き込むか、関数が純粋で最適化可能であることを示唆する言語に相当するように書きます。これにより、意図が明確になり、巨大な読み取り不能な行を作成することなく、慣用的なループスタイルが維持されます。また、それが独自の機能である場合、条件チェックに名前を付けるため、読者は複雑な場合はチェックせずに、チェックの対象を論理的にすぐに確認できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.