有效地消除.NET表达式树中的常见子表达式

Ani*_*Ani 17 .net c# algorithm optimization expression-trees

我编写了一个DSL和一个从中生成.NET表达式树的编译器.树中的所有表达式都是无副作用的,并且表达式保证是"非语句"表达式(没有本地,循环,块等).(编辑:树可能包括文字,属性访问,标准操作符和函数调用 - 这可能正在做一些奇特的事情,如内部的memoization,但外部副作用是免费的).

现在我想对它执行"Common sub-expression elimination"优化.

例如,给定一个对应于C#lambda的树:

foo =>      (foo.Bar * 5 + foo.Baz * 2 > 7) 
         || (foo.Bar * 5 + foo.Baz * 2 < 3)  
         || (foo.Bar * 5 + 3 == foo.Xyz)
Run Code Online (Sandbox Code Playgroud)

...我想生成树等价的(忽略一些短路语义被忽略的事实):

foo =>
{
     var local1 = foo.Bar * 5;

     // Notice that this local depends on the first one.        
     var local2 = local1 + foo.Baz * 2; 

     // Notice that no unnecessary locals have been generated.
     return local2 > 7 || local2 < 3 || (local1 + 3 == foo.Xyz);
}
Run Code Online (Sandbox Code Playgroud)

我熟悉编写表达式访问者,但这种优化的算法对我来说并不是很明显 - 我当然可以在树中找到"重复",但显然有一些技巧来分析子内部和之间的依赖关系.树可以有效和正确地消除子表达式.

我在谷歌上寻找算法,但它们似乎很难快速实施.而且,它们看起来非常"一般",并不一定考虑到我所考虑的树木的简单性.

Han*_*ant 13

你正在做不必要的工作,常见的子表达式消除是抖动优化器的工作.我们以您的示例为例,查看生成的代码.我这样写的:

    static void Main(string[] args) {
        var lambda = new Func<Foo, bool>(foo => 
               (foo.Bar * 5 + foo.Baz * 2 > 7)
            || (foo.Bar * 5 + foo.Baz * 2 < 3) 
            || (foo.Bar * 5 + 3 == foo.Xyz));
        var obj = new Foo() { Bar = 1, Baz = 2, Xyz = 3 };
        var result = lambda(obj);
        Console.WriteLine(result);
    }
}

class Foo {
    public int Bar { get; internal set; }
    public int Baz { get; internal set; }
    public int Xyz { get; internal set; }
}
Run Code Online (Sandbox Code Playgroud)

x86抖动为lambda表达式生成了此机器代码:

006526B8  push        ebp                          ; prologue
006526B9  mov         ebp,esp  
006526BB  push        esi  
006526BC  mov         esi,dword ptr [ecx+4]        ; esi = foo.Bar
006526BF  lea         esi,[esi+esi*4]              ; esi = 5 * foo.Bar
006526C2  mov         edx,dword ptr [ecx+8]        ; edx = foo.Baz
006526C5  add         edx,edx                      ; edx = 2 * foo.Baz
006526C7  lea         eax,[esi+edx]                ; eax = 5 * foo.Bar + 2 * foo.Baz
006526CA  cmp         eax,7                        ; > 7 test
006526CD  jg          006526E7                     ; > 7 then return true
006526CF  add         edx,esi                      ; HERE!!
006526D1  cmp         edx,3                        ; < 3 test
006526D4  jl          006526E7                     ; < 3 then return true
006526D6  add         esi,3                        ; HERE!!
006526D9  mov         eax,esi  
006526DB  cmp         eax,dword ptr [ecx+0Ch]      ; == foo.Xyz test
006526DE  sete        al                           ; convert to bool
006526E1  movzx       eax,al  
006526E4  pop         esi                          ; epilogue
006526E5  pop         ebp  
006526E6  ret 
006526E7  mov         eax,1  
006526EC  pop         esi  
006526ED  pop         ebp  
006526EE  ret   
Run Code Online (Sandbox Code Playgroud)

