web-dev-qa-db-ja.com

C ++コンパイラは不要な括弧を削除/最適化しますか?

コードは

int a = ((1 + 2) + 3); // Easy to read

より遅く走る

int a = 1 + 2 + 3; // (Barely) Not quite so easy to read

または、「役に立たない」括弧を削除/最適化するのに十分に賢い最新のコンパイラーです。

非常に小さな最適化の問題のように思えるかもしれませんが、C#/ Java/...ではなくC++を選択することは、最適化(IMHO)に関するすべてです。

19
Serge

コンパイラが実際に括弧を挿入または削除することはありません。それは、式に対応する解析ツリー(括弧が存在しない)を作成するだけであり、そのためには、記述した括弧を尊重する必要があります。式を完全に括弧で囲むと、解析ツリーが何であるかが人間の読者にもすぐにわかります。 int a = (((0)));のように露骨に冗長な括弧を入れるという極端な場合、パーサーでいくつかのサイクルを無駄にする一方で、結果の解析を変更せずに、リーダーのニューロンに無駄なストレスを与えます。ツリー(および生成されたコード)のわずかなビット。

しないでくださいかっこを記述した場合でも、パーサーは解析ツリーを作成する役割を果たさなければならず、演算子の優先順位と結合規則のルールにより、どの構文解析ツリーを構築する必要があるかが正確にわかります。これらの規則は、コンパイラーにどの(暗黙の)括弧をコードに含めるかを指示するものと考えるかもしれません挿入この場合、パーサーは実際には括弧を処理しません:生成するために構築されただけです括弧が特定の場所にある場合と同じ解析ツリー。 int a = (1+2)+3;(_+_の結合性は左側にある)のように、括弧をこれらの場所に正確に配置すると、パーサーはわずかに異なるルートで同じ結果に到達します。 int a = 1+(2+3);のように別の括弧を使用すると、別の解析ツリーが強制されます。これにより、別のコードが生成される可能性があります(ただし、コンパイラが解析ツリーを構築した後に変換を適用する場合があるため、 実行の効果である限り、結果のコードはそれに対して異なることはありません)。再実行するコードに違いがあるとすると、どちらがより効率的であるかについては、一般的に言えません。最も重要な点はもちろん、ほとんどの場合、解析ツリーは数学的に同等の式を与えないため、実行速度を比較することは重要ではありません。式を書くだけで適切な結果が得られます。

つまり、正確さのために必要に応じて、また読みやすさのために必要に応じて、括弧を使用します。冗長な場合、それらは実行速度にまったく影響を与えません(そしてコンパイル時間にはほとんど影響しません)。

そして、これには最適化に関係するものがありません。これは、解析ツリーが構築された後に行われるため、解析ツリーがどのように構築されたかを知ることができません。これは、コンパイラーの最も古くて愚かなコンパイラーから、最もスマートで最新のコンパイラーに変更なしで適用されます。インタプリタ言語(「コンパイル時間」と「実行時間」が一致する場合)でのみ、余分な括弧のペナルティが発生する可能性がありますが、それでも、そのような言語のほとんどは、少なくとも解析フェーズが1回だけ実行されるように編成されていると思いますステートメントごと(実行のために事前に解析された形式のフォームを保存)。

87

括弧はコンパイラーではなく、ユーザーのためにあります。コンパイラーは、ステートメントを表す正しいマシンコードを作成します。

参考までに、コンパイラーはそれを完全に最適化できるほど賢いです。あなたの例では、これはint a = 6;コンパイル時。

46
gbjbaanb

あなたが実際に尋ねた質問に対する答えは「いいえ」ですが、あなたが尋ねようとしていた質問に対する答えは「はい」です。括弧を追加してもコードの速度は低下しません。

最適化について質問しましたが、括弧は最適化とは関係ありません。コンパイラーは、生成されたコードのサイズまたは速度のいずれか(場合によっては両方)を改善することを目的として、さまざまな最適化手法を適用します。たとえば、式A ^ 2(Aの2乗)を受け取り、A x A(Aにそれ自身を乗算したもの)で置き換えると、より高速になります。ここでの答えは「いいえ」です。コンパイラーは、括弧が存在するかどうかに応じて、最適化フェーズで違いはありません。

可読性を向上させると思われる場所で、式に不要な括弧を追加した場合に、コンパイラーが同じコードを生成するかどうかを尋ねるつもりだったと思います。言い換えると、かっこを追加すると、コンパイラは、どういうわけかより貧弱なコードを生成するのではなく、それらを再び削除するのに十分スマートになります。答えは常にイエスです。

それを注意深く言いましょう。完全に不要な(式の評価の意味や評価の順序に影響を与えない)式に括弧を追加すると、コンパイラーはそれらを自動的に破棄して同じコードを生成します。

