技术开发 频道

.NET、Mono与Java、C++性能测试大PK

  【IT168 技术】任何计算设备硬件资源都是有限的,越多的程序和服务竞争资源,用户的体验越糟糕(通常表现为延迟较长),性能下降的部分原因是因为安装了不需要的组件,还有部分原因是程序内部的设计问题,如让程序随系统启动而启动,或不管你是否会使用它,都让它在后台运行着,这些运行着但又未使用的进程都会抢占有限的系统资源。

  虽然我见过一些有关程序性能测试的文章,但却未见过对程序的启动时间进行测试的,更别说是不同编程语言(框架),或同一框架的不同版本了,但这种测试结果对于选择特定硬件系统后,确定编程语言是非常有帮助的。本文将介绍当前比较流行的语言(框架) -.NET,Java,Mono和C++程序的启动性能对比,所有测试都是在它们各自的默认设置下进行的。但.NET,Mono,Java托管代码和C++原生代码谁的启动时间最短,谁的性能较好呢?首先来看一下热启动的对比结果吧!

  由于测试中有诸多因素会影响结果,为了使测试结果显得更公平,我们只使用了一些简单的,可重复的测试,所有语言都可执行这些测试。

  首先我们要测试的是从进程创建到进入main函数所花的时间,简称为“启动时间”,要精确地测试出启动时间是很困难的,有时只有凭用户的感觉,接下来测量了内存占用情况,内核和用户消耗的处理器时间。

  如何计算启动时间

  在下面的内容中,凡是提到操作系统API,我指的操作系统都是指Windows XP,由于没有现成的操作系统API可以获得程序的启动时间,因此我用了自己发明的方法来计算,我使用了简单的进程间通信机制来解决这个问题,创建进程时将创建时间作为一个命令行参数传递给测试进程,执行到退出代码时返回当前时间和创建时间的差,具体步骤说明如下:

  在调用者进程(BenchMarkStartup.exe)中获得当前的UTC系统时间;

  启动测试进程,将前面获得的进程创建时间作为参数传递给它;

  在分支进程中,获得main函数开始执行时的当前系统UTC时间;

  在同一进程中,计算并调整时间差;

  执行到退出代码时返回时间差;

  在调用者进程(BenchMarkStartup.exe)中捕捉退出代码。

  本文会使用到两个启动时间:冷启动时间和热启动时间,冷启动表示系统重启后,程序的第一次启动时间,热启动时间表示程序关闭后,再次启动所花的时间。冷启动需要的时间往往会长一些,因为需要加载I/O组件,热启动可以利用操作系统的预取功能,因此热启动的时间要短得多。

  影响性能的因素

  对于托管的运行时,与原生代码比起来,JIT编译器将会消耗额外的CPU时间和内存。特别是对于冷启动时间的对比可能会有失公允,C++原生代码肯定会占有优势,而托管型的Mono,Java和.NET代码需要更长的加载时间。另外,如果其它程序加载了你需要的库,I/O操作也会减少,启动时间也会得到改善。在Java方面,也有一些启动加速程序,如Java Quick Starter,Jinitiator,为了公平起见,应该禁用它们。缓存和预取功能也应该留给操作系统去管理,不要浪费不必要的资源。

  C++性能测试代码

  C++测试代码是直接由调用者进程调用的,当它获得一个命令行参数时,它会将其转换成__int64来表示FILETIME,其值是从1601/1/1到现在的100 毫微秒间隔数,因此我们可以获得时间差,以毫秒数返回,用32位大小就足够了。

int _tmain(int argc, _TCHAR* argv[])

  {

  FILETIME ft;
  etSystemTimeAsFileTime(
&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秒延迟
      
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);
                   }            
// 启动子进程          
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;              
//中断;
          }
            
//等待20秒,或直到子进程退出          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 );
              
// 关闭进程和线程处理               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 );
           }          
// 关闭进程和线程处理           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); //1秒延时
      }
      
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,需要DllImport,对于Java需要JNI,这样就使程序变得更加臃肿了;第二个原因是我发现创建时间不是CreateProcess API被调用的真正时间。从本地硬盘运行测试时,由这两个因素引起的时间会相差0-10毫秒,如果是从网络驱动器运行,时间会有数百毫秒的出入,如果是从软盘上运行,甚至可能达到几秒。我把这个时间差叫做预创建时间,我猜测这是因为操作系统没有考虑创建新进程时,从存储介质读取文件所花的时间所致,因为只在冷启动时有这个差异,而热启动就没有。

0
相关文章