foo.Bar * 5用HERE 标记了代码中用于消除子表达式的位置.值得注意的是它没有消除foo.Bar * 5 + foo.Baz * 2子表达式,在地址006526CF再次执行添加.有一个很好的理由,x86抖动没有足够的寄存器来存储中间结果.如果您查看x64抖动生成的机器代码,那么您确实看到它被消除了,r9寄存器会存储它.

这应该给出足够的理由重新考虑你的意图.你正在做的工作不需要做.不仅如此,你是容易产生更坏的代码比抖动会产生,因为你没有估计CPU寄存器预算的奢侈品.

不要这样做.

  • 显然,您也忽略了内联优化,抖动可以*告诉*一个方法没有副作用。在您自己的代码中证明它只会在抖动无法分辨的情况下有用。这需要您处理委托和虚拟方法,祝您好运。 (2认同)
  • @HansPassant 你如何检查为给定的 C# 代码生成什么机器代码?我知道您可以使用 ILSpy 之类的工具检查 MSIL。你如何进一步进入机器代码? (2认同)
  • 使用调试 + Windows + 反汇编。只看Release build,使用Tools + Options,Debugging,General,取消勾选“Suppress JIT optimization”。 (2认同)

Gen*_*ene 7

你是正确的指出这不是一个小问题.

编译器处理它的经典方法是表达式的有向无环图(DAG)表示.DAG的构建方式与抽象语法树相同(可以通过遍历AST构建 - 也许是表达式访问者的工作;我不太了解C#库),除了先前发出的子图的字典维持.在生成具有给定子节点的任何给定节点类型之前,将查询字典以查看是否已存在.仅当此检查失败时才创建新的,然后添加到字典中.

由于现在节点可能来自多个父节点,因此结果是DAG.

然后首先遍历DAG以生成代码.由于公共子表达式现在由单个节点表示,因此该值仅计算一次并存储在temp中,以便稍后在代码生成中使用时发出的其他表达式.如果原始代码包含赋值,则此阶段会变得复杂.由于您的树木是无副作用的,因此DAG应该是解决问题的最直接的方法.

我记得,龙书中对DAG的报道特别好.

正如其他人所指出的那样,如果你的树最终将由现有的编译器编译,那么重做已经存在的东西是徒劳的.

加成

我有一些Java代码来自一个学生项目(我教),所以破解了一个如何工作的小例子.发帖太长了,但请看这里的要点.

在输入上运行它会打印下面的DAG.parens中的数字是(唯一ID,DAG父计数).需要父计数来决定何时计算本地临时变量以及何时仅将表达式用于节点.

Binary OR (27,1)
  lhs:
    Binary OR (19,1)
      lhs:
        Binary GREATER (9,1)
          lhs:
            Binary ADD (7,2)
              lhs:
                Binary MULTIPLY (3,2)
                  lhs:
                    Id 'Bar' (1,1)
                  rhs:
                    Number 5 (2,1)
              rhs:
                Binary MULTIPLY (6,1)
                  lhs:
                    Id 'Baz' (4,1)
                  rhs:
                    Number 2 (5,1)
          rhs:
            Number 7 (8,1)
      rhs:
        Binary LESS (18,1)
          lhs:
            ref to Binary ADD (7,2)
          rhs:
            Number 3 (17,2)
  rhs:
    Binary EQUALS (26,1)
      lhs:
        Binary ADD (24,1)
          lhs:
            ref to Binary MULTIPLY (3,2)
          rhs:
            ref to Number 3 (17,2)
      rhs:
        Id 'Xyz' (25,1)
Run Code Online (Sandbox Code Playgroud)

然后它生成此代码:

t3 = (Bar) * (5);
t7 = (t3) + ((Baz) * (2));
return (((t7) > (7)) || ((t7) < (3))) || (((t3) + (3)) == (Xyz));
Run Code Online (Sandbox Code Playgroud)

您可以看到temp var数字对应于DAG节点.您可以使代码生成器更复杂,以摆脱不必要的括号,但我会留给其他人.