C#编译器或JIT是否可以优化Lambda表达式中的方法调用?

克里斯多夫

我在讨论之后开始这个问题,该讨论开始于另一个StackOverflow问题在评论中),我很想知道答案。考虑以下表达式:

var objects = RequestObjects.Where(r => r.RequestDate > ListOfDates.Max());

ListOfDates.Max()在这种情况下,将评估移出Where子句是否会有任何(性能)​​优势,还是1.编译器或2. JIT对此进行了优化?

我相信C#只会在编译时进行常量折叠,并且可能会争辩说,除非ListOfDates本身是常量,否则在编译时无法知道ListOfDates.Max()。

也许还有另一种编译器(或JIT)优化可以确保只对它进行一次评估?

攻击

好吧,这是一个复杂的答案。

这里涉及两件事。(1)编译器和(2)JIT。

编译器

简而言之,编译器只是将您的C#代码转换为IL代码。在大多数情况下,这是一个非常琐碎的翻译,.NET的核心思想之一是每个函数都被编译为IL代码的自治块。

因此,不要期望C#-> IL编译器有太多期望。

准时制

那...有点复杂。

JIT编译器基本上将您的IL代码转换为汇编器。JIT编译器还包含一个基于SSA的优化器。但是,这是有时间限制的,因为我们不想等待太长时间才能开始运行代码。基本上,这意味着JIT编译器不会执行所有会使代码快速运行的超酷工作,仅仅是因为这会花费太多时间。

我们当然可以对其进行测试:)确保VS在您运行时进行优化(选项->调试器->取消选中抑制[...]和我的代码),在x64发行模式下进行编译,放置一个断点并查看切换到汇编器视图时会发生什么。

但是,嘿,只有理论才有意思。让我们对其进行测试。:)

static bool Foo(Func<int, int, int> foo, int a, int b)
{
    return foo(a, b) > 0;  // put breakpoint on this line.
}

public static void Test()
{
    int n = 2;
    int m = 2;
    if (Foo((a, b) => a + b, n, m)) 
    {
        Console.WriteLine("yeah");
    }
}

您应该注意的第一件事是断点被击中。这已经表明该方法没有内联;如果是这样,您根本不会达到断点。

接下来,如果您观看汇编程序的输出,您会注意到使用地址的“调用”指令。这是您的功能。仔细检查后,您会发现它正在呼叫委托。

现在,基本上这意味着该调用未进行内联,因此未进行优化以匹配本地(方法)上下文。换句话说,不使用委托并将东西放入方法中可能比使用委托更快。

在另一方面,呼叫非常有效的。基本上,函数指针只是简单地传递和调用。没有vtable查找,只有一个简单的调用。这意味着它可能胜过调用成员(例如IL callvirt)。不过,静态调用(IL call)应该更快,因为它们是可预测的编译时间。再次,让我们测试一下,对吧?

public static void Test()
{
    ISummer summer = new Summer();
    Stopwatch sw = Stopwatch.StartNew();
    int n = 0;
    for (int i = 0; i < 1000000000; ++i)
    {
        n = summer.Sum(n, i);
    }
    Console.WriteLine("Vtable call took {0} ms, result = {1}", sw.ElapsedMilliseconds, n);

    Summer summer2 = new Summer();
    sw = Stopwatch.StartNew();
    n = 0;
    for (int i = 0; i < 1000000000; ++i)
    {
        n = summer.Sum(n, i);
    }
    Console.WriteLine("Non-vtable call took {0} ms, result = {1}", sw.ElapsedMilliseconds, n);

    Func<int, int, int> sumdel = (a, b) => a + b;
    sw = Stopwatch.StartNew();
    n = 0;
    for (int i = 0; i < 1000000000; ++i)
    {
        n = sumdel(n, i);
    }
    Console.WriteLine("Delegate call took {0} ms, result = {1}", sw.ElapsedMilliseconds, n);

    sw = Stopwatch.StartNew();
    n = 0;
    for (int i = 0; i < 1000000000; ++i)
    {
        n = Sum(n, i);
    }
    Console.WriteLine("Static call took {0} ms, result = {1}", sw.ElapsedMilliseconds, n);
}

