Contents

C#的程序一个可能遇到的性能问题就是启动时间可能会太长,这是因为C#编译后会成为IL,在第一次运行时会由JIT即时编译编译为机器代码,然后运行。.NET Just in Time Compilation and Warming up Your System介绍了作者对于JIT的一些经验,其中谈到了如何通过Windbg来看JIT究竟是什么时候发生的,很有意思。下面我们也就拿段小程序用Windbg看看。

假设有如下一个简单的程序,编译成Demo.exe。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace Demo
{
class MyClass
{
public void a()
{
int i = 0;
while (true)
{
b(i++);
}
}

public void b(int i)
{
Console.WriteLine(i);
}
}

class Program
{

static void Main(string[] args)
{
MyClass mc = new MyClass();
mc.a();
}
}
}

然后我们打开Windbg,按快捷键Ctrl+E,或者点击File->Open Executable,然后选择刚才编译好的Demo.exe。这样就用Windbg把Demo.exe运行起来了。

接着在Windbg里运行下面的命令来让Windbg在Demo.exe加载clr时停下来。

1
sxe ld:clr

然后输入g命令,很快Windbg就会停下来,这时敲k命令可以看到如下的输出,正在加载clr。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0:000> k
ChildEBP RetAddr
0018f6fc 7796bf70 ntdll!ZwMapViewOfSection+0x12
0018f750 7796c5fb ntdll!LdrpMapViewOfSection+0xc7
0018f844 7796c42c ntdll!LdrpFindOrMapDll+0x333
0018f9c4 7796c558 ntdll!LdrpLoadDll+0x2b2
0018f9fc 755b2ca2 ntdll!LdrLoadDll+0xaa
0018fa38 74045ba3 KERNELBASE!LoadLibraryExW+0x1f1
0018fb9c 7404b32c mscoreei!RuntimeDesc::LoadLibrary+0xcf
0018fbd8 740468f7 mscoreei!RuntimeDesc::LoadMainRuntimeModuleHelper+0x96
0018fc1c 7404a70a mscoreei!RuntimeDesc::LoadMainRuntimeModule+0x1b8
0018fc74 7404a877 mscoreei!RuntimeDesc::EnsureLoaded+0x8e
0018fc8c 7404a960 mscoreei!RuntimeDesc::GetProcAddressInternal+0xe
0018fce0 7404ff19 mscoreei!CLRRuntimeInfoImpl::GetProcAddress+0x48
0018fd28 7404ffaf mscoreei!GetCorExeMainEntrypoint+0xe9
0018fd68 74267f16 mscoreei!_CorExeMain+0x54
0018fd78 74264de3 MSCOREE!ShellShim__CorExeMain+0x99
0018fd80 7544338a MSCOREE!_CorExeMain_Exported+0x8
0018fd8c 77969f72 KERNEL32!BaseThreadInitThunk+0xe
0018fdcc 77969f45 ntdll!__RtlUserThreadStart+0x70
0018fde4 00000000 ntdll!_RtlUserThreadStart+0x1b

这时我们在Windbg里运行命令.loadby sos clr来加载sos,然后运行命令!bpmd Demo.exe Demo.Program.Main在Main函数上加一个断点,然后运行命令g继续,一会儿就会停在Main函数上。这个时候再运行!bpmd Demo.exe Demo.MyClass.a加一个断点在函数a上,运行命令g继续,等停下来之后运行命令!dso来看看当前都有什么对象,命令输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
0:000> !dso
OS Thread Id: 0x1978 (0)
ESP/REG Object Name
ecx 020f230c Demo.MyClass
0018F2C4 020f230c Demo.MyClass
0018F2DC 020f230c Demo.MyClass
0018F2E0 020f230c Demo.MyClass
0018F2E4 020f22fc System.Object[] (System.String[])
0018F360 020f22fc System.Object[] (System.String[])
0018F4BC 020f22fc System.Object[] (System.String[])
0018F4EC 020f22fc System.Object[] (System.String[])
0018FA20 020f1238 System.SharedStatics

找到我们需要的Demo.MyClass的地址020f230c,运行命令!do 020f230c来找到Demo.MyClass的MethodTable,输出如下:

1
2
3
4
5
6
7
8
0:000> !do 020f230c 
Name: Demo.MyClass
MethodTable: 00293898
EEClass: 002914a0
Size: 12(0xc) bytes
File: D:\Documents\Visual Studio 2013\Projects\Demo\bin\Debug\Demo.exe
Fields:
None

