针对 .Net、Mono、Java、C++ 及其各自 UI 的启动和系统性能基准测试






4.94/5 (43投票s)
.Net、Mono、Java 与 C++ 的启动和系统性能开销,以及 Forms、WPF、Swing 与 MFC 的对比

目录
- 引言
- 背景
- 如何计算启动时间
- 影响系统性能的因素
- C++ 性能测试代码
- .net 和 mono C# 性能测试代码
- 使用 Mono
- 使用更多 .net 版本
- 使用 Java 性能测试代码
- 获取内存使用情况
- 表格化的结果
- 对托管运行时使用本机映像
- 运行测试
- 如何对 UI 应用程序进行基准测试
- 关注点
- 历史
引言
任何计算设备都拥有有限的硬件资源,随着越来越多的应用程序和服务争夺这些资源,用户体验常常会变得迟钝。
部分性能下降是由于安装的垃圾软件造成的,但有些可能是系统启动时运行的程序、您使用应用程序时,或只是在后台运行(无论您是否需要它们)的底层技术固有的。更糟糕的是,移动设备由于 CPU 和 IO 活动增加会更快耗尽电池电量。
您可能听说过许多性能基准测试,它们比较了基于某些非常具体的特性,甚至是使用竞争技术开发的功能等效应用程序的运行时。
到目前为止,我还没有找到一个专注于启动性能以及对使用不同技术开发的应用程序的整个系统影响的基准测试。
在我看来,这类基准测试可能比其他测试更能反映用户体验。
当根据目标硬件系统决定使用哪种技术时,这种信息可能很有帮助。我专注于几种流行的技术,如 .net/C#、java、mono/C#,最后是使用 C++ 编译的本地代码,所有这些都使用其默认安装设置进行测试。
截至我撰写本文时(2010 年 7 月),Windows XP 仍拥有最大的操作系统市场份额,这些技术的最新版本是 Oracle 的 Java 1.6、Novell 的 Mono 2.6.4、Microsoft 的 .Net 4.0 和 Visual Studio 2010 中的 Visual C++。
背景
这种基准测试难以做到公平和一致的原因是,在运行此类测试时,有大量因素会起作用,并且难以找到相同的功能和行为作为共同点。
因此,我专注于一些简单的基准测试,这些测试易于重现且对所有技术都通用。
第一个测试将确定从进程创建到执行进入主函数并进程准备好执行有用操作所需的时间。这完全是从用户的角度出发,将测量活生生的人所体验到的挂钟时间。我将此称为启动时间,正如您在本文中将看到的那样,这是最难衡量但可能对用户体验最重要的方面。
下一批测量将捕获内存使用情况以及内核和用户时间作为处理器时间,而不是上面提到的启动时间。后两个 CPU 时间实际上是进程的完整生命周期,而不仅仅是进入主函数的时间。我试图将内部逻辑保持在最低限度,因为我关心的是框架的开销而不是应用程序的开销。
如何计算启动时间
在本文中,当我提到 OS API 时,我指的是 Windows XP,我将宽松地使用运行时这个术语,即使是谈论原生 C++ 代码时也是如此。OS API 没有用于检索我上面定义的启动时间的方法。
为此,我不得不设计自己的方法来计算所有框架。获取此时间的困难在于,我在此时间点在进程创建之前进行测量,然后在被调用的进程中,一旦它进入主函数就进行测量。
为了解决这个障碍,我使用了最简单的进程间通信:在创建进程时将创建时间作为命令行参数传递,并将时间差作为退出代码返回。实现此目标的步骤如下所述
您会注意到,在本文中,我提供了冷启动和热启动两个时间值。冷启动是在刚重启的机器上测量的,并且在此之前启动的应用程序尽可能少。热启动是针对同一应用程序后续启动的测量。冷启动往往较慢,主要是因为与之相关的 IO 组件。热启动利用了 OS 预取功能,通常在启动时间方面更快。
影响系统性能的因素
对于托管运行时,与原生代码相比,JIT 编译器会消耗一些额外的 CPU 时间和内存。
测试结果可能因在这些测试之前加载或运行的现有应用程序或服务而失真,尤其是在冷启动时间方面。如果其他应用程序或服务加载了您的应用程序也正在使用的库,则 I/O 操作将减少,启动时间会得到改善。
在 Java 方面,有一些应用程序设计用于加载和缓存 dll,因此,当测试运行时,您必须禁用 Java Quick Starter 或Jinitiator。
我认为缓存和预取最好留给操作系统管理,而不是不必要地浪费资源。
C++ 性能测试代码
被调用进程的 C++ 测试代码实际上是最直接的
当它获取命令行参数时,它会将其转换为 __int64,这只是表示 FILETIME 的另一种方式。FILETIME
是自 1601/1/1 以来遇到的 100 纳秒单位,因此我们计算差值并以毫秒为单位返回差值。32 位大小的退出代码应该足以处理我们正在处理的时间值,而不会溢出。
int _tmain(int argc, _TCHAR* argv[]) { FILETIME ft; GetSystemTimeAsFileTime(&ft); static const __int64 startEpoch2 = 0; // 1601/1/1 if( argc < 2 ) { ::Sleep(5000); return -1; } FILETIME userTime; FILETIME kernelTime; FILETIME createTime; FILETIME exitTime; if(GetProcessTimes(GetCurrentProcess(), &createTime, &exitTime, &kernelTime, &userTime)) { __int64 diff; __int64 *pMainEntryTime = reinterpret_cast<__int64 *>(&ft); _int64 launchTime = _tstoi64(argv[1]); diff = (*pMainEntryTime -launchTime)/10000; return (int)diff; } else return -1; }
下面是创建测试进程、为其提供原始时间,然后以退出代码的形式检索启动时间并显示其他进程属性的代码。第一次调用算作冷启动,后续调用算作热启动。根据后者,一些辅助函数提供多个热启动样本的统计数据,然后显示结果。
DWORD BenchMarkTimes( LPCTSTR szcProg) { ZeroMemory( strtupTimes, sizeof(strtupTimes) ); ZeroMemory( kernelTimes, sizeof(kernelTimes) ); ZeroMemory( preCreationTimes, sizeof(preCreationTimes) ); ZeroMemory( userTimes, sizeof(userTimes) ); BOOL res = TRUE; TCHAR cmd[100]; int i,result = 0; DWORD dwerr = 0; PrepareColdStart(); ::Sleep(3000);//3 seconds delay for(i = 0; i <= COUNT && res; i++) { STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory( &si, sizeof(si) ); si.cb = sizeof(si); ZeroMemory( &pi, sizeof(pi) ); ::SetLastError(0); __int64 wft = 0; if(StrStrI(szcProg, _T("java")) && !StrStrI(szcProg, _T(".exe"))) { wft = currentWindowsFileTime(); _stprintf_s(cmd,100,_T("java -client -cp .\\.. %s \"%I64d\""), szcProg,wft); } else if(StrStrI(szcProg, _T("mono")) && StrStrI(szcProg, _T(".exe"))) { wft = currentWindowsFileTime(); _stprintf_s(cmd,100,_T("mono %s \"%I64d\""), szcProg,wft); } else { wft = currentWindowsFileTime(); _stprintf_s(cmd,100,_T("%s \"%I64d\""), szcProg,wft); } // Start the child process. if( !CreateProcess( NULL,cmd,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi )) { dwerr = GetLastError(); _tprintf( _T("CreateProcess failed for '%s' with error code %d:%s.\n"),szcProg, dwerr,GetErrorDescription(dwerr) ); return dwerr; //break; } // Wait up to 20 seconds or until the child process exits. dwerr = WaitForSingleObject( pi.hProcess, 20000 ); if(dwerr != WAIT_OBJECT_0) { dwerr = GetLastError(); _tprintf( _T("WaitForSingleObject failed for '%s' with error code %d\n"),szcProg, dwerr ); // Close process and thread handles. CloseHandle( pi.hProcess ); CloseHandle( pi.hThread ); break; } res = GetExitCodeProcess(pi.hProcess,(LPDWORD)&result); FILETIME CreationTime,ExitTime,KernelTime,UserTime; if(GetProcessTimes(pi.hProcess,&CreationTime,&ExitTime,&KernelTime,&UserTime)) { __int64 *pKT,*pUT, *pCT; pKT = reinterpret_cast<__int64 *>(&KernelTime); pUT = reinterpret_cast<__int64 *>(&UserTime); pCT = reinterpret_cast<__int64 *>(&CreationTime); if(i == 0) { _tprintf( _T("cold start times:\nStartupTime %d ms"), result); _tprintf( _T(", PreCreationTime: %u ms"), ((*pCT)- wft)/ 10000); _tprintf( _T(", KernelTime: %u ms"), (*pKT) / 10000); _tprintf( _T(", UserTime: %u ms\n"), (*pUT) / 10000); _tprintf( _T("Waiting for statistics for %d warm samples"), COUNT); } else { _tprintf( _T(".")); kernelTimes[i-1] = (int)((*pKT) / 10000); preCreationTimes[i-1] = (int)((*pCT)- wft)/ 10000; userTimes[i-1] = (int)((*pUT) / 10000); strtupTimes[i-1] = result; } } else { printf( "GetProcessTimes failed for %p", pi.hProcess ); } // Close process and thread handles. CloseHandle( pi.hProcess ); CloseHandle( pi.hThread ); if((int)result < 0) { _tprintf( _T("%s failed with code %d: %s\n"),cmd, result,GetErrorDescription(result) ); return result; } ::Sleep(1000); //1s delay } if(i <= COUNT ) { _tprintf( _T("\nThere was an error while running '%s', last error code = %d\n"),cmd,GetLastError()); return result; } double median, mean, stddev; if(CalculateStatistics(&strtupTimes[0], COUNT, median, mean, stddev)) { _tprintf( _T("\nStartupTime: mean = %6.2f ms, median = %3.0f ms, standard deviation = %6.2f ms\n"), mean,median,stddev); } if(CalculateStatistics(&preCreationTimes[0], COUNT, median, mean, stddev)) { _tprintf( _T("PreCreation: mean = %6.2f ms, median = %3.0f ms, standard deviation = %6.2f ms\n"), mean,median,stddev); } if(CalculateStatistics(&kernelTimes[0], COUNT, median, mean, stddev)) { _tprintf( _T("KernelTime : mean = %6.2f ms, median = %3.0f ms, standard deviation = %6.2f ms\n"), mean,median,stddev); } if(CalculateStatistics(&userTimes[0], COUNT, median, mean, stddev)) { _tprintf( _T("UserTime : mean = %6.2f ms, median = %3.0f ms, standard deviation = %6.2f ms\n"), mean,median,stddev); } return GetLastError(); }
请注意,启动 mono 或 java 应用程序的命令行与 .net 或本机代码不同。此外,我没有在任何地方使用 性能监视器计数器。
调用者进程正在检索子进程的内核时间和用户时间。如果您想知道为什么我没有使用 GetProcessTimes 提供的创建时间,原因有二。
首先,它需要 .net 和 Mono 的 DllImports,以及 Java 的 JNI,这将使应用程序变得“更臃肿”。
第二个原因是我注意到创建时间并不是真正调用 CreateProcess API 的时间。在从内部硬盘运行时,两者之间我可以看到 0 到 10 毫秒的差异,但是当从较慢的介质(例如网络驱动器)运行时,这个差异可能会增加到数百毫秒,甚至几秒(这里没有打错字,伙计们,是几秒),当从软盘驱动器运行时。这个时间差将在我的测试中显示为“预创建时间”,仅供您参考。
我只能推测这是因为操作系统在创建新进程时没有计算从介质读取文件所需的时间,因为它总是出现在冷启动中,而很少出现在热启动中。
.net 和 mono C# 性能测试代码
在被调用的 .net 代码中计算启动时间与 C++ 略有不同,但使用 DateTime
的 FromFileTimeUtc 辅助方法使其与 C++ 一样容易。
private const long TicksPerMiliSecond = TimeSpan.TicksPerSecond / 1000; static int Main(string[] args) { DateTime mainEntryTime = DateTime.UtcNow;//100 nanoseconds units since 1601/1/1 int result = 0; if (args.Length > 0) { DateTime launchTime = System.DateTime.FromFileTimeUtc(long.Parse(args[0])); long diff = (mainEntryTime.Ticks - launchTime.Ticks) / TicksPerMiliSecond; result = (int)diff; } else { System.GC.Collect(2, GCCollectionMode.Forced); System.GC.WaitForPendingFinalizers(); System.Threading.Thread.Sleep(5000); } return result; }
使用 Mono
为了使用 mono,您必须下载它并通过添加 _C:\PROGRA~1\MONO-2~1.4\bin_ 或您的版本路径来更改环境变量 path
。安装时,您可以取消选择 XSP 和 GTK# 组件,因为此测试不需要它们。为了编译它,最简单的方法是使用我下载中包含的 buildMono.bat。
使用更多 .net 版本
我包含了 C# Visual Studio 1.1、2.0、3.5 和 4.0 版本的项目。如果您只想运行二进制文件,则必须下载并安装它们各自的运行时。要构建它们,您需要 Visual Studio 2003 和 2010,或者如果您喜欢命令提示符,则需要特定的 SDK。为了强制加载目标运行时版本,我为所有 .net 可执行文件创建了配置文件。
除了特定版本,它们应该如下所示
<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v1.1.4322" /> </startup> </configuration>
使用 Java 性能测试代码
如果要构建 Java 测试,您必须下载 Java SDK,安装它并在运行前正确设置 PATH。另外,在构建之前,您必须设置正确的 javac.exe 编译路径,该路径取决于版本。它应该如下所示
set path=C:\Program Files\Java\jdk1.6.0_16\bin;%path%
同样,我在下载中有一个 buildJava.bat 文件可以提供帮助。Java 测试的代码如下所示,并且必须对 Java 纪元进行调整public static void main(String[] args) { long mainEntryTime = System.currentTimeMillis();//miliseconds since since 1970/1/1 int result = 0; if (args.length > 0) { //FileTimeUtc adjusted for java epoch long fileTimeUtc = Long.parseLong(args[0]);//100 nanoseconds units since 1601/1/1 long launchTime = fileTimeUtc - 116444736000000000L;//100 nanoseconds units since 1970/1/1 launchTime /= 10000;//miliseconds since since 1970/1/1 result = (int)(mainEntryTime - launchTime); } else { try { System.gc(); System.runFinalization(); Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); } } java.lang.System.exit(result); }
Java 在测量给定时间的持续时间时缺乏时间分辨率,这就是我不得不使用毫秒而不是其他运行时提供的更精细单位的原因。然而,一毫秒对于这些测试来说已经足够了。
获取内存使用情况
您可能认为内存不是影响性能的真正因素,但这是一种肤浅的评估。当系统可用物理内存不足时,页面活动增加,这会导致磁盘 IO 和内核 CPU 活动增加,从而对所有正在运行的应用程序产生不利影响。
Windows 进程在内存使用方面有很多方面,我将我的测量限制在 私有字节、最小和峰值 工作集。您可以从 Windows 专家那里获取有关这个复杂主题的更多信息。
如果您想知道为什么在没有参数的情况下被调用进程会等待 5 秒,现在您有了答案。在等待 2 秒后,调用者将测量下面代码中的内存使用情况
BOOL PrintMemoryInfo( const PROCESS_INFORMATION& pi) { //wait 2 seconds while the process is sleeping for 5 seconds if(WAIT_TIMEOUT != WaitForSingleObject( pi.hProcess, 2000 )) return FALSE; if(!EmptyWorkingSet(pi.hProcess)) printf( "EmptyWorkingSet failed for %x\n", pi.dwProcessId ); BOOL bres = TRUE; PROCESS_MEMORY_COUNTERS_EX pmc; if ( GetProcessMemoryInfo( pi.hProcess, (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc)) ) { printf( "PrivateUsage: %lu KB,", pmc.PrivateUsage/1024 ); printf( " Minimum WorkingSet: %lu KB,", pmc.WorkingSetSize/1024 ); printf( " PeakWorkingSet: %lu KB\n", pmc.PeakWorkingSetSize/1024 ); } else { printf( "GetProcessMemoryInfo failed for %p", pi.hProcess ); bres = FALSE; } return bres; }
最小工作集是我在通过 EmptyWorkingSet API 缩小被调用进程的内存后计算出的值。
表格化的结果
这些测试产生了大量需要消化的结果。我选择了我认为与此主题最相关的测量值,并将热启动的摘要放在本页顶部的图表中。如果您在调试模式下运行测试,则可以获得更多结果。
对于热启动,我运行了 9 次测试,但冷启动是一次性的,如上所述。我只包含了中位数,内核时间和用户时间合并为 CPU 时间。下面的结果是在一台运行 3 GHz Pentium 4 CPU 和 2 GB RAM 的 Windows XP 机器上生成的。
运行时 | 冷启动 | 热启动 | 私有使用 (KB) |
最小工作集 (KB) |
峰值工作集 (KB) |
||
启动 时间(ms) |
CPU 时间(ms) |
启动 时间(ms) |
CPU 时间(ms) |
||||
.Net 1.1 | 1844 | 156 | 93 | 93 | 3244 | 104 | 4712 |
.Net 2.0 | 1609 | 93 | 78 | 93 | 6648 | 104 | 5008 |
.Net 3.5 | 1766 | 125 | 93 | 77 | 6640 | 104 | 4976 |
.Net 4.0 | 1595 | 77 | 46 | 77 | 7112 | 104 | 4832 |
Java 1.6 | 1407 | 108 | 94 | 92 | 39084 | 120 | 11976 |
Mono 2.6.4 | 1484 | 156 | 93 | 92 | 4288 | 100 | 5668 |
CPP 代码 | 140 | 30 | 15 | 15 | 244 | 40 | 808 |
您可能认为 .Net 4.0 和 .Net 2.0 违反了物理定律,因为它们的启动时间低于 CPU 时间,但请记住,CPU 时间是针对进程的整个生命周期,而启动只是为了进入主函数。这仅仅告诉我们,这些框架有一些优化可以提高启动速度。
正如您很容易看到的,C++ 在所有类别中都明显获胜,但有一些注意事项:调用者进程通过预加载一些常用 DLL 来“帮助”C++ 测试,而且没有调用垃圾回收器。
我没有所有运行时的历史数据,但仅查看 .net,以下趋势表明,新版本倾向于更快,但代价是占用更多内存。

对托管运行时使用本机映像
由于除本机代码之外的所有运行时都使用中间代码,下一步自然是尝试在可能的情况下创建本机映像并再次评估性能。
Java 没有易于使用的工具来达到此目的。GCJ 是朝这个方向迈出的一小步,但它并非官方运行时的一部分,因此我将忽略它。Mono 具有类似的功能,称为 Ahead of Time (AOT)。不幸的是,此编译功能在 Windows 上会失败。
.Net 从一开始就支持本机代码生成,并且 ngen.exe 是运行时分发的一部分。
为了您的方便,我已将 make_nativeimages.bat 添加到下载中,它将为用于测试的程序集生成本机映像。
运行时 | 冷启动 | 热启动 | 私有使用 (KB) |
最小工作集 (KB) |
峰值工作集 (KB) |
||
启动 时间(ms) |
CPU 时间(ms) |
启动 时间(ms) |
CPU 时间(ms) |
||||
.Net 1.1 | 2110 | 140 | 109 | 109 | 3164 | 108 | 4364 |
.Net 2.0 | 1750 | 109 | 78 | 77 | 6592 | 108 | 4796 |
.Net 3.5 | 1859 | 140 | 78 | 77 | 6588 | 108 | 4800 |
.Net 4.0 | 1688 | 108 | 62 | 61 | 7044 | 104 | 4184 |
再次,我们似乎看到了另一个悖论:本机编译的程序集的启动时间似乎更高,这将违背使用本机代码生成的目的。但是,如果我们进一步思考,它开始变得有意义,因为加载本机映像所需的 IO 操作可能比编译我们测试程序中的少量代码花费更长的时间。
运行测试
您可以通过将测试可执行文件作为参数传递给 BenchMarkStartup.exe 来运行特定测试。对于 Java,包名必须与目录结构匹配,因此参数 JavaPerf.StartupTest 将需要一个 ..\JavaPerf 文件夹。
我已在下载中包含 runall.bat,但该批处理文件无法捕获真实的冷启动时间。
如果您想要真实的测试,可以手动重启或在每 20-30 分钟的夜间计划任务中从发布文件夹调用 benchmark.bat,并在文本日志文件中获取结果(我就是这样做的)。这将通过重启您的机器来运行所有运行时的真实测试。
最新的计算机通常会为了节能而限制 CPU 频率,但这可能会改变这些测试结果。因此,在运行测试之前,除了我之前提到的事项外,您还必须将电源方案设置为“高性能”,以获得一致的结果。有如此多的软件和硬件因素,您可能会看到截然不同的结果,甚至运行时排名也会发生变化。
如何对 UI 应用程序进行基准测试
到目前为止,我测试的应用程序都附加到控制台,没有任何 UI 组件。添加 UI 赋予了启动时间新的含义,它成为从进程启动到用户界面显示并准备使用的时间。为简单起见,用户界面仅由一个中心带有一些文本的窗口组成。
看起来,启动时间可以很容易地仅从调用进程中测量,方法是计时创建进程和从 WaitForInputIdle 返回之间的时间间隔。不幸的是,除了 MFC 和 .Net 1.1 中的 WinForms(我将在下面描述的测试中)之外,情况并非如此。其他测试的框架甚至在 UI 显示之前就会从 WaitForInputIdle 返回。这让我回到了我上面用于获取启动时间的先前方法。
另一个障碍是,与其他框架不同,Java 缺乏一种在应用程序启动后立即发出用户不活动信号的方法,除非使用第三方解决方案。因此,我将用户不活动替换为 UI 显示时的最后已知事件。计算启动时间的脚本变为
调用者代码
计算启动时间的调用者代码与非 UI 测试的代码类似,只是这次我们可以通过简单的 Windows 消息控制窗口状态和进程生命周期,我们将使用 javaw.exe 而不是 java.exe。
DWORD BenchMarkTimes( LPCTSTR szcProg) { ZeroMemory( strtupTimes, sizeof(strtupTimes) ); ZeroMemory( kernelTimes, sizeof(kernelTimes) ); ZeroMemory( preCreationTimes, sizeof(preCreationTimes) ); ZeroMemory( userTimes, sizeof(userTimes) ); BOOL res = TRUE; TCHAR cmd[100]; int i,result = 0; DWORD dwerr = 0; PrepareColdStart(); ::Sleep(3000);//3 seconds delay for(i = 0; i <= COUNT && res; i++) { STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory( &si, sizeof(si) ); si.cb = sizeof(si); ZeroMemory( &pi, sizeof(pi) ); ::SetLastError(0); __int64 wft = 0; if(StrStrI(szcProg, _T("java")) && !StrStrI(szcProg, _T(".exe"))) { //java -client -cp .\.. JavaPerf.StartupTest wft = currentWindowsFileTime(); //will use javaw instead of java _stprintf_s(cmd,100,_T("javaw.exe -client -cp .\\.. %s \"%I64d\""), szcProg,wft); } else if(StrStrI(szcProg, _T("mono")) && StrStrI(szcProg, _T(".exe"))) { //mono ..\monoperf\mono26perf.exe wft = currentWindowsFileTime(); _stprintf_s(cmd,100,_T("mono %s \"%I64d\""), szcProg,wft); } else { wft = currentWindowsFileTime(); _stprintf_s(cmd,100,_T("%s \"%I64d\""), szcProg,wft); } // Start the child process. if( !CreateProcess( NULL,cmd,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi )) { dwerr = GetLastError(); _tprintf( _T("CreateProcess failed for '%s' with error code %d:%s.\n"),szcProg, dwerr,GetErrorDescription(dwerr) ); return dwerr; } if(!CloseUIApp(pi)) break; // Wait up to 20 seconds or until the child process exits. dwerr = WaitForSingleObject( pi.hProcess, 20000 ); if(dwerr != WAIT_OBJECT_0) { dwerr = GetLastError(); _tprintf( _T("WaitForSingleObject failed for '%s' with error code %d\n"),szcProg, dwerr ); // Close process and thread handles. //TerminateProcess( pi.hProcess, -1 ); CloseHandle( pi.hProcess ); CloseHandle( pi.hThread ); break; } res = GetExitCodeProcess(pi.hProcess,(LPDWORD)&result); FILETIME CreationTime,ExitTime,KernelTime,UserTime; if(GetProcessTimes(pi.hProcess,&CreationTime,&ExitTime,&KernelTime,&UserTime)) { __int64 *pKT,*pUT, *pCT; pKT = reinterpret_cast<__int64 *>(&KernelTime); pUT = reinterpret_cast<__int64 *>(&UserTime); pCT = reinterpret_cast<__int64 *>(&CreationTime); if(i == 0) { _tprintf( _T("cold start times:\nStartupTime %d ms"), result); _tprintf( _T(", PreCreationTime: %u ms"), ((*pCT)- wft)/ 10000); _tprintf( _T(", KernelTime: %u ms"), (*pKT) / 10000); _tprintf( _T(", UserTime: %u ms\n"), (*pUT) / 10000); _tprintf( _T("Waiting for statistics for %d warm samples"), COUNT); } else { _tprintf( _T(".")); kernelTimes[i-1] = (int)((*pKT) / 10000); preCreationTimes[i-1] = (int)((*pCT)- wft)/ 10000; userTimes[i-1] = (int)((*pUT) / 10000); strtupTimes[i-1] = result; } } else { printf( "GetProcessTimes failed for %p", pi.hProcess ); } // Close process and thread handles. CloseHandle( pi.hProcess ); CloseHandle( pi.hThread ); if((int)result < 0) { _tprintf( _T("%s failed with code %d: %s\n"),cmd, result,GetErrorDescription(result) ); return result; } ::Sleep(1000); //1s delay }//for ends if(i <= COUNT ) { result = GetLastError(); _tprintf( _T("\nThere was an error while running '%s', last error code = %d: %s\n"),cmd,result,GetErrorDescription(result)); return result; } double median, mean, stddev; if(CalculateStatistics(&strtupTimes[0], COUNT, median, mean, stddev)) { _tprintf( _T("\nStartupTime: mean = %6.2f ms, median = %3.0f ms, standard deviation = %6.2f ms\n"), mean,median,stddev); } if(CalculateStatistics(&preCreationTimes[0], COUNT, median, mean, stddev)) { _tprintf( _T("PreCreation: mean = %6.2f ms, median = %3.0f ms, standard deviation = %6.2f ms\n"), mean,median,stddev); } if(CalculateStatistics(&kernelTimes[0], COUNT, median, mean, stddev)) { _tprintf( _T("KernelTime : mean = %6.2f ms, median = %3.0f ms, standard deviation = %6.2f ms\n"), mean,median,stddev); } if(CalculateStatistics(&userTimes[0], COUNT, median, mean, stddev)) { _tprintf( _T("UserTime : mean = %6.2f ms, median = %3.0f ms, standard deviation = %6.2f ms\n"), mean,median,stddev); } return GetLastError(); }
内存使用情况的测量也与之前的方法类似,只是使用了默认和最小化状态。像 MinimizeUIApp 或 CloseUIApp 这样的辅助函数不是本文的主题,因此我不会描述它们。
DWORD BenchMarkMemory( LPCTSTR szcProg) { TCHAR cmd[100]; STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory( &si, sizeof(si) ); si.cb = sizeof(si); ZeroMemory( &pi, sizeof(pi) ); DWORD dwerr; if(StrStrI(szcProg, _T("java")) && !StrStrI(szcProg, _T(".exe"))) { _stprintf_s(cmd,100,_T("javaw -client -cp .\\.. %s"), szcProg); } else if(StrStrI(szcProg, _T("mono")) && StrStrI(szcProg, _T(".exe"))) { _stprintf_s(cmd,100,_T("mono %s"), szcProg); } else { _stprintf_s(cmd,100,_T("%s"), szcProg); } ::SetLastError(0); // Start the child process. if( !CreateProcess( NULL,cmd,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi )) { dwerr = GetLastError(); _tprintf( _T("CreateProcess failed for '%s' with error code %d:%s.\n"),szcProg, dwerr,GetErrorDescription(dwerr) ); return dwerr; } //wait to show up ::Sleep(3000); PROCESS_MEMORY_COUNTERS_EX pmc; if ( GetProcessMemoryInfo( pi.hProcess, (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc)) ) { printf( "Normal size->PrivateUsage: %lu KB,", pmc.PrivateUsage/1024 ); printf( " Current WorkingSet: %lu KB\n", pmc.WorkingSetSize/1024 ); } else { printf( "GetProcessMemoryInfo failed for %p", pi.hProcess ); } HWND hwnd = MinimizeUIApp(pi); //wait to minimize ::Sleep(2000); if ( GetProcessMemoryInfo( pi.hProcess, (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc)) ) { printf( "Minimized-> PrivateUsage: %lu KB,", pmc.PrivateUsage/1024 ); printf( " Current WorkingSet: %lu KB\n", pmc.WorkingSetSize/1024 ); } else printf( "GetProcessMemoryInfo failed for %p", pi.hProcess ); if(!EmptyWorkingSet(pi.hProcess)) printf( "EmptyWorkingSet failed for %x\n", pi.dwProcessId ); else { ZeroMemory(&pmc, sizeof(pmc)); if ( GetProcessMemoryInfo( pi.hProcess, (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc)) ) { printf( "Minimum WorkingSet: %lu KB,", pmc.WorkingSetSize/1024 ); printf( " PeakWorkingSet: %lu KB\n", pmc.PeakWorkingSetSize/1024 ); } else printf( "GetProcessMemoryInfo failed for %p", pi.hProcess ); } if(hwnd) ::SendMessage(hwnd,WM_CLOSE,NULL,NULL); // Wait up to 20 seconds or until the child process exits. dwerr = WaitForSingleObject( pi.hProcess, 20000 ); if(dwerr != WAIT_OBJECT_0) { dwerr = GetLastError(); _tprintf( _T("WaitForSingleObject failed for '%s' with error code %d\n"),szcProg, dwerr ); } // Close process and thread handles. CloseHandle( pi.hProcess ); CloseHandle( pi.hThread ); return GetLastError(); }
MFC 测试代码
如果需要,您仍然可以使用 Win32 API 创建本机窗口,但很可能您会使用现有库来完成。有几种基于 C++ 的 UI 框架用于开发 UI 应用程序,例如 ATL、WTL、MFC 等。性能测试使用动态链接的 MFC 库,因为它是 C++ 开发中最流行的 UI 框架。下面的代码片段显示了在对话框初始化期间如何计算启动时间并从应用程序返回。
void CCPPMFCPerfDlg::OnShowWindow(BOOL bShow, UINT nStatus) { CDialog::OnShowWindow(bShow, nStatus); FILETIME ft; GetSystemTimeAsFileTime(&ft); if( __argc < 2 )//__argc, __targv { theApp.m_result = 0; return; } FILETIME userTime; FILETIME kernelTime; FILETIME createTime; FILETIME exitTime; if(GetProcessTimes(GetCurrentProcess(), &createTime, &exitTime, &kernelTime, &userTime)) { __int64 diff; __int64 *pMainEntryTime = reinterpret_cast<__int64 *>(&ft); _int64 launchTime = _tstoi64(__targv[1]); diff = (*pMainEntryTime -launchTime)/10000; theApp.m_result = (int)diff; } else theApp.m_result = 0; } int CCPPMFCPerfApp::ExitInstance() { int result = CWinApp::ExitInstance(); if(!result) return 0; else return m_result; }
Windows Forms 测试代码
.Net 从一开始就提供了 Windows Forms 作为编写 UI 应用程序的方式。下面的测试代码在所有 .Net 版本中都相似。
public partial class Form1 : Form { private const long TicksPerMiliSecond = TimeSpan.TicksPerSecond / 1000; int _result = 0; public Form1() { InitializeComponent(); } /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static int Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Form1 f = new Form1(); Application.Run(f); return f._result; } protected override void OnActivated(EventArgs e) { base.OnActivated(e); if (Environment.GetCommandLineArgs().Length == 1) { System.GC.Collect(2, GCCollectionMode.Forced); System.GC.WaitForPendingFinalizers(); } else if (this.WindowState == FormWindowState.Normal) { DateTime mainEntryTime = DateTime.UtcNow; string launchtime = Environment.GetCommandLineArgs()[1]; DateTime launchTime = System.DateTime.FromFileTimeUtc(long.Parse(launchtime)); long diff = (mainEntryTime.Ticks - launchTime.Ticks) / TicksPerMiliSecond; _result = (int)diff; } } }
WPF 测试代码
Microsoft 最新在 .Net 框架之上运行的 UI 是 WPF,它应该比 Windows Forms 更全面,并且面向最新版本的 Windows。获取启动时间的相关代码如下所示。
public partial class Window1 : Window { public Window1() { InitializeComponent(); } private const long TicksPerMiliSecond = TimeSpan.TicksPerSecond / 1000; private void Window_Loaded(object sender, RoutedEventArgs e) { if (Environment.GetCommandLineArgs().Length == 1) { System.GC.Collect(2, GCCollectionMode.Forced); System.GC.WaitForPendingFinalizers(); } else if (this.WindowState == WindowState.Normal) { DateTime mainEntryTime = DateTime.UtcNow; string launchtime = Environment.GetCommandLineArgs()[1]; DateTime launchTime = System.DateTime.FromFileTimeUtc(long.Parse(launchtime)); long diff = (mainEntryTime.Ticks - launchTime.Ticks) / TicksPerMiliSecond; (App.Current as App)._result= (int)diff; } } } public partial class App : Application { internal int _result = 0; protected override void OnExit(ExitEventArgs e) { e.ApplicationExitCode = _result; base.OnExit(e); } }
Java 测试代码
Java 有两个流行的 UI 库:Swing 和 AWT。由于 Swing 比 AWT 更受欢迎,我用它来构建 UI 测试,如下所示。
public class SwingTest extends JFrame { static int result = 0; JLabel textLabel; String[] args; SwingTest(String[] args) { this.args = args; } public static void main(String[] args) { try { //Create and set up the window. SwingTest frame = new SwingTest(args); frame.setTitle("Simple GUI"); frame.enableEvents(AWTEvent.WINDOW_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK); frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frame.setResizable(true); frame.textLabel = new JLabel("Java Swing UI Test", SwingConstants.CENTER); frame.textLabel.setAutoscrolls(true); frame.textLabel.setPreferredSize(new Dimension(300, 100)); frame.getContentPane().add(frame.textLabel, BorderLayout.CENTER); //Display the window. frame.setLocation(100, 100);// setLocationRelativeTo(null); frame.pack(); frame.setVisible(true); } catch (Exception ex) { System.exit(-1); } } protected void processWindowEvent(WindowEvent e) { super.processWindowEvent(e); switch (e.getID()) { case WindowEvent.WINDOW_OPENED: long mainEntryTime = System.currentTimeMillis();//miliseconds since since 1970/1/1 if (args.length == 0) { try { System.gc(); System.runFinalization(); Thread.sleep(2000); } catch (Exception ex) { ex.printStackTrace(); } } else { //FileTimeUtc adjusted for java epoch long fileTimeUtc = Long.parseLong(args[0]);//100 nanoseconds units since 1601/1/1 long launchTime = fileTimeUtc - 116444736000000000L;//100 nanoseconds units since 1970/1/1 launchTime /= 10000;//miliseconds since since 1970/1/1 result = (int)(mainEntryTime - launchTime); //displayMessage("startup time " + result + " ms"); } break; case WindowEvent.WINDOW_CLOSED: System.exit(result); break; } } }
Mono 上的 Forms
Mono 拥有与 Microsoft Forms 兼容的自己的 WinForms。C# 代码与上面显示的 C# 代码几乎相同,只是构建命令不同
set path=C:\PROGRA~1\MONO-2~1.4\bin\;%path%
gmcs -target:exe -o+ -nostdlib- -r:System.Windows.Forms.dll -r:System.Drawing.dll MonoUIPerf.cs -out:Mono26UIPerf.exe
这些命令也包含在 buildMonoUI.bat 中,以方便您使用。
UI 表格化结果
UI 应用程序在 Windows XP 上运行时会调整工作集,无论是在最小化状态还是默认大小下,除了我之前测试中获得的数据外,我还捕获了这些额外的测量值。我只保留了默认大小窗口的“私有字节”,因为它总是非常接近或与最小化窗口相同。
UI/运行时版本 | 冷启动 | 热启动 | 当前工作集 | 私有使用 (KB) |
最小工作集 (KB) |
峰值工作集 (KB) |
|||
启动 时间(ms) |
CPU 时间(ms) |
启动 时间(ms) |
CPU 时间(ms) |
最小化 大小(KB) |
默认值 大小(KB) |
||||
Forms 1.1 | 4723 | 296 | 205 | 233 | 584 | 8692 | 5732 | 144 | 8776 |
Forms 2.0 | 3182 | 202 | 154 | 171 | 604 | 8552 | 10252 | 128 | 8660 |
Forms 3.5 | 3662 | 171 | 154 | 155 | 600 | 8604 | 10252 | 128 | 8712 |
WPF 3.5 | 7033 | 749 | 787 | 733 | 1824 | 14160 | 15128 | 184 | 14312 |
Forms 4.0 | 3217 | 265 | 136 | 139 | 628 | 9224 | 10956 | 140 | 9272 |
WPF 4.0 | 6828 | 733 | 718 | 702 | 2056 | 16456 | 16072 | 212 | 16620 |
Swing Java 1.6 | 1437 | 546 | 548 | 546 | 3664 | 21764 | 43708 | 164 | 21780 |
Forms Mono 2.6.4 | 4090 | 968 | 804 | 984 | 652 | 12116 | 6260 | 132 | 15052 |
MFC C++ | 875 | 108 | 78 | 61 | 592 | 3620 | 804 | 84 | 3636 |
WPF 相较于 Forms 较差的结果部分原因可能是 WPF 是从 Windows Vista 向后移植到 Windows XP 的,并且其功能集更大。
由于一图胜千言,在下面的图表中,我展示了每种技术的最新版本的启动时间、CPU 累计时间、窗口默认大小下的工作集和私有字节。

与之前的非 UI 测试一样,C++/MFC 似乎比其他框架表现得更好。
本机映像对 UI 性能的影响
您可以使用 ngen.exe 或 make_nativeimages.bat 为用于测试的程序集生成本机映像。
UI/运行时版本 | 冷启动 | 热启动 | 当前工作集 | 私有使用 (KB) |
最小工作集 (KB) |
峰值工作集 (KB) |
|||
启动 时间(ms) |
CPU 时间(ms) |
启动 时间(ms) |
CPU 时间(ms) |
最小化 大小(KB) |
默认值 大小(KB) |
||||
Forms 1.1 | 6046 | 280 | 237 | 218 | 568 | 8116 | 5640 | 144 | 8204 |
Forms 2.0 | 5316 | 140 | 217 | 186 | 580 | 7928 | 10144 | 580 | 8036 |
Forms 3.5 | 2866 | 202 | 158 | 140 | 588 | 7992 | 10140 | 132 | 8092 |
Forms 4.0 | 4149 | 156 | 217 | 187 | 632 | 8416 | 10892 | 144 | 8476 |
WPF 3.5 | 7844 | 671 | 889 | 708 | 1840 | 13644 | 15052 | 1840 | 13800 |
WPF 4.0 | 6520 | 718 | 718 | 718 | 2032 | 15748 | 15976 | 216 | 15948 |
同样,对于非常小的 UI 测试应用程序使用本机映像似乎不会带来任何性能提升,有时甚至可能比没有本机代码生成更糟糕。
运行 UI 测试
您可以执行下载中包含的 RunAllUI.bat,但该批处理文件无法捕获真实的冷启动时间。由于这次您正在运行 UI 应用程序,因此除非您创建自己的自动重启设置,否则您无法在重启后不登录的情况下使用相同的调度程序技巧。
关注点
希望本文能帮助您看清时髦术语的本质,让您了解在使用托管运行时而非本机代码的便利性与性能之间的权衡所带来的开销,并在决定开发所使用的软件技术时提供帮助。
您也可以根据我的模型创建自己的新测试,或在不同平台上进行测试。我提供了 Visual Studio 2003、2008 和 2010 的解决方案,其中包含 Java 和 Mono 的源代码和构建文件。编译选项已设置为在可用时优化速度。
历史
1.0 版于 2010 年 7 月发布。
1.1 版于 2010 年 9 月发布,增加了 UI 基准测试。