web-dev-qa-db-ja.com

重要な条件ステートメントをループの初期化セクションに移動する必要がありますか?

私はこのアイデアを 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
}
21
Celeritas

私がやろうとしていることは次のようなものです:

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)をループヘッダーに初期化する正当な理由は、そのスコープを正しく保つことです。非問題がニースのタイトな囲みスコープであることを確認するために必要なことはすべてです。

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

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

62
candied_orange

優れたコンパイラはどちらの方法でも同じコードを生成するため、パフォーマンスを向上させる場合は、クリティカルループおよびにある場合にのみ変更を加えます。実際にそれをプロファイリングし、それが違いを生むことを発見しました。コンパイラーが最適化できない場合でも、関数呼び出しのケースに関するコメントで人々が指摘したように、ほとんどの状況では、パフォーマンスの差が小さすぎてプログラマーの考慮に値しません。

ただし...

コードは主に人間間の通信の媒体であり、どちらのオプションも他の人間とうまく通信しないことを忘れてはなりません。最初の式は、反復ごとに式を計算する必要があるという印象を与え、2番目の式は初期化セクションにあるため、ループ内のどこかで更新されることを意味します。

私は実際にそれをループの上に引き出してfinalにして、コードを読んでいる人がすぐにそして豊富にそれを明確にすることを望みます。変数のスコープが増えるため、これも理想的ではありませんが、囲み関数には、そのループよりも多くのループを含めることはできません。

38
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 value 0 vs. register rbp offsetted -14)および_jl 0x100000f59_(次の命令にジャンプ)前の比較が「2nd-arg <1st-arg」)の場合にループします。

Math.floor(Math.sqrt(x)) + 1から_+ 1_を削除したことに注意してください。数学がうまくいくためには、開始値は_int i = «iterationCount» - 1_でなければなりません。また、注目に値するのは、イテレーターに署名が必要なことです。 _unsigned int_は機能せず、コンパイラ警告が表示される可能性があります。

Cベースの言語で約20年間プログラミングした後、forward-index-iterateする特別な理由がない限り、reverse-index-iteratingループのみを記述します。条件文でのより簡単なチェックに加えて、逆反復はしばしば、そうでなければ面倒な配列の変更中に反復することになる問題を回避します。

9

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

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

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

3
Pieter B

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

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

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

個人的には、可能であればforループ内に条件を置く方が少しエレガントだと思いますが、複雑な場合はconstexprまたはinline関数、または同等の言語に記述します関数が純粋で最適化可能であることを示唆します。これにより、意図が明確になり、読みにくい大きな行を作成することなく、慣用的なループスタイルが維持されます。また、条件チェックが独自の機能である場合は、条件チェックに名前を付けます。これにより、読者は複雑な場合にチェックを読み取らずに、チェックの目的を論理的に即座に確認できます。

0
Vality