运行命令!dumpmt -md 00293898来看看Demo.MyClass的函数,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0:000> !dumpmt -md 00293898
EEClass: 002914a0
Module: 00292edc
Name: Demo.MyClass
mdToken: 02000002
File: D:\Documents\Visual Studio 2013\Projects\Demo\bin\Debug\Demo.exe
BaseSize: 0xc
ComponentSize: 0x0
Slots in VTable: 7
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
Entry MethodDe JIT Name
7290952c 7262612c PreJIT System.Object.ToString()
7291ec30 72626134 PreJIT System.Object.Equals(System.Object)
7291e860 72626154 PreJIT System.Object.GetHashCode()
7291e2a0 72626168 PreJIT System.Object.Finalize()
004800b0 00293890 JIT Demo.MyClass..ctor()
004800e8 00293878 JIT Demo.MyClass.a()
0029c041 00293884 NONE Demo.MyClass.b(Int32)

这里我们可以清楚地看到Demo.MyClass.a已经被JIT了,而Demo.MyClass.b还没有。所以JIT的粒度是函数级的

所以我们在写C#的代码时要尽量做到模块化,特别是有一些很少会用到的代码,比如异常处理和日志,一定不要直接放在Main函数里,把他们抽到别的函数里就可以保证在需要他们的时候才JIT,从而不会影响到启动性能。

我们也可以直接在JIT一个方法的入口函数clr!UnsafeJitFunction上加个断点,bp clr!UnsafeJitFunction,然后继续运行g,等断点出发后我们看看callstack,运行!dumpstack,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
0:000> !dumpstack
OS Thread Id: 0x1978 (0)
Current frame: clr!UnsafeJitFunction
ChildEBP RetAddr Caller, Callee
0018f0d0 739d878c clr!MethodDesc::MakeJitWorker+0x36e, calling clr!UnsafeJitFunction
0018f0f8 77963cfe ntdll!RtlAllocateHeap+0x23a, calling ntdll!RtlpAllocateHeap
0018f1ac 73ae97b5 clr!MethodDesc::DoPrestub+0x598, calling clr!MethodDesc::MakeJitWorker
0018f1f8 739ba4e5 clr!ETWTraceStartup::ETWTraceStartup+0x37, calling clr!ETWTraceStartup::StartupTraceEvent
0018f208 739ba33b clr!MethodDesc::CheckRestore+0x23, calling clr!MethodTable::IsFullyLoaded
0018f224 739bb02b clr!PreStubWorker+0xf0, calling clr!MethodDesc::DoPrestub
0018f28c 739a2a0c clr!ThePreStub+0x16, calling clr!PreStubWorker
0018f2bc 0048012f (MethodDesc 00293878 +0x47 Demo.MyClass.a()), calling 0029c041
0018f2d4 00480099 (MethodDesc 002937e0 +0x49 Demo.Program.Main(System.String[])), calling (MethodDesc 00293878 +0 Demo.MyClass.a())

从这里我们可以看到JIT发生在函数调用的同一个线程里

另外,提高系统启动性能通常的做法有

  1. Ngen.exe (Native Image Generator),参见我的博文使用MPGO和NGEN来优化C#桌面程序的启动性能

  2. 强制JIT,下面是一段示例代码来通过RuntimeHelpers.PrepareMethod.aspx)API强制JIT。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;
using System.Reflection;
namespace Demo
{
class Program
{
static private void ForceJit(Assembly assembly)
{
var types = assembly.GetTypes();

foreach (Type type in types)
{
var ctors = type.GetConstructors(BindingFlags.NonPublic
| BindingFlags.Public
| BindingFlags.Instance
| BindingFlags.Static);

foreach (var ctor in ctors)
{
JitMethod(assembly, ctor);
}

var methods = type.GetMethods(BindingFlags.DeclaredOnly
| BindingFlags.NonPublic
| BindingFlags.Public
| BindingFlags.Instance
| BindingFlags.Static);

foreach (var method in methods)
{
JitMethod(assembly, method);
}
}
}

static private void JitMethod(Assembly assembly, MethodBase method)
{
if (method.IsAbstract || method.ContainsGenericParameters)
{
return;
}

System.Runtime.CompilerServices.RuntimeHelpers.PrepareMethod(method.MethodHandle);
}

static void Main(string[] args)
{
ForceJit(Assembly.LoadFile(@"d:\Scratch\asm.dll"));
}
}
}

小结一下本文用到的Windbg命令。

1
2
3
4
sxe ld:clr                        // 当加载clr时break
!bpmd Demo.exe Demo.Program.Main // 在Main函数上加断点
!dumpmt -md // 看类的函数具体信息,有没有被JIT
bp clr!UnsafeJitFunction // 在JIT函数入口加断点
Contents