结果:

Vtable call took 2714 ms, result = -1243309312
Non-vtable call took 2558 ms, result = -1243309312
Delegate call took 1904 ms, result = -1243309312
Static call took 324 ms, result = -1243309312

有趣的是,实际上是最新的测试结果。请记住,静态调用(IL call)是完全确定的。这意味着针对编译器进行优化是相对简单的事情。如果检查汇编程序输出,则会发现对Sum的调用实际上是内联的。这是有道理的。实际上,如果您要测试它,只需将代码放入方法中就和静态调用一样快。

关于平等的一句话

如果您衡量哈希表的性能,我的解释似乎有些混乱。看起来好像IEquatable<T>使事情进展得更快。

好吧,这是真的。:-)哈希容器用于IEquatable<T>调用Equals现在,众所周知,所有对象都实现了Equals(object o)因此,容器可以调用Equals(object)Equals(T)通话本身的性能是相同的。

但是,如果您还实现IEquatable<T>,则实现通常如下所示:

bool Equals(object o)
{
    var obj = o as MyType;
    return obj != null && this.Equals(obj);
}

此外,如果MyType是struct,则运行时还需要应用装箱和拆箱。如果只调用IEquatable<T>,则无需执行这些步骤。因此,即使它看起来较慢,也与呼叫本身无关。

你的问题

在这种情况下,将ListOfDates.Max()的评估移出Where子句是否会有任何(性能)​​优势,还是1.编译器或2. JIT对此进行了优化?

是的,这将是一个优势。编译器/ JIT不会对其进行优化。

我相信C#只会在编译时进行常量折叠,并且可能会争辩说,除非ListOfDates本身是常量,否则在编译时无法知道ListOfDates.Max()。

实际上,如果您将静态调用更改为n = 2 + Sum(n, 2),则会注意到汇编器输出将包含4这证明了JIT优化器确实进行了持续折叠。(实际上,如果您知道SSA优化器的工作原理,那显然是很明显的。const折叠和简化被称为几次)。

函数指针本身未优化。不过可能会在将来。

也许还有另一种编译器(或JIT)优化可以确保只对它进行一次评估?

至于“另一种编译器”,如果您愿意添加“另一种语言”,则可以使用C ++。在C ++中,有时会优化掉这类调用。

更有趣的是,Clang基于LLVM,并且还有一些用于LLVM的C#编译器。我相信Mono可以选择针对LLVM进行优化,而CoreCLR正在研究LLILC。尽管我还没有对此进行测试,但是LLVM绝对可以进行此类优化。

本文收集自互联网,转载请注明来源。

如有侵权,请联系[email protected] 删除。

编辑于
0

我来说两句

0条评论
登录后参与评论

相关文章

来自分类Dev

C#编译器会将lambda表达式视为公共方法还是私有方法?

来自分类Dev

在C#编译器和虚拟机中如何处理lambda表达式?

来自分类Dev

编译Lambda表达式时SBCL中的微妙编译器警告

来自分类Dev

如果从动态分配的对象中调用方法,方法是否可以以某种方式内联(编译器优化)

来自分类Dev

晦涩的编译器的lambda表达式翻译

来自分类Dev

Lambda表达式的编译器错误

来自分类Dev

我怎么知道C ++编译器在编译时是否对表达式求值?

来自分类Dev

C#编译器用来解析lambda表达式中的类型的规则是什么?

来自分类Dev

编译器如何从LAMBDA表达式中推断委托类型?

来自分类Dev

CSHTML文件中Lambda表达式的运行时编译器问题

来自分类Dev