ただし、明らかに不必要な括弧が式の評価の順序を実際に変更する特定の式が存在し、その場合、コンパイラーは実際に記述したものを有効にするコードを生成しますが、意図したものとは異なる場合があります。例を示します。 これを行わないでください!

short int a = 30001, b = 30002, c = 30003;
int d = -a + b + c;    // ok
int d = (-a + b) + c;  // ok, same code
int d = (-a + b + c);  // ok, same code
int d = ((((-a + b)) + c));  // ok, same code
int d = -a + (b + c);  // undefined behaviour, different code

必要に応じてかっこを追加しますが、かっこが本当に不要であることを確認してください。

私は決してしません。エラーのリスクがあり、実際の利益はありません。


脚注:符号なしの動作は、符号付き整数式が、表現可能な範囲外の値(この場合は-32767〜+32767)に評価されると発生します。これは複雑なトピックであり、この回答の範囲外です。

23
david.pfx

括弧は、演算子の優先順位を操作するためだけにあります。コンパイルされると、ランタイムはブラケットを必要としないため、ブラケットは存在しません。コンパイルプロセスでは、角かっこ、スペース、およびその他の必要な構文上の砂糖がすべて削除され、すべての演算子がコンピューターで実行できるように[はるかに]単純なものに変更されます。

それで、あなたと私がどこで見るか...

  • 「int a =((1 + 2)+ 3);」

...コンパイラーは次のようなものを出力する場合があります。

  • Char [1] :: "a"
  • Int32 :: DeclareStackVariable()
  • Int32 :: 0x00000001
  • Int32 :: 0x00000002
  • Int32 :: Add()
  • Int32 :: 0x00000003
  • Int32 :: Add()
  • Int32 :: AssignToVariable()
  • void :: DiscardResult()

プログラムは、最初から開始して各命令を順番に実行することで実行されます。
オペレーターの優先順位が「先着順」になりました。
すべてが強く型付けされています。これは、コンパイラーがすべてのthatを処理したためです。

OK、それはあなたや私が扱っているようなものではありませんが、私たちはそれを実行していません!

7
Phill W.

浮動小数点かどうかによって異なります。

  • 浮動小数点では、算術加算は関連付けられないため、オプティマイザーは演算を並べ替えることができません(fastmathコンパイラスイッチを追加しない限り)。

  • 整数演算では、それらは並べ替えられます。

あなたの例では、両方がまったく同じコードにコンパイルされるため、両方がまったく同じ時間に実行されます(加算は左から右に評価されます)。

ただし、JavaおよびC#でも最適化でき、実行時に実行するだけです。

6
ratchet freak

通常のC++コンパイラは、C++自体ではなく、マシンコードに変換されます。はい、不要な括弧を削除します。完了した時点では、括弧はまったく存在しないためです。マシンコードはそのようには機能しません。

6
The Spooniest

どちらのコードも6としてハードコーディングされています。

movl    $6, -4(%rbp)

自分で確認してください ここ

5
MonoThreaded

いいえ、しかしそうです、しかし多分、しかし多分その逆ですが、いいえ。

