.NET编译器中的分支消除与Jit'ter相比

时间:2013-09-13 10:20:40

标签: c# .net optimization jit

对于我的一个项目,我使用了很多分支。想想:

if (foo) { do something } 
else if (bar) { do something } 
else if (bar2) { do something } 
... and so on

if (foo) { do something } 
if (bar) { do something } 
if (bar2) { do something } 
... and so on

我一直在想的是,是否有必要进行子表达式和/或逻辑消除来加速它。为了完整起见,您可以假设所有这些都在一个函数中。比如说,如果foobar有一个共同的子表达式,你可以这样写:

if (commonSubExpr) 
{
    if (foo without commonSubExpr) { do something }
    if (bar without commonSubExpr) { do something } 
}
if (bar2) { do something } 
... and so on

同样,您可以应用许多简单的布尔逻辑规则来优化规则。

我的问题是:这样做有意义吗?或者我可以期待JIT'ter能够解决这个问题吗?

(根据excellent article by Eric Lippert,除了常量折叠之外,编译器没有对此进行优化 - 我认为仍然是这种情况。)

更新+ 1

好吧,我不应该问这是否有意义,因为现在我找到了很好的人试图向我解释什么是过早的优化,这不是我追求的......我的错误。假设我知道过度设计,过早优化等等 - 请 - 这正是我在这里试图避免的。

所以尝试2 ...我想知道如何工作我对编译器/ JIT'ter 的期望。

我也注意到一些上下文可能对此有所帮助,所以这里有一点关于应用程序:

在这种情况下,应用程序是一种特定于域的语言,使用Reflection.Emit到IL在运行时编译。我不能使用现有语言或现有编译器是有充分理由的。性能至关重要,在编译之后会执行大量操作(这就是为什么它首先编译为IL而不是简单地解释代码)。

我问这个的原因是因为我想知道我应该在编译器中设计优化器的范围。如果JIT'ter负责消除子表达式,我将设计优化器只做基本的事情,如常量折叠,如果.NET期望在编译器中发生这种情况,我将在编译器中进行设计。根据我的预期,优化器将采用完全不同的设计。由于分支机构可能是最重要的性能排放者,并且因为实施对我的软件设计有很大的影响,所以我特意决定询问这个问题。

我知道在实现编译器之前无法测试这个 - 这是相当多的工作 - 所以我想在开始实现之前直接了解我的基础知识。我不知道如何测试这个的原因是因为我不知道在什么情况下JIT'ter优化了哪些代码;我希望.NET运行时中的某些触发器会导致某些优化(使测试结果不可靠)......如果你知道解决方法,请告诉我。

表达式foobar等可以是您通常在代码中看到的任何形式,但您可以假设它是单个函数。因此,它可以是if (StartDate < EndDate)形式,但不能像if (method1() < method2())那样。解释一下:在后一种情况下,编译器不能简单地假设方法的返回值(在优化它之前需要获得有关返回值的信息),因此消除子表达式并非易事。 / p>

因此,作为子表达式消除的一个例子:

if (int1 < int2 && int1 < int3) {
    //...
}
else if (int1 < int2 && int1 < int3) {
    //...
}

可以改写为:

if (int1 < int2)
{
    if (int1 < int3) {
        //...
    }
    else if (int1 < int3) {
        //...
    }
}

总而言之:我想知道的是,这些类型的子表达式消除优化是否有意义 - 或者它们是否由JIT'ter处理。

2 个答案:

答案 0 :(得分:4)

  

因此,它可以是(StartDate&lt; EndDate)

的形式

不,它不能。您的编译器需要生成对DateTime.op_LessThan()方法的调用。生成对方法的调用的麻烦在于,您无法100%确定该方法不会产生可观察到的副作用。 DateTime.op_LessThan没有,但这不是你的编译器可以自己找到的东西。您必须在编译器中对该规则进行硬编码。

然而,抖动可以 知道该方法的代码是什么样的。它非常小,它会将方法内联到单个CPU指令。它平均在 less 中执行,而不是一个CPU周期。内置于处理器中的分支预测逻辑确保分支不可能使管道停滞。

很难让编译器中的优化器得到回报。对于没有副作用的非常简单的代码,它只能消除常见的子表达式,但这种代码已经运行得非常快。 C#编译器是一个很好的模型,它不会优化并使作业失去作用。抖动执行的优化在this answer中描述。是的,常见的子表达式消除是一种它知道如何执行的优化。

