3

如果我给一个变量赋值,然后想给它赋值第二个值,但前提是它满足一个条件,那么使用简写 if 语句是否同样有效?这是一个例子。

这是不是更有效率

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

比这个

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

我想我真正要问的是如果x不满足条件那么x = xelse 语句中的会影响性能吗?

4

3 回答 3

11

我喜欢这个版本:

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

但请记住,所有这些都是过早的优化。今天更快的东西明天可能会变慢,就像 Windows 更新这样简单的东西,它改变了框架以添加新的或不同的 JIT 优化。

如果你在一个大循环中运行这样的检查,我可能会花一些时间寻找:

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

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

检查循环的原因不是因为代码会尽可能快地运行多次,更强调小的低效率,而是因为在整个循环中来回使用if或更改。else

由于现代 cpu 的一项功能称为分支预测,我希望该代码效率低下。如果ifand的内容else足够重要且足够不同,您可以通过在前端执行所有这些检查(并将所有预测失败),然后一起运行所有 s来使代码运行if更快由所有elses。它会更快,因为在第二阶段运行 if 和 else 的分支预测(这可能会更昂贵)将更准确。

这是一个小程序来演示差异:

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(); }
    }
}

我的机器上的结果是第二个测试,做更多的工作,跑得更快:

幼稚未排序的
时间:1118652 分离/分支预测友好的时间:1005190

这里要带走的重要一点不是您需要返回并查看是否所有循环都可以从分支预测中受益。这只是众多 CPU 功能之一,可以让性能结果让您大吃一惊。这里要带走的重要一点是,要确定代码将如何执行,您实际上需要衡量您的性能。如果你不仔细构建这个,天真的技术仍然可以获胜(我的第一次尝试没有达到我预期的速度)。

此外,我需要指出,在这些情况下并没有太大的区别。这种性能提升是否值得,或者您是否会做得更好,将时间花在其他地方?了解这一点的唯一方法是实际衡量整个应用程序的性能,并找出它真正花费时间的地方。它在哪里真的比它应该的慢?这称为profiling,并且有一些工具可以帮助您准确地做到这一点。

于 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 回答