6

cおよびc++の引数の評価順序が定義されていないことは既知の事実です。たとえばfoo(a(),b())、上記の呼び出しでは、どの評価順序を選択するか、したがってどの関数を最初に実行するかを決定するのはコンパイラの実装次第です。最近、私の友人の1人が、なぜCまたはC++で評価の順序が指定されていないのかと尋ねました。グーグルで検索したところ、評価順序を指定するとコード生成が最適ではないことがわかりました。しかし、どうですか?引数の評価の定義された順序が最適ではないコードにつながるのはなぜですか?そして、私がJavaの引数評価順序を参照したとき。スペックで以下を見つけました。

15.7.4。引数リストは左から右に評価されます

メソッドまたはコンストラクターの呼び出しまたはクラスインスタンス作成式では、引数式はコンマで区切られた括弧内に表示される場合があります。各引数式は、その右側の引数式のどの部分よりも前に完全に評価されているように見えます。引数式の評価が突然完了した場合、その右側の引数式のどの部分も評価されていないように見えますか?

その場合、Javaには引数の評価順序が定義されていますが、CまたはC ++コンパイラーがそのような動作が指定されている場合、次善のコードを生成すると言うのは少し奇妙に思えます。これに光を当ててもらえますか?

4

4 に答える 4

9

これは部分的に歴史的です。たとえば、レジ​​スタが少ないプロセッサでは、従来の(そして単純な)最適化手法の1つは、最も多くのレジスタを最初に必要とする部分式を評価することです。たとえば、一方の部分式に5つのレジスタが必要で、もう一方の部分式に4つのレジスタが必要な場合、5が必要なものの結果を、4が必要なものでは不要なレジスタに保存できます。

これはおそらく、通常考えられているほど関連性がありません。式に副作用がない場合、または並べ替えによってプログラムの監視可能な動作が変更されない場合、コンパイラは(Javaでも)並べ替えることができます。最新のコンパイラーは、20年以上前(C ++ルールが作成されたとき)のコンパイラーよりもはるかに優れた方法でこれを判別できます。そしておそらく、彼らがこれを決定できないとき、あなたは各式で十分なことをしているので、メモリへの余分な流出は問題ではありません。

少なくとも、それは私の直感です。実際にオプティマイザーに取り組んでいる人から、大きな違いが出ると言われたことがあるので、確信は持てません。

編集:

Javaモデルに関するコメントを追加するだけです。Javaが設計されていたとき、それはインタプリタ言語として設計されていました。極端なパフォーマンスは問題ではありませんでした。目標は、極度の安全性と再現性でした。したがって、多くのことを非常に正確に指定するため、コンパイルするプログラムは、プラットフォームに関係なくまったく同じ動作をします。未定義の動作、実装で定義された動作、および未指定の動作はないと想定されていました。コストに関係なく(ただし、これは最も普及しているマシンのいずれかで妥当なコストで実行できると信じています)。C(および間接的にC ++)の最初の設計目標の1つは、不要な追加の実行時コストを最小限に抑えること、プラットフォーム間の一貫性を目標にしないこと(当時、一般的なプラットフォームでさえ大きく異なるため)、およびその安全性でした。懸念はあるものの、原始的ではありませんでした。態度はある程度進化しましたが、そこにある可能性のあるすべてのマシンを効率的にサポートできるようにするという目標はまだあります。最新の最も複雑なコンパイラテクノロジを必要としません。そして、異なる目標は当然異なる解決策につながります。

于 2012-07-12T10:50:56.620 に答える
3

Javaは、オペランドを並べ替える利点がないスタックベースの仮想マシンを想定しています。James Kanzeの回答によると、Cおよびほとんどの完全にコンパイルされた言語は、レジスタアーキテクチャを想定しています。このアーキテクチャでは、メモリへのレジスタの「スピル」は高価であり、回避する必要があります。したがって、オペランドを並べ替えた方がよい場合があります。レジスターの使用を最大化し、流出を最小化するためのもの。

于 2012-07-12T11:01:58.413 に答える
1

私たちはそれを分析しすぎていると思います。本当の答えはおそらく、K&Rが事実上の標準であったC標準の前の昔は、引数を評価する順序を指定することを気にせず、さまざまなコンパイラ実装がさまざまな方法で行われたということです。

人間の観点からの論理的な方法は、引数を左から右に評価することです(Javaのように)。コンパイラの観点からの簡単な方法は、引数の評価を右から左に行うことです。これは、引数が評価されたら、どこにも保存する必要がなく、呼び出しの準備ができた状態でスタックにプッシュできるようにするためです。引数にスタックを使用するほとんどのC実装は、逆の順序でそれらをプッシュする必要があります。これは、K&R Cには、関数が同じソースファイルで定義されていない場合に関数が取る引数の数をコンパイラが把握する方法がなく、プログラマーがそれを利用して可変個引数関数のプリミティブ形式を提供していたためです。

そのため、標準の作成者は、「正しい」方法(左から右)で実行するか、多くのコードを壊すか、既存のコンパイラのほとんどが実行する方法で実行するか、他のコードを壊すか、それに固執するかの選択に直面しました。現状とコンパイラ設計者に何をすべきかを選択させる。

それはとにかく私の意見であり、事実に基づくものではありません。

于 2012-07-12T16:13:31.930 に答える
0

あなたの例のように関数評価のためではありませんが、単純な式の場合、2つの式を並行して実行することもできます。最新のアーキテクチャはパイプライン化されており、実行する必要のある操作が重複するように、2つのパイプラインを(ほぼ)同時にフィードする方が効率的です。

また、引数を評価する式は2つしかないという印象を受けているようですが、、、、の4つがaあります。関数呼び出しの半順序は、左から右です。ba()b()

    a -- a()
             \
         f --- f(a(), b())
             /
    b -- b()

写真からわかるように、多くの潜在的な並列処理があり、最新のコンパイラーは、規定された評価順序を持たないことによって何かを得ることができます。

編集:議論を考慮して、いくつかの追加の詳細。a()およびが関数呼び出しである場合b()、関数がインライン化されている場合でも、これらの関数呼び出しがインターリーブしないことが標準で保証されています。したがって、上の画像には追加の制約がa()ありb()、何らかの方法で順序付けする必要があります。(写真にそれを入れる方法がわかりません。)

一方、マクロ評価などの他の式である場合、ゲインがある場合、これらの式はインターリーブされる可能性があります(おそらくインターリーブされます)。

于 2012-07-12T12:05:19.870 に答える