16
using System; 

namespace ConsoleApplication1
{ 
    class TestMath
    {  
        static void Main()
        {
            double res = 0.0;

            for(int i =0;i<1000000;++i)
                res +=  System.Math.Sqrt(2.0);

            Console.WriteLine(res);

            Console.ReadKey();  
        }
    }
}

このコードを C++ バージョンに対してベンチマークすると、パフォーマンスが C++ バージョンよりも 10 倍遅いことがわかりました。私はそれで問題ありませんが、それは私を次の質問に導きます:

(いくつかの検索の後) JIT コンパイラーは、C++ コンパイラーができるようにこのコードを最適化できないようです。つまり、sqrt を 1 回呼び出して *1000000 を適用するだけです。

JITにそれを強制する方法はありますか?

4

3 に答える 3

10

C++ バージョンのクロックは 1.2 ミリ秒、C# バージョンのクロックは 12.2 ミリ秒です。その理由は、C++ コード ジェネレーターとオプティマイザーが発行するマシン コードを見ればすぐにわかります。ループを次のように書き換えます (C# の等価物を使用):

double temp = Math.Sqrt(2.0);
for (int i = 0; i < 1000000; ++i) {
    res += temp;
}

これは、「不変コード モーション」と「ループ巻き上げ」と呼ばれる 2 つの最適化の組み合わせです。つまり、C++ コンパイラは sqrt() 関数について十分に認識しており、その戻り値が周囲のコードの影響を受けないため、自由に移動できることを認識しています。そして、そのコードをループの外に移動し、結果を格納するための追加のローカル変数を作成することは価値があります。そして、sqrt() の計算は加算よりも遅いです。当たり前のように聞こえますが、これはオプティマイザーに組み込む必要があるルールであり、非常に多くのルールの 1 つとして考慮する必要があります。

そして、はい、ジッターオプティマイザーはそれを見逃しています. C++ オプティマイザと同じ時間を費やすことができないという罪悪感があり、厳しい時間制約の下で動作します。時間がかかりすぎると、プログラムの開始に時間がかかりすぎるためです。

冗談: C# プログラマーは、コード ジェネレーターよりも少し賢く、これらの最適化の機会を自分で認識する必要があります。これはかなり明白なものです。さて、とにかくそれについて知ったので:)

于 2012-12-24T22:02:55.390 に答える
6

必要な最適化を行うために、コンパイラは、関数Sqrt()が特定の入力に対して常に同じ値を返すことを保証する必要があります。

コンパイラは、関数がステートレスかどうかを確認するために、関数が他の「外部」変数を使用していないことをあらゆる種類のチェックを行うことができます。しかし、それはまた、副作用の影響を受けないということを常に意味するわけではありません.

関数がループ内で呼び出される場合、各反復で呼び出す必要があります (これが重要である理由を理解するには、マルチスレッド環境を考えてみてください)。したがって、通常、そのような最適化が必要な場合、ループから一定のものを取り出すのはユーザー次第です。

C++ コンパイラに戻ると、コンパイラはそのライブラリ関数に対して特定の最適化を行っている場合があります。多くのコンパイラは、数学ライブラリなどの重要なライブラリを最適化しようとしているため、コンパイラ固有のものである可能性があります。

もう 1 つの大きな違いは、C++ では通常、ヘッダー ファイルからそのようなものをインクルードすることです。これは、関数呼び出しが呼び出し間で変化しないかどうかを判断するために必要なすべての情報をコンパイラが持っている可能性があることを意味します。

.Net コンパイラ (コンパイル時 - Visual Studio) は、解析するすべてのコードを常に持っているわけではありません。ほとんどのライブラリ関数は既にコンパイルされています (IL に - 第 1 段階)。そのため、サードパーティの dll を考慮して、深い最適化を行うことができない場合があります。また、JIT (ランタイム) コンパイルでは、アセンブリ全体でこの種の最適化を行うにはコストがかかりすぎるでしょう。

于 2012-12-24T20:17:07.450 に答える
5

Math.Sqrtとして注釈が付けられている場合、JIT (または C# コンパイラ) に役立つ可能性があります[Pure]。次に、関数への引数が例のように一定であると仮定すると、値の計算はループの外で持ち上げることができます。

さらに、このようなループは次のコードに合理的に変換できます。

double res = 1000000 * Math.Sqrt(2.0);

理論的には、コンパイラまたは JIT がこれを自動的に実行できます。ただし、実際のコードではめったに発生しないパターンの最適化になるのではないかと思います。

ReSharperの機能リクエストを開き、設計時のツールがそのようなリファクタリングを提案することを提案しました。

于 2012-12-24T21:41:45.700 に答える