3

変数に値を代入し、次に 2 番目の値を代入したいが、それが条件を満たしている場合にのみ、簡略化された if ステートメントを使用するのは効率的ですか? ここに例があります。

これはより効率的ですか

int x = GetInt();
if (x < 5)
    x = 5;

これより

int x = GetInt();
x = x < 5 ? 5 : x;

私が本当に求めているのはx、条件を満たさない場合x = x、else ステートメントがパフォーマンスに影響を与えるかということだと思います。

4

3 に答える 3

11

私はこのバージョンが好きです:

int x = Math.Max(5, GetInt());

ただし、これらはすべて時期尚早の最適化であることに注意してください。フレームワークを変更して新しい JIT 最適化または別の JIT 最適化を追加する Windows Update のような単純なもので、今日は速くても、明日は遅くなる可能性があります。

大きなループ内で次のようなチェックを実行している場合、私が時間を費やす可能性があるのは次のとおりです。

 var items = Enumerable.Range(0, 1000000);

 foreach(int item in items)
 {
     if (item % 3 == 0)
     { 
         //...
     }
     else
     {
         //...
     }
 }

ループをチェックする理由は、コードが可能な限り迅速に何度も実行され、小さな非効率性に重点が置かれるためではなく、 または を使用するかどうかがifループelse全体で変化するためです。

分岐予測として知られる最新の CPU の機能により、そのコードは非効率的であると予想されます。ifandの内容がelse十分に重要であり、十分に異なる場合、フロントエンドでこれらすべてのチェックを実行して (そしてすべての予測が失敗するように) 移動し、その後にすべての を一緒に実行することで、そのコードをより高速に実行ifできます。すべてのelsesによって。if と else を実行する第 2 フェーズでの分岐予測 (これはおそらく実行コストがはるかに高くなります) がより正確になるため、より高速になります。

違いを示す小さなプログラムを次に示します。

class Program
{
    static int samplesize = 1000000;
    //ensure these are big enough that we don't spend time allocating new buffers while the stopwatch is running
    static Dictionary<int, string> ints = new Dictionary<int,string>(samplesize * 4); 
    static Dictionary<double,string> doubles = new Dictionary<double,string>(samplesize * 4);

    static void Main(string[] args)
    {
        var items = Enumerable.Range(0, samplesize).ToArray() ;
        var clock = new Stopwatch();

        test1(items); //jit hit, discard first run. Also ensure all keys already exist in the dictionary for both tests
        clock.Restart();
        test1(items);
        clock.Stop();
        Console.WriteLine("Time for naive unsorted: " + clock.ElapsedTicks.ToString());

        test2(items); //jit hit
        clock.Restart();
        test2(items);
        clock.Stop();
        Console.WriteLine("Time for separated/branch prediction friendly: " + clock.ElapsedTicks.ToString());

        Console.ReadKey(true);
    }

    static void test1(IEnumerable<int> items)
    {
        foreach(int item in items)
        {
            //different code branches that still do significant work in the cpu
            // doing more work here results in a larger branch-prediction win, to a point
            if (item % 3 == 0)
            {   //force hash computation and multiplication op (both cpu-bound)
                ints[item] = (item * 2).ToString();
            }
            else
            {
                doubles[(double)item] = (item * 3).ToString();
            }
        }
    }

    static void test2(IEnumerable<int> items)
    {
        //doing MORE work: need to evaluate our items two ways, allocate arrays
        var intItems = items.Where(i => i % 3 == 0).ToArray();
        var doubleItems = items.Where(i => i % 3 != 0).ToArray();

        // but now there is no branching... adding all the ints, then adding all the doubles.
        foreach (var item in intItems) { ints[item] = (item * 2).ToString(); }
        foreach (var item in doubleItems) { doubles[(double)item] = (item * 3).ToString(); }
    }
}

そして、私のマシンでの結果は、より多くの作業を行う 2 番目のテストがより速く実行されたことです。

単純な未ソートの時間: 1118652
分離/分岐予測に適した時間: 1005190

ここで取り上げるべき重要なことは、すべてのループが分岐予測の恩恵を受けることができるかどうかを検討する必要があるということではありません。これは、驚くべきパフォーマンス結果をもたらす数多くの CPU 機能の 1 つにすぎません。ここで重要なことは、コードがどのように機能するかを確実に知るには、実際にパフォーマンスを測定する必要があるということです。これを慎重に構築しなければ、単純な手法でも勝てます (最初の試みでは、期待した速度が得られませんでした)。

さらに、これらのケースではそれほど大きな違いはないことを指摘する必要があります。このパフォーマンスの向上はそれだけの価値がありましたか、それとも他の場所で時間を費やしたほうがよかったでしょうか? それを知る唯一の方法は、アプリ全体のパフォーマンスを実際に測定し、実際に時間を費やしている場所を見つけることです。本来よりも実際に遅いのはどこですか?これはプロファイリングと呼ばれ、これを正確に行うのに役立つツールがあります。

于 2013-05-30T14:33:15.113 に答える
3

このコード

void Main()
{
        int x = GetInt();
        x = x < 5 ?  5 : x;
}

int GetInt()
{return 5;}

このようにILに翻訳されます

IL_0000:  ldarg.0     
IL_0001:  call        UserQuery.GetInt
IL_0006:  stloc.0     // x
IL_0007:  ldloc.0     // x
IL_0008:  ldc.i4.5    
IL_0009:  blt.s       IL_000E
IL_000B:  ldloc.0     // x
IL_000C:  br.s        IL_000F
IL_000E:  ldc.i4.5    
IL_000F:  stloc.0     // x

GetInt:
IL_0000:  ldc.i4.5    
IL_0001:  ret         

これは

void Main()
{
    int x = GetInt();
    if (x < 5) x = 5;
}            

int GetInt()
{return 5;}

に翻訳されます

IL_0000:  ldarg.0     
IL_0001:  call        UserQuery.GetInt
IL_0006:  stloc.0     // x
IL_0007:  ldloc.0     // x
IL_0008:  ldc.i4.5    
IL_0009:  bge.s       IL_000D
IL_000B:  ldc.i4.5    
IL_000C:  stloc.0     // x

GetInt:
IL_0000:  ldc.i4.5    
IL_0001:  ret         

したがって、これはより「効率的」に思えます (?)。

しかし、これは実際にはコードに何の違いももたらさないマイクロ最適化であるため、最も読みやすいものを使用することをお勧めします (私の意見では、これは最後のものと一致します)。

編集 間違いなく Joel Coehoorn の答えは最高のものです:(少なくとも読みやすさとコードサイズの点で)

IL_0000:  ldc.i4.5    
IL_0001:  ldarg.0     
IL_0002:  call        UserQuery.GetInt
IL_0007:  call        System.Math.Max

GetInt:
IL_0000:  ldc.i4.5    
IL_0001:  ret     
于 2013-05-30T14:35:49.880 に答える
2

x が getter/setter に関連付けられていない場合、コンパイラは x = x の形式ですべてのステートメントを除外する必要があります。その結果、他のものもすべてなくなります。

于 2013-05-30T14:30:53.120 に答える