Chr*_*heD 15 c# optimization lambda jit constant-expression
我在另一个StackOverflow问题开始(在评论中)的讨论后开始这个问题,我很想知道答案.考虑以下表达式:
var objects = RequestObjects.Where(r => r.RequestDate > ListOfDates.Max());
Run Code Online (Sandbox Code Playgroud)
ListOfDates.Max()在这种情况下,是否有任何(性能)优势可以移出Where子句的评估,还是1.编译器或2. JIT优化它?
我相信C#只会在编译时进行常量折叠,并且可以认为ListOfDates.Max()在编译时是不可知的,除非ListOfDates本身在某种程度上是常量.
也许还有另一个编译器(或JIT)优化,确保只评估一次?
atl*_*ste 16
嗯,这是一个复杂的答案.
这里涉及两件事.(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");
}
}
Run Code Online (Sandbox Code Playgroud)
你应该注意的第一件事是断点被击中.这已经告诉该方法没有内联; 如果是的话,你根本就不会遇到断点.
接下来,如果您观察汇编程序输出,您会注意到使用地址的"调用"指令.这是你的功能.仔细观察,你会注意到它正在呼叫代表.
现在,基本上这意味着调用没有内联,因此没有进行优化以匹配本地(方法)上下文.换句话说,不使用委托并在您的方法中放置东西可能比使用委托更快.
在另一方面,呼叫是非常有效的.基本上,函数指针只是传递和调用.没有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);
}
Run Code Online (Sandbox Code Playgroud)
结果:
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
Run Code Online (Sandbox Code Playgroud)
这里有趣的事实上是最新的测试结果.请记住,静态调用(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);
}
Run Code Online (Sandbox Code Playgroud)
此外,如果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绝对可以进行这些优化.
| 归档时间: |
|
| 查看次数: |
1359 次 |
| 最近记录: |