人々が既に指摘したように(C、C++、C#、Javaなど、追加が左結合である言語を想定すると)、式((1 + 2) + 3)1 + 2 + 3とまったく同じです。それらは、ソースコードに何かを書き込むさまざまな方法であり、結果のマシンコードまたはバイトコードに影響を与えません。

どちらにしても、結果は、たとえば2つのレジスターを追加してから3番目のレジスターを追加するか、スタックから2つの値を取得して追加し、それをプッシュバックしてから別のレジスターを使用してそれらを追加するか、3つのレジスターを単一の操作で追加するか、他の方法で3つの数値を合計する次のレベルで最も賢明なもの(マシンコードまたはバイトコード)に依存します。バイトコードの場合、それはおそらくその中で同様の再構築を受けるでしょう。これに相当するILは(スタックへの一連のロードであり、ペアをポップして追加してから結果をプッシュバックします)、そのロジックをマシンコードレベルで直接コピーしませんが、問題のマシン。

しかし、あなたの質問にはもっと何かがあります。

正気なC、C++、Java、またはC#コンパイラーの場合、私はあなたが与える両方のステートメントの結果が次とまったく同じ結果になると期待します。

int a = 6;

結果のコードがリテラルの計算に時間を費やす必要があるのはなぜですか?プログラムの状態を変更しても、1 + 2 + 36であるという結果が停止することはありません。そのため、実行中のコードでこれを行う必要があります。確かに、それでもそうでないかもしれません(その6で何をするかによっては、全部を捨てることができます。C#でも「ジッターはこれを最適化するので、大幅に最適化しない」という哲学で、 int a = 6と同等か、不要なものをすべて捨てます)。

しかし、これはあなたの質問の拡張の可能性につながります。以下を検討してください。

int a = (b - 2) / 2;
/* or */
int a = (b / 2)--;

そして

int c;
if(d < 100)
  c = 0;
else
  c = d * 31;
/* or */
int c = d < 100 ? 0 : d * 32 - d
/* or */
int c = d < 100 && d * 32 - d;
/* or */
int c = (d < 100) * (d * 32 - d);

(最後の2つの例は有効なC#ではありませんが、他のすべての例は有効であり、C、C++、およびJavaでも有効です。)

ここでも、出力に関してはまったく同じコードを使用しています。これらは定数式ではないため、コンパイル時に計算されません。あるフォームが別のフォームよりも高速である可能性があります。どちらが速いですか?それはプロセッサに依存し、おそらくかなり恣意的な状態の違いに依存します(特に、1つがより高速である場合、それほど高速ではない可能性があるためです)。

そして、それらは主に何かが概念的に行われた順序の違いに関するものであるため、あなたの質問と完全に無関係ではありません。

それらのそれぞれにおいて、一方が他方よりも速いかもしれないと疑う理由があります。単一のデクリメントには特別な命令があるため、(b / 2)--は実際に(b - 2) / 2よりも高速である可能性があります。 d * 32は、おそらくd << 5に変換してd * 32 - dd * 31よりも高速にすることで、より高速に生成される可能性があります。最後の2つの違いは特に興味深いものです。 1つは一部の処理をスキップすることを許可しますが、もう1つは分岐の誤予測の可能性を回避します。

したがって、これにより2つの質問が残ります。 2.コンパイラは遅いものを速いものに変換しますか?

そして答えは1です。 2.多分。

拡大するかどうかは、問題のプロセッサに依存しているためです。確かに、一方に相当する単純な機械語コードが、もう一方に相当する単純な機械語コードよりも速いプロセッサーが存在します。電子コンピューティングの歴史の中で、常に高速なものもありませんでした(特に、パイプライン処理されていないCPUがより一般的だった場合、分岐の誤予測要素は、多くの人には関係がありませんでした)。

そして、おそらく、コンパイラー(およびジッター、スクリプトエンジン)が実行するさまざまな最適化がたくさんあり、一部のケースでは特定の要件が課せられることもありますが、論理的に同等なコードの一部を見つけることができます。最もナイーブなコンパイラでもまったく同じ結果といくつかの論理的に等価なコードがあり、最も洗練されたコンパイラでも、一方よりも速いコードを生成します(ポイントを証明するためだけに完全に病理学的なものを記述する必要がある場合でも)。

非常に小さな最適化の問題のように思えるかもしれませんが、

いいえ。ここで説明するよりも複雑な違いがあっても、最適化とはまったく関係のない非常に細かい懸念のようです。どちらかと言えば、読みにくい((1 + 2) + 3は読みやすい1 + 2 + 3よりも遅くなる可能性があるため、悲観化の問題です。

ただし、C#/ Java/...ではなくC++を選択することは、最適化(IMHO)に関するすべてです。

それが本当にC#またはJava=が「すべて」である)を選択する場合、私は人々がStroustrupおよびISO/IEC 14882のコピーを書き込み、C++コンパイラのスペースを解放して、いくつかのMP3または何かのための余地を残してください。

これらの言語には、互いに異なる利点があります。

その1つは、C++の方がメモリ使用量が一般的に高速で軽量であることです。ええ、C#やJavaの方が高速である、および/またはメモリのアプリケーション寿命の使用が優れている例がいくつかあります。これらは、関連するテクノロジーが向上するにつれて一般的になりつつありますが、 C++で記述された平均的なプログラムは、その2つの言語のいずれかで同等のものよりも速く、より少ないメモリを使用して、その仕事をする小さな実行可能ファイルであると期待します。

これは最適化ではありません。

最適化は、「物事をより速くする」という意味で使用されることがあります。私たちが実際に「最適化」について話しているとき、私たちは確かに物事をより速くすることについて話しているので、それは理解できます。そのため、一方は他方の短縮形になり、私自身もそのようにWordを誤用していることを認めます。

「物事を速くする」という正しい言葉は、最適化ではありません。ここでの正しい単語はimprovementです。プログラムに変更を加え、唯一の意味のある違いは、プログラムがより高速になり、最適化されていないことです。

最適化は、特定の側面および/または特定のケースに関して改善を行うときです。一般的な例は次のとおりです。

  1. あるユースケースでは高速になりましたが、別のユースケースでは遅くなります。
  2. 今は高速ですが、より多くのメモリを使用します。
  3. 今ではメモリは軽くなりますが、遅くなります。
  4. 現在は高速ですが、保守が困難です。
  5. メンテナンスが簡単になりましたが、遅くなります。

このような場合は、たとえば次の場合に正当化されます。

  1. 高速なユースケースは、一般的であるか、そもそも厳しく妨げられています。
  2. プログラムは許容できないほど遅く、私たちはたくさんのRAM無料です。
  3. プログラムは非常に多くのRAM=を使用したため、非常に高速な処理を実行するよりも多くの時間をスワップに費やしました。
  4. プログラムは許容できないほど遅く、理解しにくいコードは十分に文書化されており、比較的安定しています。
  5. プログラムは依然として許容範囲内で高速であり、より理解しやすいコードベースは維持費が安く、他の改善をより容易に行うことができます。

しかし、そのような場合は他のシナリオでも正当化されません。コードは絶対的な間違いのない品質測定によって改善されたわけではなく、特定の用途により適した特定の点で改善されました。最適化。

速度、メモリ使用量、読みやすさがすべて影響を受ける可能性があるため、ここで言語の選択が影響しますが、他のシステムとの互換性、ライブラリの可用性、ランタイムの可用性、特定のオペレーティングシステムでのランタイムの成熟度も影響を受ける可能性があります(私の罪のために、私はどういうわけかLinuxとAndroidを私のお気に入りのOSとして、C#を私のお気に入りの言語として、そしてMonoは素晴らしいですが、私はまだこれにかなり反対しています) 。

「C#/ Java/...を介したC++の選択はすべて最適化に関するものです」と言っても、最適化は「より優れている」ではなく「より優れている」という理由でC++が本当にうまいと考える場合にのみ意味があります。 C++自体が優れていると思う場合、必要な最後のことは、そのような可能なマイクロオプトについて心配することです。実際、おそらくそれを放棄する方がよいでしょう。幸せなハッカーも最適化する品質です!

しかし、「C++が大好きで、C++が気に入っている点の1つが余分なサイクルを押し出すことだ」と言う傾向がある場合、それは別の問題です。マイクロオプトがそれを再帰的な習慣にできる場合にのみ価値があるというのは依然としてケースです(つまり、自然にコーディングする傾向がある方法は、遅いよりも速いことが多いです)。そうでなければ、それらは時期尚早な最適化でさえなく、物事を悪化させる時期尚早の悲観論です。

1
Jon Hanna

括弧は、どの順序式を評価するかをコンパイラーに伝えるためにあります。とにかく使用される順序を指定しているため、(読みやすさを向上または悪化させる場合を除いて)役に立たない場合があります。時々彼らは順序を変えます。に

int a = 1 + 2 + 3;

事実上、存在するすべての言語には、合計が1 + 2を加算し、次に結果に3を加算して評価されるというルールがあります。

int a = 1 + (2 + 3);

次に、括弧は異なる順序を強制します。最初に2 + 3を加算し、次に1と結果を加算します。括弧の例では、とにかく生成されるのと同じ注文が生成されます。この例では、演算の順序が少し異なりますが、整数の加算が機能する方法は、結果は同じです。に

int a = 10 - (5 - 4);

括弧は重要です。それらを省略すると、結果は9から1に変わります。

コンパイラーがどの操作をどの順序で実行するかを決定した後、括弧は完全に忘れられます。この時点でコンパイラが覚えているのは、どの操作をどの順序で実行するかだけです。したがって、コンパイラーがここで最適化できるものは何もありません。括弧はgoneです。

0
gnasher729

私は言われたことの多くに同意します、しかし…ここでの過剰なアーチは、括弧が操作の順序を強制するためにそこにあるということです...コンパイラが絶対に行っています。はい、それはマシンコードを生成します…しかし、それはポイントではなく、求められていることではありません。

かっこは確かになくなっています。前述のように、かっこはマシンコードの一部ではありません。つまり、数字であり、他のものではありません。アセンブリコードはマシンコードではなく、人間が読める形式で、命令をオペコードではなく名前で含んでいます。マシンは、オペコードと呼ばれるもの、つまりアセンブリ言語の数値表現を実行します。

Javaのような言語は、それらを生成するマシンで部分的にのみコンパイルされるため、中間の領域に分類されます。それらは、それらを実行するマシンで特定のコードをマシンにコンパイルするためにコンパイルされますが、違いはありませんこの質問に-最初のコンパイル後も括弧は消えています。

0
jinzai

コンパイラは、言語に関係なく、すべての中置数学を後置に変換します。言い換えれば、コンパイラが次のようなものを見たとき:

((a+b)+c)

それをこれに変換します:

 a b + c +

これは、中置記法の方が読みやすいためですが、後置記法は、仕事を完了するためにコンピュータが実行する必要のある実際の手順に非常に近いためです(また、十分に開発されたアルゴリズムがすでにあるためです)。定義によると、postfixは操作の順序または括弧のすべての問題を排除しているため、実際に実際にマシンコードを作成する際にはるかに簡単です。

この件の詳細については、 逆ポーランド記法に関するウィキペディアの記事 をお勧めします。

0
Brian Drozd