分析 IL 并了解您的性能





2.00/5 (8投票s)
2004年9月4日
3分钟阅读

50766
避免不必要的变量声明和冗余指令。在一个简单的例子中分析和理解编译器和生成的 IL 指令,从而节省你的性能。
引言
在本文中,我将尝试演示一个我很久以前就有的理论,并且一些和我交谈的人有时会忽略它。“请避免使用过多和不必要的私有变量——当你真的不需要它的时候!” 理论,或者说友好地称为 “不够清晰的代码” 理论。
首先,我不是英语母语者,也不是 .NET 专家,所以,请对我温柔一点 :)
问题所在
想象一下下面的代码
int result = Sum(value1,value2);
if(result>0)
return true;
else
return false;
你们中的大多数人都已经知道我的意思。为什么我们需要 result
。是的,我们为什么需要它?我们不需要。但是有些人坚持这样做。通常,理由是“这样做更清楚,做一个 if
指令,更具可读性”。我知道……你们不是他们中的一员 ;) 。但是对于那些没有阅读这篇文章的人,我将明确解释为什么你应该避免不必要的代码,以及为什么你应该将其变成“return Sum(value1, value0)>0;
”,甚至更好,变成“return value1+value0>0;
”。
当然,本文不是关于如上所示的“确切”代码,而是关于避免不必要的堆栈内存使用,并真正理解原因。
那么真正的原因是什么?
首先,不,抱歉,但它不够清晰!(有人不同意吗?)。我无法理解为什么有人会觉得这更清晰。如果有人没有阅读这篇文章,却这么认为,我建议也把变量名 result
重命名为 resultOfSomeSomeBetweenTwoValuesThatImGonnaSeeIfItIsGreatherThanZero
。这样更清晰。
但说到底,有人知道这会在你的代码中产生什么结果吗?当然,你知道:不必要的堆栈内存消耗和冗余的运行时指令。
请考虑以下控制台应用程序
using System;
namespace TestStack
{
class Class1
{
private static int Sum(int value1, int value2)
{
return value1 + value2;
}
private static bool AnnoyingIsResultGreaterThanZero
(int value1, int value2 )
{
int result = Sum(value1,value2);
if (result>0)
return true;
else
return false;
}
private static bool IsResultGreaterThanZero(int value1, int value2 )
{
return Sum(value1,value2)>0;
}
[STAThread]
static void Main(string[] args)
{
const int ITERATIONS = 2147483647;
Console.WriteLine("Doing {0} iterations...",ITERATIONS);
System.DateTime start ;
bool result;
//Warm up
result = IsResultGreaterThanZero(1,1);
result = AnnoyingIsResultGreaterThanZero(1,1);
result = (Sum(1,1)>0);
result = (1+1>0);
start = System.DateTime.UtcNow;
Console.WriteLine("Calling the " +
"AnnoyingIsResultGreaterThanZero method...");
for (int i=0 ; i<ITERATIONS ; i++)
{
result = AnnoyingIsResultGreaterThanZero(1,1);
}
Console.WriteLine("Total time taken with " +
"AnnoyingIsResultGreaterThanZero method : " +
(System.DateTime.UtcNow - start).ToString());
start = System.DateTime.UtcNow;
Console.WriteLine("Calling the IsResultGreaterThanZero method...");
for (int i=0 ; i<ITERATIONS ; i++)
{
result = IsResultGreaterThanZero(1,1);
}
Console.WriteLine("Total time taken with " +
"IsResultGreaterThanZero method : " +
(System.DateTime.UtcNow - start).ToString());
start = System.DateTime.UtcNow;
Console.WriteLine("Directly calling the Sum method ...");
for (int i=0 ; i<ITERATIONS ; i++)
{
result = (Sum(1,1)>0);
}
Console.WriteLine("Total time taken with Sum method: " +
(System.DateTime.UtcNow - start).ToString());
start = System.DateTime.UtcNow;
Console.WriteLine("Directly making the sum ...");
for (int i=0 ; i<ITERATIONS ; i++)
{
result = (1+1>0);
}
Console.WriteLine("Total time taken with directly sum : " +
(System.DateTime.UtcNow - start).ToString());
Console.ReadLine();
}
}
}
我们这里有一些基本的成员
Sum
返回两个整数的和,AnnoyingIsResultGreaterThanZero
和IsResultGreaterThanZero
返回true
,如果两个值的结果大于零,但AnnoyingIsResultGreaterThanZero
将以冗余的方式执行;Main
启动应用程序并运行一些性能测试,将所花费的时间写入控制台;
AnnoyingIsResultGreaterThanZero
和 IsResultGreaterThanZero
都产生相同的结果。虽然,两者在生成的 IL 中是截然不同的。
让我们看看由 IsResultGreaterThanZero
生成的 IL。要查看 IL,可以使用 SDK 中提供的 Reflector 或 ILDASM。(提示:使用 Reflector,当鼠标悬停在 IL 指令上时,你可以看到有关生成的 IL 的更多详细信息。)
.method private hidebysig static bool
IsResultGreaterThanZero(int32 value1, int32 value2) cil managed
{
// Code Size: 11 byte(s)
.maxstack 2
L_0000: ldarg.0
L_0001: ldarg.1
L_0002: call int32 TestStack.Class1::Sum(int32, int32)
L_0007: ldc.i4.0
L_0008: cgt
L_000a: ret
}
这看起来非常简单的 IL。L_0000
和 L_00001
将参数加载到计算堆栈中。接下来,我们调用 Sum
,加载一个 0(要比较的值),并比较 Sum
返回的值和刚刚推入堆栈的值 (0),然后退出子程序。
当分析由 AnnoyingIsResultGreaterThanZero
生成的 IL 时,我们可以看到以下内容
.method private hidebysig static bool
AnnoyingIsResultGreaterThanZero(int32 value1, int32 value2) cil managed
{
// Code Size: 28 byte(s)
.maxstack 4
.locals (
int32 V_0,
bool V_1)
L_0000: ldarg.0
L_0001: ldarg.1
L_0002: call int32 TestStack.Class1::Sum(int32, int32)
L_0007: stloc.0
L_0008: ldloc.0
L_0009: ldc.i4.0
L_000a: ble.s L_0013
L_000c: ldc.i4.1
L_000d: stloc.1
L_000e: br L_001a
L_0013: ldc.i4.0
L_0014: stloc.1
L_0015: br L_001a
L_001a: ldloc.1
L_001b: ret
}
正如你所看到的,这截然不同。首先,正如你所看到的,使用行 L_000a
和 L_000d
,我们需要更多内存来存储“if
”求值的结果。其次,运行时有 10 条以上的指令需要求值。
运行应用程序并查看结果
我的测试环境是 PIV 3.06Mhz HT,1GB RAM,运行 .NET 1.1。应用程序在发布模式下编译,没有使用 /optimize 开关。结果是
Doing 2147483647 iterations...
Calling the AnnoyingIsResultGreaterThanZero method...
Total time taken with AnnoyingIsResultGreaterThanZero method : 00:00:11.5937500
Calling the IsResultGreaterThanZero method...
Total time taken with IsResultGreaterThanZero method : 00:00:01.5468750
Directly calling the Sum method ...
Total time taken with Sum method: 00:00:01.5312500
Directly making the sum ...
Total time taken with directly sum : 00:00:01.7187500
我进行了 2147483647 次迭代。我知道这很多,但通过这种测试用例,只有通过大量的迭代,我们才能得出一些结论。正如你所看到的,我们谈论的是 10 倍以上。
即使使用 /optimize 开关,我们仍然会得到更慢的结果,你可能会看到
Doing 2147483647 iterations...
Calling the
AnnoyingIsResultGreaterThanZero method...
Total time taken with AnnoyingIsResultGreaterThanZero method : 00:00:02.2343750
Calling the IsResultGreaterThanZero method...
Total time taken with IsResultGreaterThanZero method : 00:00:01.4843750
Directly calling the Sum method ...
Total time taken with Sum method: 00:00:01.5000000
Directly making the sum ...
Total time taken with directly sum : 00:00:01.5000000
结论
对于我来说,这是一个非常冒险的话题,因为,正如我在之前的文章评论中所说,我不是 JIT 方面的专家。在之前的文章版本中,我犯了一些错误,正是因为这一点,也许我现在正在犯更严重的错误 . . . :) 请随时发表评论并帮助就此主题得出结论。