嗯,这是一个有点复杂的答案。
这里涉及两件事。(1) 编译器和 (2) JIT。
编译器
简而言之,编译器只是将您的 C# 代码转换为 IL 代码。在大多数情况下,这是一个非常简单的翻译,.NET 的核心思想之一是每个函数都被编译为一个自主的 IL 代码块。
因此,不要对 C# -> IL 编译器抱有太多期望。
JIT
这……有点复杂。
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 的调用实际上是内联的。这是有道理的。实际上,如果您要对其进行测试,只需将代码放入方法中就与静态调用一样快。
关于 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>
,则不需要这些步骤。因此,即使它看起来较慢,但这与调用本身无关。
你的问题
在这种情况下,将 ListOfDates.Max() 的评估移出 Where 子句是否会有任何(性能)优势,或者 1. 编译器或 2. JIT 会对此进行优化吗?
是的,会有优势。编译器/JIT 不会优化它。
我相信 C# 只会在编译时进行常量折叠,并且可以说 ListOfDates.Max() 在编译时无法知道,除非 ListOfDates 本身以某种方式保持不变。
实际上,如果您将静态调用更改为,n = 2 + Sum(n, 2)
您会注意到汇编器输出将包含一个4
. 这证明了 JIT 优化器确实进行了常量折叠。(如果您了解 SSA 优化器的工作原理,这实际上是非常明显的...... const 折叠和简化被调用了几次)。
函数指针本身没有优化。不过可能会在未来。
也许还有另一个编译器(或 JIT)优化可以确保只评估一次?
至于'另一种编译器',如果你愿意添加'另一种语言',你可以使用C++。在 C++ 中,这些类型的调用有时会被优化掉。
更有趣的是,Clang 基于 LLVM,还有一些用于 LLVM 的 C# 编译器。我相信 Mono 可以选择优化 LLVM,而 CoreCLR 正在研究 LLILC。虽然我没有对此进行测试,但 LLVM 绝对可以进行这些优化。