无论是否应用优化都是不可预测的,这取决于它需要在方法中生成的其他代码。我没有检查过这个具体案例,我很怀疑它是否会因为分支而产生。如果您还想为&amp;&amp; amp;提供短路评估,那么除了眼睛之外还有更多的东西。和||运营商。您可以找到的唯一方法是查看实际生成的机器代码。使用平台目标设置选择要验证的抖动。构建测试代码的Release配置。和工具+选项,调试,常规,取消勾选“抑制JIT优化”选项。并使用断点和Debug + Windows + Disassembly查看机器代码。注意伪测试代码,如果抖动可以优化太多,它通常会以不切实际的速度运行。并注意燃烧方式太多时间:)

答案 1 :(得分:3)

在评论中,许多人已经注意到,你似乎走错了方向。

如果你真的打算大力发展它,我会更担心这段代码的可管理性和清晰度。

如果您的foobarbar2commonSubExpr 只是布尔,这可能是您应用中最快的部分,无论方法如何1或2。

如果foobarbar2commonSubExpr 评估函数可能很昂贵,那么您应该自己优化函数,也许如果可能,缓存结果。但在这种情况下,它与if/else子句的组成和结构无关。

<强>更新

如果你有这样的代码:

class Program
{
    static void Main(string[] args)
    {
        var int1 = 1;
        var int2 = 2;
        var int3 = 3;


        if (int1 < int2 && int1 < int3) {
            Console.WriteLine("Branch 1");
        }
        else if (int1 < int2 && int1 < int3) {
            Console.WriteLine("Branch 2");
        }
    }
}

优化的MSIL将如下所示:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       44 (0x2c)
  .maxstack  2
  .locals init ([0] int32 int1,
           [1] int32 int2,
           [2] int32 int3)
  IL_0000:  ldc.i4.1
  IL_0001:  stloc.0
  IL_0002:  ldc.i4.2
  IL_0003:  stloc.1
  IL_0004:  ldc.i4.3
  IL_0005:  stloc.2
  IL_0006:  ldloc.0
  IL_0007:  ldloc.1
  IL_0008:  bge.s      IL_0019
  IL_000a:  ldloc.0
  IL_000b:  ldloc.2
  IL_000c:  bge.s      IL_0019
  IL_000e:  ldstr      "Branch 1"
  IL_0013:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0018:  ret
  IL_0019:  ldloc.0
  IL_001a:  ldloc.1
  IL_001b:  bge.s      IL_002b
  IL_001d:  ldloc.0
  IL_001e:  ldloc.2
  IL_001f:  bge.s      IL_002b
  IL_0021:  ldstr      "Branch 2"
  IL_0026:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_002b:  ret
} // end of method Program::Main

然而,第二个例子:

static void Main(string[] args)
{
    var int1 = 1;
    var int2 = 2;
    var int3 = 3;

    if (int1 < int2)
    {
        if (int1 < int3)
        {
            Console.WriteLine("Branch 1");
        }
        else if (int1 < int3)
        {
            Console.WriteLine("Branch 2");
        }
    }
}

将产生3行代码:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       40 (0x28)
  .maxstack  2
  .locals init ([0] int32 int1,
           [1] int32 int2,
           [2] int32 int3)
  IL_0000:  ldc.i4.1
  IL_0001:  stloc.0
  IL_0002:  ldc.i4.2
  IL_0003:  stloc.1
  IL_0004:  ldc.i4.3
  IL_0005:  stloc.2
  IL_0006:  ldloc.0
  IL_0007:  ldloc.1
  IL_0008:  bge.s      IL_0027
  IL_000a:  ldloc.0
  IL_000b:  ldloc.2
  IL_000c:  bge.s      IL_0019
  IL_000e:  ldstr      "Branch 1"
  IL_0013:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0018:  ret
  IL_0019:  ldloc.0
  IL_001a:  ldloc.2
  IL_001b:  bge.s      IL_0027
  IL_001d:  ldstr      "Branch 2"
  IL_0022:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0027:  ret
} // end of method Program::Main

粗略地说,差异是 3条指令

  IL_001a:  ldloc.1
  IL_001b:  bge.s      IL_002b
  IL_001d:  ldloc.0

根据其他来源(阅读here),JIT不进行这种类型的优化,但即使这样,也无法在任何程度上进行衡量。