草庐IT

c# - C# 编译器或 JIT 能否优化掉 lambda 表达式中的方法调用?

coder 2024-05-21 原文

我是在关于另一个 StackOverflow 问题的讨论( in comments )之后开始这个问题的,我很想知道答案。考虑以下表达式:

var objects = RequestObjects.Where(r => r.RequestDate > ListOfDates.Max());
移动ListOfDates.Max()的评价有没有(性能)优势?在这种情况下,在 Where 子句之外,还是会 1. 编译器或 2. JIT 优化它?
我相信 C# 只会在编译时进行常量折叠,并且可以说 ListOfDates.Max() 在编译时无法知道,除非 ListOfDates 本身在某种程度上是常量。
也许还有另一个编译器(或 JIT)优化可以确保只计算一次?

最佳答案

嗯,这是一个有点复杂的答案。

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

编译器

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

所以,不要对 C# -> IL 编译器期望过高。

JIT

那……有点复杂。

JIT 编译器基本上将您的 IL 代码转换为汇编程序。 JIT 编译器还包含一个基于 SSA 的优化器。但是,有一个时间限制,因为我们不想在代码开始运行之前等待太久。基本上这意味着 JIT 编译器不会做所有能让你的代码运行得非常快的 super 酷的事情,仅仅是因为这会花费太多时间。

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

但是,嘿,只有理论有什么乐趣?让我们来测试一下。 :)

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

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

接下来,如果您观察汇编器输出,您会注意到使用地址的“调用”指令。这是你的功能。仔细检查后,您会注意到它正在调用委托(delegate)。

现在,基本上这意味着调用不是内联的,因此没有优化以匹配本地(方法)上下文。换句话说,不使用委托(delegate)并将内容放入您的方法中可能比使用委托(delegate)更快。

另一方面,调用非常有效。基本上,函数指针只是简单地传递和调用。没有 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 的调用实际上是内联的。这是有道理的。实际上,如果您要测试它,只需将代码放入方法中与静态调用一样快。

关于 Equals 的小评论

如果您测量哈希表的性能,我的解释似乎有些可疑。它看起来好像 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是一个结构体,运行时也需要应用装箱和拆箱。如果它只是拨打 IEquatable<T> ,这些步骤都不是必需的。因此,即使它看起来更慢,但这与调用本身无关。

您的问题

Will there be any (performance) advantage of moving the evaluation of ListOfDates.Max() out of the Where clause in this case, or will 1. the compiler or 2. JIT optimize this away?



是的,会有优势。编译器/JIT 不会优化它。

I believe C# will only do constant folding at compile time, and it could be argued that ListOfDates.Max() can not be known at compile time unless ListOfDates itself is somehow constant.



实际上,如果您将静态调用更改为 n = 2 + Sum(n, 2)您会注意到汇编器输出将包含 4 .这证明 JIT 优化器确实进行了常量折叠。 (实际上,如果您了解 SSA 优化器的工作原理,这将非常明显…… const 折叠和简化被调用了几次)。

函数指针本身没有优化。不过也可能是在 future 。

Perhaps there is another compiler (or JIT) optimization that makes sure that this is only evaluated once?



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

更有趣的是,Clang 基于 LLVM,并且还有一些用于 LLVM 的 C# 编译器。我相信 Mono 可以选择优化到 LLVM,而 CoreCLR 正在研究 LLILC。虽然我还没有测试过,但 LLVM 绝对可以做这些类型的优化。

关于c# - C# 编译器或 JIT 能否优化掉 lambda 表达式中的方法调用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36464287/

有关c# - C# 编译器或 JIT 能否优化掉 lambda 表达式中的方法调用?的更多相关文章

  1. c# - 如何在 ruby​​ 中调用 C# dll? - 2

    如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

  2. C# 到 Ruby sha1 base64 编码 - 2

    我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

  3. ruby - Sinatra set cache_control to static files in public folder编译错误 - 2

    我不知道为什么,但是当我设置这个设置时它无法编译设置:static_cache_control,[:public,:max_age=>300]这是我得到的syntaxerror,unexpectedtASSOC,expecting']'(SyntaxError)set:static_cache_control,[:public,:max_age=>300]^我只想将“过期”header设置为css、javaascript和图像文件。谢谢。 最佳答案 我猜您使用的是Ruby1.8.7。Sinatra文档中显示的语法似乎是在Ruby1.

  4. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  5. 安卓apk修改(Android反编译apk) - 2

    最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路

  6. ruby - 了解在 Ruby 中与 lambda 一起使用的 inject 行为 - 2

    我经常将预配置的lambda插入可枚举的方法中,例如“map”、“select”等。但是“注入(inject)”的行为似乎有所不同。例如与mult4=lambda{|item|item*4}然后(5..10).map&mult4给我[20,24,28,32,36,40]但是,如果我制作一个2参数lambda用于像这样的注入(inject),multL=lambda{|product,n|product*n}我想说(5..10).inject(2)&multL因为“inject”有一个可选的单个初始值参数,但这给了我......irb(main):027:0>(5..10).inject

  7. ruby - 当你有一个没有参数的 case 语句并且 when 子句是 lambda 时会发生什么? - 2

    这段代码没有像我预期的那样执行:casewhen->{false}then"why?"else"ThisiswhatIexpect"end#=>"why?"这也不是casewhen->(x){false}then"why?"else"ThisiswhatIexpect"end#=>"why?"第一个then子句在两种情况下都被执行,这意味着我提供给when子句的lambda没有被调用。我知道无论when子句的主题是什么,都应该调用大小写相等运算符===。我想知道当没有为case提供参数时,===的另一边会发生什么。我在想它可能是nil,但它不可能是:->{false}===nil#=>

  8. ruby - 如何将 lambda 传递给 Hash.each? - 2

    如何将lambda传递给hash.each,以便我可以重复使用一些代码?>h={a:'b'}>h.eachdo|key,value|end=>{:a=>"b"}>test=lambdado|key,value|puts"#{key}=#{value}"end>test.call('a','b')a=b>h.each&testArgumentError:wrongnumberofarguments(1for2)from(irb):1:in`blockinirb_binding'from(irb):5:in`each'from(irb):5from/Users/jstillwell/.rv

  9. .net - 是否有 Ruby .NET 编译器? - 2

    是否有适用于Ruby语言的.NETFramework编译器?我听说过DLR(动态语言运行时),这是否将使Ruby能够用于.NET开发? 最佳答案 IronRuby是Microsoft支持的项目,建立在动态语言运行时之上。 关于.net-是否有Ruby.NET编译器?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/199638/

  10. c# - C# 中的 Flatten Ruby 方法 - 2

    我如何做Ruby方法"Flatten"RubyMethod在C#中。此方法将锯齿状数组展平为一维数组。例如:s=[1,2,3]#=>[1,2,3]t=[4,5,6,[7,8]]#=>[4,5,6,[7,8]]a=[s,t,9,10]#=>[[1,2,3],[4,5,6,[7,8]],9,10]a.flatten#=>[1,2,3,4,5,6,7,8,9,10 最佳答案 递归解决方案:IEnumerableFlatten(IEnumerablearray){foreach(variteminarray){if(itemisIEnume

随机推荐