aix c コンパイラ




a=a++のセマンティクスを未定義にする理由は何ですか? (6)

a = a++;

Cでは未定義の動作です。私が求めている質問は、なぜですか?

私は、物事を行うべき一貫した順序を提供することは難しいかもしれないということを意味します。 しかし、特定のコンパイラは常に1つまたは複数のコンパイラでそれを(最適化レベルで)実行します。 だから、これはコンパイラが決めるのがなぜ正確なのですか?

明確にするために、私はこれが設計上の決定であったかどうかを知りたいと思います。 あるいは、何らかのハードウェアの制限がありますか?

(注:質問のタイトルが不明または十分でない場合は、フィードバックや変更を歓迎します)


postfix ++演算子は、インクリメントの前に値を返します。 したがって、最初のステップで、 aは古い値( ++が返す値)に割り当てられます。 次の時点では、両方の操作が同じオブジェクト( a )に適用され、これらの演算子の評価順序については何も言わないため、インクリメントまたは割り当てが最初に行われるかどうかは未定義です。


aが値0x0001ffffポインタであるとします。 また、アーキテクチャが分割されているため、コンパイラが上位と下位の部分にインクリメントを別々に適用する必要があるとします。 オプティマイザは、書き込まれた最終値が0x0002ffffように書き込みを並べ替えることができると考えられます。 つまり、インクリメントの前のローの部分とインクリメントの後のハイの部分です。

この値は、期待していたいずれかの値の2倍です。 アプリケーションが所有していないメモリを指しているかもしれませんし、(一般的に)トラッピング表現かもしれません。 言い換えれば、CPUは、この値がレジスタにロードされるとすぐにハードウェア障害を発生させ、アプリケーションをクラッシュさせる可能性があります。 すぐにクラッシュすることがなくても、アプリが使用するには間違った価値があります。

他の基本型でも同じことが起こる可能性があり、C言語ではintにもトラップ表現ができます。 Cは幅広いハードウェア上で効率的に実装を試みます。 8086などのセグメント化されたマシンで効率的なコードを取得するのは難しいです。 この未定義の動作を行うことで、言語実装者は積極的に最適化する自由が少します。 実際にパフォーマンスの相違が生じたかどうかはわかりませんが、言語委員会はオプティマイザにあらゆる利点を与えることを明らかにしていました。


いくつかの例外を除いて、式が評価される順序は指定されていません。 これは意図的な設計上の決定であり、実装によって、より効率的なマシンコードが得られるように、記述順序から評価順序を再構成することができます。 同様に、 ++および--副作用が適用される順序は、次のシーケンスポイントの前に発生するという要件を超えて指定されていません。これにより、最適な方法で操作を配置する自由が実現されます。

残念ながら、これは、 a = a++ような式の結果は、コンパイラ、コンパイラ設定、周囲のコードなどに基づいて変化することを意味する。この動作は、言語標準では特に定義されていないので、コンパイラの実装者はそのようなケースを検出し、それらに対して診断を行うことについて心配する。 a = a++ようなケースは明らかa = a++が、

void foo(int *a, int *b)
{
  *a = (*b)++;
}

これがファイル内の唯一の関数(または呼び出し元が別のファイルにある場合)の場合、コンパイル時にabが同じオブジェクトを指しているかどうかを知る方法はありません。 職業はなんですか?

すべての式が特定の順序で評価され、すべての副作用が評価の特定の時点で適用されるようにすることは、完全に可能であることに注意してください。 それはJavaやC#のようなものです。これらの言語ではa = a++ような式は常に明確に定義されています。


そのようなコードを書かなければならない理由がないため、偽のコードに対して特別な動作を要求しないため、コンパイラはより積極的に書かれたコードを最適化することができるため、未定義です。 たとえば、 *p = i++は、 piを指す場合にクラッシュを引き起こすように最適化される可能性があります。これは、2つのコアが同じメモリ位置に同時に書き込むためです。 *pが明示的にiとして書き出され、 i = i++を得るという論理的には、特定の場合にこれも未定義であるという事実。


更新:この質問は2012年6月18日のブログの主題でした 。 素晴らしい質問をありがとう!

どうして? 私はこれが設計上の決定であるかどうかを知りたいのですが、もしそうなら、それを促すのは何ですか?

基本的にはANSI Cの設計委員会の議事録を求めていますが、私はそれを手元に置いていません。 あなたの質問には、その日に部屋にいた人が決定的にしか答えることができない場合は、その部屋にいた人を見つけなければなりません。

しかし、私はより広い質問に答えることができます:

法律プログラム( * )の動作を "未定義"または "実装定義"( ** )のままにしておく言語設計委員会の主導的な要因は何ですか?

最初の主な要因は、特定のプログラムの動作に同意しない、市場における言語の2つの既存の実装が存在するかどうかです。 FooCorpのコンパイラがM(A(), B())を「コールA、コールB、コールM」としてコンパイルし、BarCorpのコンパイラは「コールB、コールA、コールM」とコンパイルし、どちらも「明らかに正しい"その場合、言語設計委員会は、"あなたはどちらか正しい "と言い、それを実装定義の振る舞いにする強いインセンティブがあります。 特に、FooCorpとBarCorpの両方が委員会に代理人を派遣している場合です。