Linux中g ++编译器下的lambda表达式

来自分类Dev

编译器如何从LAMBDA表达式中推断委托类型?

来自分类Dev

C ++ lambda表达式-编译器如何解释它们?

来自分类Dev

编译器优化是否可以消除在for循环的条件中反复调用的函数?

来自分类Dev

编译器可以优化离开方法的调用吗?

来自分类Dev

是否可以将Lambda表达式存储在数组C#中

来自分类Dev

C编译器优化器能否违反逻辑AND表达式中操作数的短路并重新排序内存访问?

来自分类Dev

编译器和高端处理器流水线中的布尔表达式优化

来自分类Dev

是什么使C#编译器输出表达式树而不是代码?

来自分类Dev

C中的编译器错误,错误:预期表达式

来自分类Dev

Java和C ++中for循环的边界检查的编译器/ JIT优化

来自分类Dev

Typescript编译器无法调用类型缺少调用签名的表达式

来自分类Dev

C#编译器优化

来自分类Dev

Lambda语句仅出现Roslyn编译器错误:无法将表达式转换为表达式树

来自分类Dev

是否可以为Haskell编写即时(JIT)编译器?

来自分类Dev

Hotspot JIT编译器是否可以重制任何指令?

来自分类Dev

编译器是否必须评估表达式是否取决于模板参数?

来自分类Dev

编译器是否会将此表达式优化为一个临时常量,而不是每次迭代都将其解析?

来自分类Dev

Intellij 14,Lambda表达式错误,尽管没有编译器错误也显示错误

Related 相关文章

  1. 1

    C#编译器会将lambda表达式视为公共方法还是私有方法?

  2. 2

    在C#编译器和虚拟机中如何处理lambda表达式?

  3. 3

    编译Lambda表达式时SBCL中的微妙编译器警告

  4. 4

    如果从动态分配的对象中调用方法,方法是否可以以某种方式内联(编译器优化)

  5. 5

    晦涩的编译器的lambda表达式翻译

  6. 6

    Lambda表达式的编译器错误

  7. 7

    我怎么知道C ++编译器在编译时是否对表达式求值?

  8. 8

    C#编译器用来解析lambda表达式中的类型的规则是什么?

  9. 9

    编译器如何从LAMBDA表达式中推断委托类型?

  10. 10

    CSHTML文件中Lambda表达式的运行时编译器问题

  11. 11

    Linux中g ++编译器下的lambda表达式

  12. 12

    编译器如何从LAMBDA表达式中推断委托类型?

  13. 13

    C ++ lambda表达式-编译器如何解释它们?

  14. 14

    编译器优化是否可以消除在for循环的条件中反复调用的函数?

  15. 15

    编译器可以优化离开方法的调用吗?

  16. 16

    是否可以将Lambda表达式存储在数组C#中

  17. 17

    C编译器优化器能否违反逻辑AND表达式中操作数的短路并重新排序内存访问?

  18. 18

    编译器和高端处理器流水线中的布尔表达式优化

  19. 19

    是什么使C#编译器输出表达式树而不是代码?

  20. 20

    C中的编译器错误,错误:预期表达式

  21. 21

    Java和C ++中for循环的边界检查的编译器/ JIT优化

  22. 22

    Typescript编译器无法调用类型缺少调用签名的表达式

  23. 23

    C#编译器优化

  24. 24

    Lambda语句仅出现Roslyn编译器错误:无法将表达式转换为表达式树

  25. 25

    是否可以为Haskell编写即时(JIT)编译器?

  26. 26

    Hotspot JIT编译器是否可以重制任何指令?

  27. 27

    编译器是否必须评估表达式是否取决于模板参数?

  28. 28

    编译器是否会将此表达式优化为一个临时常量,而不是每次迭代都将其解析?

  29. 29

    Intellij 14,Lambda表达式错误,尽管没有编译器错误也显示错误

热门标签

归档