奇怪,难道K. Scott Allen错了[事实证明是自己错了,附Scott的Mail]
今天看到MSDN Magzine2005年第一期(2004年还未结束,2005年一月刊就出来了:))K. Scott Allen撰写的一篇文章Get a Charge From Statics with Seven Essential Programming Tips,其中提到静态字段初始化对性能的影响。静态字段初始化有两种方式:一是直接赋值,他称为得隐式调用类型构造函数;一是在构造函数内赋值,即所谓的显式调用构造函数。
例如这样两个类:
class ExplicitConstructor
{
private static string message;
static ExplicitConstructor()
{
message = "Hello World";
}
public static string Message
{
get { return message; }
}
}
class ImplicitConstructor
{
private static string message = "Hello World";
public static string Message
{
get { return message; }
}
}
Scott通过ILDASM分析了两种方式的IL代码,如下:
.class private auto ansi ExplicitConstructor
extends [mscorlib]System.Object
{
} // end of class ExplicitConstructor
.class private auto ansi beforefieldinit ImplicitConstructor
extends [mscorlib]System.Object
{
} // end of class ImplicitConstructor
他讲到在ImplicitConstructor类中包含一个附加的元数据标志beforefieldinit。该标志决定了运行时初始化静态字段的时刻。
Notice that ImplicitConstructor has an additional metadata flag named beforefieldinit. This flag allows the runtime to execute the type constructor method at any time it chooses, as long as the method executes before the first access to a static field for the type. In other words, beforefieldinit gives the runtime a license to perform aggressive optimizations. Without beforefieldinit, the runtime must run the type constructor at a precise time—just before the first access to static or instance fields and methods of the type. When an explicit type constructor is present, the compilers will not mark the type with beforefieldinit, and the precise timing restrictions lead to the performance drop hinted at by FxCop.
该标志允许运行时合适地选择执行类型构造器方法的时机,只有当初次访问类型的其中一个静态字段之前,该方法才执行。换句话说,beforefieldinit标志给与了运行时最大限度的性能优化机制。如果没有该标志,运行时就必须在特定的时间内执行类型构造器——即,恰好在初次访问类型的静态或实例字段以及方法之前。当显式的类型构造器存在的时候,编译器不会为类型标记beforefieldinit,这种规定时间的限制就会导致性能的降低,就像FxCop所提示的那样。
文中列出了一段测试代码:
Module Module1
Sub Main()
TestSharedPropertyAccess()
Console.ReadLine()
End Sub
Sub TestSharedPropertyAccess()
Dim begin As DateTime = DateTime.Now
For i as Integer = 0 To iterations
Dim s As String = ExplicitConstructor.Message
Next
WriteResult(DateTime.Now.Subtract(begin), _
"TestStaticPropertyAccess : ExplicitConstructor")
begin = DateTime.Now
For i As Integer = 0 To iterations
Dim s As String = ImplicitConstructor.Message
Next
WriteResult(DateTime.Now.Subtract(begin), _
"TestStaticPropertyAccess : ImplicitConstructor")
End Sub
Sub WriteResult(ByVal span As TimeSpan, ByVal message As String)
Console.WriteLine("{0} took {1} ms", _
message, span.TotalMilliseconds)
End Sub
Dim iterations As Integer = Int32.MaxValue - 1
End Module
这段代码是用VB.Net写的,使用的类即是我前面列出来的两个类:ExplicitConstructor和ImplicitConstructor。该示例用C#来表示如下:
using System;
namespace Exceptions
{
class Class1
{
private static int max = Int32.MaxValue - 1;
[STAThread]
static void Main(string[] args)
{
TestStaticPropertyAccess();
Console.ReadLine();
}
static void TestStaticPropertyAccess()
{
DateTime begin = DateTime.Now;
for (int i=0;i<max;i++)
{
string message = ExplicitConstructor.Message;
}
PrintResult(DateTime.Now.Subtract(begin),"ExplicitConstructor");
begin = DateTime.Now;
for (int i=0;i<max;i++)
{
string message = ImplicitConstructor.Message;
}
PrintResult(DateTime.Now.Subtract(begin),"ImplicitConstructor");
}
static void PrintResult(TimeSpan span,string result)
{
Console.WriteLine("{0} took {1} ms.",result,span.Milliseconds);
}
}
按照Scott的解释,ExplicitConstructor类由于没有beforefieldinit标志,因此在循环体内部执行的时候,每次都要初始化类型;而ImplicitConstructor类则相反,它将运行时对类型初始化的检测提升到了循环体外部。Scott运行后,发现结果令人吃惊:ExplicitConstructor类的操作比ImplicitConstructor类要慢8倍。差距是如此的明显,确实如Scott所言,不需要用精确的计时器来计时了。Soctt的机器配置是Pentium 4,2.8GHz。
You don't need a high-resolution timer to see the difference in speed. On my 2.8GHz Pentium 4, the first loop (with ExplicitConstructor) executes approximately eight times slower than the second loop (with ImplicitConstructor). The checks that the runtime performs in order to run the type initializer at a precise time adds overhead inside of the loop, while beforefieldinit relaxes the rules and allows the runtime to hoist these checks outside of the loop.
然而我在我的机器上运行同样的代码,却发现结果大相径庭。反而是ExplicitConstructor类的操作比ImplicitConstructor类要快,当然我的机器不过是赛扬的cpu而已。但这种性能比,与机器应该不相关啊。运行结果如图:
很明显,ExplicitConstructor类的操作耗时562ms,而ImplicitConstructor类耗时984ms。我要说明的是,我在运行代码的同时,没有做其他操作和任务。
我真的很奇怪!
最后有了结果,是因为Debug和Release的区别。刘敏(Rustle Liu)在评论中也指出了,而Scott本人也回答了我的疑问。他耐心的解释了我这个无知的问题。很佩服Scott这种严谨认真且耐心谦和的态度!
附Scott的两封mail,仔细解释了原因:
The first mail:
Hi wayfarer:
I'm glad you enjoyed the article. Thank you for the kind note.
A couple thing to check:
1. Make sure you compile the program in release mode
2. Make sure to run the program outside of the IDE.
Those two steps make sure the program is in release and has full optimizations.
If this doesn't produce results that match my article, could you share the code for your ExplicitConstructor and ImplicitConstrutor classes? I wrote up a quick test here and saw similar results to what I have in the article:
Class ExplicitConstructor
Private Shared _message As String
Shared Sub New()
_message = "Hello"
End Sub
Public Shared ReadOnly Property Message() As String
Get
Message = _message
End Get
End Property
End Class
Class ImplicitConstructor
Private Shared _message As String = "hello"
Public Shared ReadOnly Property Message() As String
Get
Message = _message
End Get
End Property
End Class
Let me know how it works out!
The second mail:
I’m glad you can see a perf difference now. I imagine it is very sensitive to the platform. Those locks might be taking more time on my hyper-threaded processor. There can be huge differences between release and debug modes. The JIT won’t perform any optimizations in debug mode.