次の主要な要素は次のとおりです。 この機能は自然に実装のさまざまな可能性を提示しますか? たとえば、C#では、コンパイラの「クエリー理解」の式の分析は、「クエリーの理解を持たない同等のプログラムに構文変換し、そのプログラムを正常に分析する」と指定されています。 実装がそうしなければならない自由はほとんどありません。

対照的に、C#仕様では、 foreachループはtryブロック内のwhileループと同じように扱われるべきですが、実装にはある程度の柔軟性があります。 AC#コンパイラでは、例えば、「配列全体に対してforeachループのセマンティクスをより効率的に実装する方法を知っています」と言って、配列の索引付け機能を使用して、仕様書に示すように配列をシーケンスに変換するのではなく、

3つ目の要素は、フィーチャが複雑なため、正確な動作の詳細な内訳が指定するのが困難または高価になることです。 C#仕様では、匿名メソッド、ラムダ式、式ツリー、動的呼び出し、イテレータブロック、および非同期ブロックの実装方法はほとんど分かりません。 必要なセマンティクスと動作に関する制限を記述し、残りを実装に委ねるだけです。

第4の要因は、この機能がコンパイラに分析するための負担を強いるかどうかです。 たとえば、C#の場合は次のようになります。

Func<int, int> f1 = (int x)=>x + 1;
Func<int, int> f2 = (int x)=>x + 1;
bool b = object.ReferenceEquals(f1, f2);

bが真であることを要求すると仮定する。 どのように2つの機能が「同じ」であるを判断するにはどうしますか? 「intensionality」分析を行う - 機能体は同じ内容ですか? - 難しく、「拡張性」分析を行う - 同じ入力が与えられたときに関数が同じ結果を持つか? - さらに困難です。 言語仕様委員会は、実装チームが解決しなければならないオープンな研究課題の数を最小限に抑えるよう努めなければならない!

C#では、これは実装定義のままです。 コンパイラはその裁量で参照を同値にするかどうかを選択できます。

第5の要因は、機能がランタイム環境に大きな負担をかけるかどうかです。

たとえば、C#逆参照では、配列の終わりが明確に定義されています。 array-index-out-of-bounds例外を生成します。 この機能は、ゼロではなく、実行時に小さなコストで実装することができます。 null受信側でインスタンスまたは仮想メソッドを呼び出すことは、nullで参照されていない例外を生成することとして定義されています。 再び、これは小さいがゼロではないコストで実施することができる。 未定義のビヘイビアを排除する利点は、ランタイムコストが小さいことにかかります。

第6の要因は、定義された振る舞いをいくつかの大きな最適化を排除するものにすることですか? たとえば、C#は、副作用の原因となるスレッドから観察された場合の副作用の順序を定義します。 しかし、別のスレッドからのあるスレッドの副作用を観察するプログラムの動作は、いくつかの "特別な"副作用を除いて実装定義されています。 (揮発性の書き込みやロックのように)C#言語では、すべてのスレッドが同じ副作用を同じ順序で観察する必要がある場合、最新のプロセッサーが効率的にジョブを実行することを制限する必要があります。 現代のプロセッサは、高水準の性能を得るために、アウト・オブ・オーダー実行と洗練されたキャッシング戦略に依存している。

それらは心に浮かぶ少数の要素に過ぎません。 もちろん、「実装定義」または「未定義」の機能を作成する前に、言語設計委員会が議論する他の多くの要因があります。

さあ、あなたの具体例に戻りましょう。

C#言語 、その動作を厳密に定義します( )。 インクリメントの副作用が割り当ての副作用の前に発生することが観察されます。 行動を選択してそれに固執することができるので、そこには「まあ、それはただ不可能です」という議論はありません。 これは、最適化のための大きな機会を妨げることもありません。 そして可能な複雑な実装戦略の多様性はありません。

C言語委員会は、市場に複数のコンパイラがあったために、実装が定義した振る舞いに副作用を命ずることになっていたことは間違いないと思います。委員会は彼らの半分に彼らが間違っていると言うことを嫌っていた。

* )あるいは時にはそのコンパイラ! しかし、その要素を無視しましょう。

** ) "未定義"の動作は、コードがハードディスクの消去を含む何らかの操作を実行できることを意味します。 コンパイラは、特定の振る舞いを持つコードを生成する必要はなく、未定義の振る舞いを持つコードを生成していることを伝える必要はありません。 "Implementation defined"という振る舞いは、コンパイラ作成者が実装戦略を選択する際にかなりの自由度が与えられることを意味しますが、戦略選択し、 一貫して使用しその選択記録する必要があります。

)単一のスレッドから観測された場合はもちろんです。


誰かが別の理由を提供するかもしれませんが、最適化(より良いと言うアセンブラプレゼンテーション)の観点からCPUレジスタに読み込む必要があります、後置演算子の値は別のレジスタまたは同じに配置する必要があります。 したがって、最後の割り当ては、オプティマイザが1つまたは2つのレジスタを使用するかどうかによって異なります。







language-lawyer