65.9K
CodeProject 正在变化。 阅读更多。
Home

操作性能评估

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (62投票s)

2011年12月23日

CPOL

23分钟阅读

viewsIcon

109441

downloadIcon

1723

调查C#、C++、Java、Fortran和JavaScript在真实世界中(即非峰值)操作的周期成本性能测量

Performance overview

引言

你是否曾想过“Fortran是最快的语言”或“C++无法被Java或C#匹敌”等编程语言的刻板印象是否仍然有效?本文试图回答这个问题的一部分。评估已在以下语言中完成

  • C#
  • C++
  • Java
  • Fortran
  • JavaScript

最后一种语言并非旨在与上述语言认真竞争,更多是出于兴趣。JavaScript的结果将不会与其它四种语言混合。每种语言都进行了多次测试,以发现其它有趣的细节。其中一些细节涉及

  • C#应用程序的Debug模式比Release模式慢多少?
  • 由于64位(而不是32位)的缘故,我能从(32位)整数操作中获得多少加速?
  • C#应用程序编译为AnyCPUx64是否有区别?
  • 将程序编译为MSIL而不是直接字节码会慢多少?
  • 哪个浏览器在每操作周期测量中拥有最快的JavaScript引擎?

为了尽可能真实地进行这些评估,我们设置了一段代码,稍后将进行解释。需要注意的是,代码已尽可能精确地重写。然而,有些语言没有提供与用C#编写的原始程序代码相同的功能。因此,JavaScript、Java和Fortran使用了其他时间测量例程,因为这些语言无法很好地连接到Windows系统,因此无法访问以Tick测量的Windows系统时间,其中1 Tick每1000个周期发生一次。

本文的用途

本文旨在对核心操作(如加法(+)、减法(-)、乘法(*)和除法(/)以及取模(%))的语言性能进行定性比较。还测试了组合操作,如除法、加法和取模(/+%)以及除法、加法、减法和乘法(/+-*)。这将使我们对所使用的编译器的优化有所了解。每种语言都极大地依赖于所使用的编译器,因为语言不过是一组语法规则和有意义的单词。编译器负责使用给定语言将代码从高级语言翻译成低级语言(如汇编代码或MSIL)。

本文不涉及的内容

本文无意深入探讨每种语言或编译器的峰值性能。优化以限制性方式使用,以确保优化不会破坏任何大型代码。因此,我将 Fortran 或 C++ 的优化级别最高设置为 O2。此外,我没有设置一台全新的计算机,即在基准测试期间,一些程序和服务正在运行且处于活动状态。然而,在进行基准测试时,CPU 处于低/中等负载,这种情况在实际应用程序中也会发生。因此,我认为这是一项实际测试。

本文也不会说任何语言比另一种语言快。在我看来,语言的速度受许多因素决定。即使一种语言执行操作更快,也并不能说内存操作也以同样的方式进行缩放。此外,许多程序访问不同的资源(硬盘、网络等),这也可能偏向于本文列出的不同语言。总而言之,一种语言对于某种操作的速度并不能说明太多关于该语言的性能——每种语言都针对不同类型的问题。

背景

为了计算集合中项目的最佳宽度,我将可用空间的宽度除以项目数量。这是一个整数除法,也就是说,我们将得到一个整数。通常,结果并不精确,也就是说,如果我将结果乘以项目数量,我就无法再次获得可用空间的宽度。为了避免这种错误,我基本上这样做:

int widthPerItem = total_width / items.Length;
int remaining = total_width % items.Length;

从这一点来看,我们有两种可能的解决方案。第一种解决方案不太优雅

items[0].Width = widthPerItem + remaining;
for(int i = 1; i < items.Length; i++)
{
    items[i].Width = widthPerItem;
}

在这里,我们将额外的(剩余的)像素分配给第一个项目,而其他项目则保持计算出的宽度。第二种解决方案更优雅

for(int i = 0; i < items.Length; i++)
{
    items[i].Width = widthPerItem + Math.Sign(remaining);
    if(remaining > 0)
        remaining--;
}

在这里,我们给每个项目的计算宽度增加一个像素,直到没有多余的像素。这意味着有些项目将获得计算宽度加一个像素,而其他项目只获得计算宽度。虽然这两种解决方案似乎适用于各种情况,但我的文章将处理先进行除法再进行取模运算的初始问题。这两种运算的开销大致相同。这两种运算也都非常昂贵!我曾被告知,除法运算的开销大约是加法运算的八倍。本文将尝试找出

  1. 经验法则仍然有效吗?
    • 减法等于加法
    • 乘法比加法更昂贵
    • 除法大约是加法的八倍
    • 取模运算等于除法
  2. 以下代码是获取剩余部分的更好方法吗?
    int widthPerItem = total_width / items.Length;
    int remaining = total_width - widthPerItem * items.Length;

基本代码

原始代码是用C#编写的,用于.NET Framework。为了测量性能,每个操作都必须重复多次。在评估中,我们将程序设置为重复10,000次。由于在for循环中简单地重复一个操作有时可以被编译器轻易解决,因此有必要修改操作数。这通过使用一个相当长的数组的数据点来解决。我们使用了100,000个数据点。这导致数组大小为100,000乘以4字节(一个整数),即400,000字节或大约391 KB。这当然比处理器的L1缓存大,但肯定比处理器的L2缓存(现在)小。

总而言之,每个操作(或组合操作)都执行109次。由于通常的频率在GHz或109 Hz范围内,我们可以估计每个循环将持续超过一秒,因为每个操作至少需要一个周期。Stopwatch实例将以tick(即103个周期)为单位提供时间。这仍然会导致相当大的数字,因此我们将结果除以1000,以获得106个周期的结果。

//The constants being used for the evaluations
const int SIZE = 100000;
const int REPEAT = 10000;

static void Main(string[] args)
{
    Random ran = new Random(0);

    //Initialize the required arrays
    int[] A = new int[SIZE];
    int[] B = new int[SIZE];
    int[] C = new int[SIZE];

    //Fill the arrays with random numbers:
    for (int i = 0; i < SIZE; i++)
    {
        A[i] = ran.Next();
        B[i] = ran.Next();
    }

    //Always use the .NET-Stopwatch for timing
    Stopwatch sw = new Stopwatch();
    sw.Start();

    for (int j = 0; j < REPEAT; j++)
        for (int i = 0; i < SIZE; i++)
            C[i] = A[i] + B[i];

    sw.Stop();
    Console.WriteLine("+:       " + sw.ElapsedTicks / 1000);

    /* The same for -, *, /, % */

    sw.Reset();
    sw.Start();

    //First combined operation
    for (int j = 0; j < REPEAT; j++)
        for (int i = 0; i < SIZE; i++)
            C[i] = A[i] / B[i] + A[i] % B[i];

    sw.Stop();
    Console.WriteLine("/ + %:  " + sw.ElapsedTicks / 1000);

    sw.Reset();
    sw.Start();

    //Second one - to verify if this one really executes faster
    for (int j = 0; j < REPEAT; j++)
        for (int i = 0; i < SIZE; i++)
        {
            C[i] = A[i] / B[i];
            C[i] += A[i] - C[i] * B[i];
        }

    sw.Stop();
    Console.WriteLine("/ + -*: " + sw.ElapsedTicks / 1000);
}

这里需要注意的是,这是一项真实世界的评估。如果我们要进行峰值性能测试,我们会实现矩阵向量乘积。没有人会争辩说,在实际应用程序中,人们首先必须从RAM甚至硬盘中收集操作数。然而,这会使评估更加困难和不准确,因为RAM和硬盘的访问时间比操作本身要长得多。在这种情况下,需要独立测量RAM或硬盘的访问时间,并将其添加到本次评估的测量结果中。

基准测试

每个基准测试都包含5个基本操作和2个组合操作。执行了以下基准测试:

  • C#,AnyCPU,调试模式
  • C#,AnyCPU,发布模式
  • C#,x64,发布模式
  • C#,x86,发布模式
  • C++.NET AnyCPU,调试模式 (/CLI)
  • C++.NET AnyCPU,调试模式 (/MSIL)
  • C++.NET x64,发布模式
  • C++.NET x86,发布模式
  • C++ (MinGW) for x64
  • C++ (MS) for x64
  • Fortran (MinGW) for x64
  • Java SDK7-x64 Release

所有测量均以 106 周期为单位进行,或以 10-3 秒(毫秒或简称 ms)为单位进行。使用后者进行的测量已使用以下公式重新计算为 106 计时单位(其中 f 为 CPU 的时钟频率,即每秒周期数):

TTicks=f * TSeconds / 1000.

通过使用Tcks / s(即cycles / ms),我们还可以使用TMilliseconds来计算以cycles为单位的时间。由于我们想比较106个cycles,我们必须将最终结果除以106。这106个cycles然后等同于103个Ticks或以千为单位的Ticks。第一轮评估的最终结果如下

+ - * / % / + % / + - *
C# 任何CPU,调试 19114 18686 19599 18916 18833 29893 37223
任何CPU,发布 2546 2638 2627 12341 12260 24269 12054
x86,发布 7743 7846 7630 12355 12072 26073 18906
x64,发布 2574 2555 2592 12486 12286 24324 12084
C++ 任意CPU,/CLI,调试 11406 11795 11629 14681 14779 24347 31903
任意CPU,/MSIL,调试 11563 11480 11514 14896 14698 24643 26446
x86,发布 3156 3337 3286 12575 12102 24939 12628
x64,发布 3850 3883 3969 13003 12846 24716 12855
x64, mingw 12522 12768 12773 15598 14572 24468 34050
x64,MS 3195 3196 3462 12828 12257 12132 12600
Fortran x64, mingw 9324 9141 9361 12980 13857 26253 23328
Java jsdk7-x64 4929 4964 2800 12466 12565 12483 13057
最佳 2546 2555 2592 12341 12072 12132 12054

根据数据,我得出以下结论

  • 调试模式确实很慢——这是由于Visual Studio Hostprocess附加到每个操作造成的。
  • C# 是一种真正受益于 64 位的语言——与 x86 相比,编译器似乎能够提高 x64 的速度。
  • 在我的x64系统上,x64和AnyCPU之间没有区别。因此,除非你想将程序限制在特定的体系结构或框架上,否则编译为AnyCPU似乎是最佳选择。一个有意义的例子是,即使在x64系统上,如果你想使用.NET Framework的x86版本。
  • GNU编译器表现不佳。这似乎证实了GNU编译器不适合高性能计算的普遍刻板印象。
  • 一些智能编译器也存在——这可以从以下事实看出:朴素的(/ + %)操作与优化的版本(/ + - *)需要相同的周期。这只有在前一个操作被自动翻译成后一个操作时才可能。Java编译器和Microsoft(C++)编译器在这方面做得非常好。
  • 在调试模式下,唯一重要的数字是操作次数,因为Visual Studio Hostprocess的附加成本高于操作本身。这意味着每个操作的成本大致相同(并且高于操作本身),并与操作次数成比例。
  • C# 证明自己非常稳健,几乎在所有方面都取得了胜利。

数据产生了以下图表

The first evaluation containing C#, C++, Fortran and Java

值得注意的是,我还对所有主流浏览器进行了多次 JavaScript 基准测试

  • V8中的JavaScript(Chrome 15)
  • Chakra 中的 JavaScript (IE 9)
  • Carakan 中的 JavaScript (Opera 11.52)
  • SpiderMonkey 中的 JavaScript (Firefox 8)
  • Nitro中的JavaScript(Safari 5.1.1)
+ - * / % / + % / + - *
JavaScript V8 (Chrome) 37274 21633 20543 32485 41556 58794 45791
Chakra (IE) 38424 38103 122998 143617 53781 231479 314839
Carakan (Opera) 66330 66426 68589 79509 68092 137450 165267
SpiderMonkey (Firefox) 49405 51231 51868 80634 195912 222444 105849
Nitro (Safari) 52014 51508 86761 69356 52750 99713 148745
最佳 37274 21633 20543 32485 41556 58794 45791

根据数据,我得出以下结论

  • 所有JavaScript引擎的性能都比C++差。
  • 与其它引擎相比,编译为字节码给Google Chrome的V8引擎带来了巨大的提升。
  • 其他引擎的性能与调试模式下其他语言的性能相同——这是由于解释过程造成的。浏览器附着在JavaScript代码上,因此在每次操作之前和之后执行一个解释步骤。这意味着操作本身并不重要——只有操作的数量才重要。
  • 一个普遍的说法可能是:如果你需要在做一些优化但操作更多的事情和天真(或优化程度较低)的事情之间做出选择,你应该坚持后者。
  • IE引擎在乘法和除法方面确实存在一些问题。取模运算不会出现这个问题。
  • 取模似乎以一种特殊的方式实现,因为除了Firefox之外的所有解释引擎都显示出比除法更好的结果。

数据产生了以下图表

The second evaluation containing JavaScript on all major browsers

此外,我有机会使用Intel提供的高性能编译器进行另一组有趣的测试。这些测试在另一台(专用)机器上运行,因此将这些结果与上述基准测试的结果进行比较是不公平的。这些基准测试包括

  • Fortran (Intel) for x64
  • C++ (Intel) for x86
  • C++ (Intel) for x64
  • C++ (MS) for x86
  • C++ (MS) for x64

所有这些额外的基准测试都以 **O2** 优化级别执行。这对 Fortran 也非常有效,因为 **O3** 总是崩溃,并且并行模式或 SSE3 优化模式被证明比纯粹的 **O2** 优化还要慢。

+ - * / % / + % / + - *
Fortran x64 - Intel 1976 2025 1975 2024 2026 1973 2987
C++ x86 - 微软 3045 3107 2974 8450 8449 8427 8594
x64 - 微软 2469 2422 2441 8359 8357 8395 8390
x86 - 英特尔 1852 1739 1876 9529 10156 18861 11644
x64 - Intel 1724 1723 1586 8196 8597 17427 10235
最佳 1724 1723 1586 2024 2026 1973 2987

根据数据,我得出以下结论

  • Fortran 显然做了很多很酷的优化。
  • 虽然 Fortran 在加法、减法和乘法等简单操作上似乎较慢,但在更高级或组合操作方面无疑更优越。
  • 英特尔似乎更了解(他们的)硬件(啊,真的吗?!),而微软似乎更了解(他们的)软件,即编译器或编程语言。这可以通过观察微软编译器为了使朴素操作(/ + %)与优化操作(/ + - *)达到相同水平所做的优化来体现。

数据产生了以下图表

The third evaluation containing a short evaluation of Intel and Microsoft compilers

评估

用于主要基准测试的计算机具有以下规格

  • 英特尔酷睿2双核CPU
  • 时钟频率 2343808 kHz
  • 6 GB内存
  • Windows 7 x64

安装了英特尔编译器的另一台电脑有以下规格

  • 英特尔酷睿2双核CPU
  • 时钟频率 3247109 kHz
  • 8 GB内存
  • Windows 7 x64

我使用以下环境来评估不同的代码文件

  • 所有 C++.NET、使用 Microsoft 编译器的 C++、使用 Intel 编译器的 C++ / Fortran 以及 C# 评估均使用带有 Service Pack 1 的 Microsoft Visual Studio 2010 完成。
  • 使用 MinGW 编译器编译的 C++ / Fortran 已通过命令行编译。
  • Java已使用IntelliJ IDEA进行编译和评估。

出于评估目的,我将原始C#代码重写为其他平台。下面代码片段中概述了不同语言和平台(.NET、Windows、浏览器等)的主要差异

Random类作为随机数生成器方面,C++.NET版本与C#版本非常接近。此外,还可以使用Stopwatch类。

#include "stdafx.h"

#define SIZE 100000
#define REPEAT 10000

using namespace System;
using namespace System::Diagnostics;

int main(array<System::String ^> ^args)
{
    int* A = new int[SIZE];
    int* B = new int[SIZE];
    int* C = new int[SIZE];

    //Initializes the random number generator with a seed
    Random^ ran = gcnew Random(0);

    //Filling the vectors with random numbers
    for (int i = 0; i < SIZE; i++)
    {
        A[i] = ran->Next();
        B[i] = ran->Next();
    }

    //Create new instance of custom Stopwatch class (managed instance and pointer!)
    Stopwatch^ sw = gcnew Stopwatch();

    sw->Start();
    for (int j = 0; j < REPEAT; j++)
        for (int i = 0; i < SIZE; i++)
            C[i] = A[i] + B[i];

    sw->Stop();
    Console::WriteLine(L"+:       " + sw->ElapsedTicks / 1000);
    sw->Reset();

    /* More evaluations */
}

从C++.NET到C++ (Windows API)的过渡并不复杂。然而,有必要使用一个自定义类来模拟.NET-Stopwatch类的行为。我编写了自己的Stopwatch,其中包含了原始版本的所有方法,并使用Windows API来检索当前的计时周期。

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <iostream>

#define SIZE 100000
#define REPEAT 10000

using namespace std;

class Stopwatch
{
    LARGE_INTEGER start;
    LARGE_INTEGER end;

    public:
        Stopwatch()
        {
            LARGE_INTEGER frequency;
            QueryPerformanceFrequency(&frequency);
            cout << "Frequency: " << frequency.QuadPart << 
                        " Ticks/s" << endl;
            Reset();
        }

        long ElapsedTicks;

        void Reset()
        {
            ElapsedTicks = 0;
        }

        void Stop()
        {
            QueryPerformanceCounter(&end);
            ElapsedTicks = end.QuadPart - start.QuadPart;
        }

        void Start()
        {
            QueryPerformanceCounter(&start);
        }
};

int main()
{
    int* A = new int[SIZE];
    int* B = new int[SIZE];
    int* C = new int[SIZE];

    //Initializes the random number generator with a seed
    srand(0);

    //Filling the vectors with random numbers
    for (int i = 0; i < SIZE; i++)
    {
        A[i] = rand() + 1;
        B[i] = rand() + 1;
    }

    //Create new instance of custom Stopwatch class
    Stopwatch* sw = new Stopwatch();

    sw->Start();
    for (int j = 0; j < REPEAT; j++)
        for (int i = 0; i < SIZE; i++)
            C[i] = A[i] + B[i];

    sw->Stop();
    cout << "+:       " << sw->ElapsedTicks / 1000 << endl;
    sw->Reset();

    /* More evaluations */
}

Java(出乎我意料)也没有包含一个可用的Stopwatch类。这可能是有道理的,因为Java也不了解Ticks,因为Java旨在实现平台独立性,而Ticks主要用于Windows系统。所以我使用了一个已经构建好的Stopwatch实例,并对其进行了扩展,使其能够自动转换为Ticks(此转换特定于**我的计算机**——因此如果你想使用它,你应该将代码中的行更改为你的每秒Ticks频率或通用时钟频率/1000)。

public class Stopwatch {
    private long startTime = 0;
    private long stopTime = 0;
    private boolean running = false;

    public void start() {
        this.startTime = System.currentTimeMillis();
        this.running = true;
    }

    public void stop() {
        this.stopTime = System.currentTimeMillis();
        this.running = false;
    }

    public void reset() {
        startTime = 0;
        stopTime = 0;
    }

    //elaspsed time in milliseconds
    public long getElapsedTime() {
        long elapsed;
        if (running) {
             elapsed = (System.currentTimeMillis() - startTime);
        }
        else {
            elapsed = (stopTime - startTime);
        }
        return elapsed;
    }

    public long getElapsedTicks() {
        //This is specific to my computer which has 2343808 Ticks / s
        double frequency = 2343808;
        return (long)((((double)getElapsedTime()) / 1000.0) * frequency);
    }

    //elaspsed time in seconds
    public long getElapsedTimeSecs() {
        long elapsed;
        if (running) {
            elapsed = ((System.currentTimeMillis() - startTime) / 1000);
        }
        else {
            elapsed = ((stopTime - startTime) / 1000);
        }
        return elapsed;
    }
}

使用自定义Stopwatch类,我可以编写以下程序

import java.util.Random;

public class Main
{
    final static int SIZE = 100000;
    final static int REPEAT = 10000;

    public static void main(String[] args)
    {
        //Initializes the random number generator with a seed
        Random ran = new Random(0);

        int[] A = new int[SIZE];
        int[] B = new int[SIZE];
        int[] C = new int[SIZE];

        //Filling the vectors with random numbers
        for (int i = 0; i < SIZE; i++)
        {
            A[i] = ran.nextInt();
            B[i] = ran.nextInt();
        }

        //Create new instance of custom Stopwatch class
        Stopwatch sw = new Stopwatch();

        sw.start();
        for (int j = 0; j < REPEAT; j++)
            for (int i = 0; i < SIZE; i++)
                C[i] = A[i] + B[i];

        sw.stop();
        System.out.println("+:       " + sw.getElapsedTicks() / 1000);
        sw.reset();

        /* More evaluations */
    }
}

最大的改变无疑是使用了Fortran。Fortran 的数组索引不是从0开始,而是从1开始。此外,Fortran 使用常规(圆括号)而不是方括号来引用数组的索引。为了向后兼容,Fortran 需要以原始方式编写,即使用非常古老的打孔卡编程风格。这意味着程序必须满足某些要求,例如前六列由编译器特殊处理。此外,行的最大长度不能超过80列,其中最后8列保留用于注释。

      PROGRAM MEASUREMENT
      INTEGER s
      INTEGER A (100000), B (100000), C (100000)
      INTEGER r
      INTEGER i, j
      s = 100000
      r = 10000

C     Those two are being used for the relative time measurements
      REAL TIMEARRAY (2)
      REAL DELAPSE

C     Initializes the random number generator with a seed
      CALL srand(0)

C     Filling the vectors with random numbers
      do i = 1, s
        A(i) = 10 * rand(0) + 1
        B(i) = 10 * rand(0) + 1
      enddo

      DELAPSE = DTIME(TIMEARRAY)
      do j = 1, r
        do i = 1, s
          C(i) = A(i) + B(i)
        enddo
      enddo

      DELAPSE = DTIME(TIMEARRAY)
      WRITE (*,*) '+:    ', DELAPSE

C     More evaluations

      END PROGRAM

用JavaScript编写评估代码相当快。与Java一样,JavaScript也没有提供特定的例程来测试时间。测试时间可能会导致相当不准确的结果——取决于浏览器。然而,考虑到测试的长度,很明显,在这种情况下,时间测量中的delta(如(较旧的?)Internet Explorer的15毫秒)不会有太大影响,因为时间肯定会以秒(甚至分钟)为单位。

var SIZE = 100000;
var REPEAT = 10000;

var A = new Array(SIZE);
var B = new Array(SIZE);
var C = new Array(SIZE);

// Filling the vectors with random numbers
for (var i = 0; i < SIZE; i++)
{
    A[i] = parseInt(Math.floor(Math.random() * 100001)) + 1;
    B[i] = parseInt(Math.floor(Math.random() * 100001)) + 1;
}

// Runs a benchmark for a certain function f
function benchmark(f, id) {
    // Starttime
    var start = (new Date).getTime();
    f();
    // Difference in time
    var diff = (new Date).getTime() - start;
    // Write result to HTML-Document using the provided id
    document.getElementById(id).innerHTML = diff;
}

function plus() {
    for (var j = 0; j < REPEAT; j++)
        for (var i = 0; i < SIZE; i++)
            C[i] = A[i] + B[i];
}

/* More evaluations */

应该指出,JavaScript 必须在之后手动转换为 Ticks / s。如果您运行此评估,那么请构建使用以下类似内容或在之后进行转换

/* Code from above ... */

/* Stays pretty much the same */
function benchmark(f, id) {
    var start = (new Date).getTime();
    f();
    var diff = (new Date).getTime() - start;
    // Now this is different! -- this one could be compared 
    // with the output of the others
    document.getElementById(id).innerHTML = parseInt(time2Ticks(diff) / 1000);
}

/* New Function! */
function time2Ticks(msec) {
    var frequency = 2343808;
    return msec / 1000 * frequency;
}

Using the Code

代码可以不受任何限制地使用。如果您有改进建议或进一步评估的想法,欢迎在此评论。需要注意的是,我曾考虑在Linux上进行相同的评估,但后来放弃了,因为我认为测试一个全新系统与一个已经运行多年且在后台加载了许多服务和程序的系统进行比较是不公平的。此外,Microsoft编译器在Linux上不可用,这将导致在此范围内进行不同的评估。对于C#,我会使用Mono实现。但是,欢迎您在Linux机器上进行类似的评估并告知我结果。只要结果以每操作周期或每操作计时单位表示,它就具有一定的可比性(再次强调:导致比较不公平的原因是系统的后台负载,即在(基准测试)程序运行时需要一些周期来执行的程序和服务)。

为了运行 Fortran,我们使用了以下编译命令(这显然是在我们的代码上执行 **O2** 优化)

$ ifort base.f /check:bounds

所有必需的代码文件都包含在source.zip文件中。

奖励:脚本语言

脚本语言领域还可以进行另一项有趣的评估。这里我包含了以下语言:

  • Perl (ActiveStatePerl for Windows 5.14.2 x64)
  • PHP (Windows 版本 5.3.1,包含在 XAMPP 中)
  • Python (适用于 Windows 的 2.5.4 版本)

首先:请注意,Python的版本显然已经过时了。我不知道更新的版本是否提供更好的性能。我没有听说过更好的性能,但如果我有一天有时间更改我电脑上的Python安装,我肯定会再进行一次评估。接下来要注意的是,我所有的这些基准测试都是在Windows上运行的。PHP在Windows上的速度越来越快,但是,我认为它在Linux上仍然比在Windows上快。这显然也适用于其他两种脚本语言,因为它们也起源于Linux操作系统。

Evaluation of the three major scripting languages

由于各种因素,PHP似乎比Perl快。一方面,它是Perl的一个专用版本;另一方面,它在过去十年中一直得到定期维护。值得注意的是,这次评估中存在一定的Windows因素。我很想看到在Linux上进行的这次评估(我将来要么自己做,要么等待有人发布可靠的结果)。无论如何,脚本语言的速度显然非常慢。我原以为它们可能比JavaScript还慢,但20倍的差距相当令人惊讶。同样值得注意的是,所有这些语言都遵循了其他脚本语言的模式,即操作之间包含昂贵的解释步骤。

总而言之,我认为这种评估有点不公平。脚本语言从来就不应该关心操作——它们应该为快速而粗糙的编程提供可靠且易于使用的来源。评估是不公平的,因为在我看来,for循环太重,需要执行太多操作。前面介绍的语言(即本文的原始目标)对这种循环有特殊处理。因此,与操作相比,执行循环本身所花费的时间可以忽略不计。这就是我们想要达到的目标。现在情况并非如此。我相信所有语言都与执行的操作数量成正比——包括条件、循环计数器等。

如果您仔细观察以下数字,就会发现这一点

+ - * / % / + % / + - *
脚本语言 Python 913839 945178 976808 942330 989024 1572805 2200857
Perl 787519 796895 794551 803926 820333 1335971 1999268
PHP 637516 637516 639860 665641 672673 1024244 1258625
最佳 637516 637516 639860 665641 672673 1024244 1258625

PHP 提供了最好的值,它与操作数量线性相关。我们有三到六个主要操作(内循环计数器递增、内循环条件以及操作数量,范围从原子操作如 **+** 到复杂操作 ** / + - ***)。因此,每个操作大约需要200个计算周期,这大约是 C# 的70倍。Perl 和 Python 内部可能会执行一些其他操作,这就是为什么线性行为不适用于此。但是,我们确实看到操作数量与计算时间成比例,这仍然是这些语言的特征。在这里,以简单的方式编写代码比尝试通过在更复杂的算法中使用更多操作来挤压计算机性能要好。

更新:JavaScript 性能

我对我的第一次JavaScript性能评估有点失望。浏览器厂商们对他们产品的新JavaScript可能性宣传得相当热情。然而,与C++相比,实际性能非常慢(除了Google的V8引擎)。现在,在对最流行的脚本语言进行这些基准测试之后,我改变了我的看法。由于JavaScript仍然(再次强调:除了Google的V8引擎之外)是一种脚本语言,因此浏览器厂商们能够挤出如此多的性能是值得称赞的。根据报告,他们仍在努力改进,以提供更快的JavaScript。

因此,我借此机会仔细研究了五种最受欢迎浏览器的最新版本(与上述列表比较)

  • V8 中的 JavaScript (Chrome **16**)
  • Chakra 中的 JavaScript (IE **10** PP2,因为 PP4 仅适用于 Windows 8 DP)
  • Carakan 中的 JavaScript (Opera 11.**6**)
  • SpiderMonkey 中的 JavaScript (Firefox **9**)
  • Nitro中的JavaScript(Safari 5.1.**2**)

这里的粗体数字表示有变动。由于另一张数据表篇幅过大,我只强调变动

  • V8 仍在不断优化。除最后一次(组合)基准测试外,所有评估结果保持不变。Google Chrome 16 在此节省了约 25% 的计算时间。我猜编译器现在能识别某些模式并对其进行优化。
  • IE 从第9版发布版本到第10版平台预览版完全没有变化。我认为大部分性能提升并非发生在JavaScript中,而是在DOM操作和DOM渲染过程中。
  • Opera 没有任何变化。
  • 火狐取得了**巨大**的飞跃。更多内容请看下文!
  • Safari保持不变——也许稍微慢了一点,但差异不显著。

Firefox 9 没有包含任何引人注目的功能——人们可能会这么认为。然而,在表面之下,Mozilla 的开发者们似乎在改进 JavaScript 引擎方面做得相当出色。Firefox 9 现在执行 JavaScript 操作的速度大约是 Firefox 8 的两倍。第一个基准测试(加法)甚至比 Google V8 更快,是这次 JavaScript 基准测试中唯一一个不叫 Google V8 的赢家。Firefox 的数据(完整数据可在附带的表格中查看)

+ - * / % / + % / + - *
Mozilla Firefox 9 23956 24385 29424 55152 39906 76924 83547
8 49405 51231 51868 80634 195912 222444 105849
9到8的比例 48% 48% 57% 68% 20% 35% 79%

Mozilla 团队在改进他们的 JavaScript 引擎方面做得非常出色。在接下来的图表中,我们也可以看到 Google 仍在努力改进他们的 JavaScript 引擎 V8。未来几年 JavaScript 将被推向何种领域将很有趣。下一个图表包含当前性能与先前性能的比率,即低于 100% 的值表示改进。围绕 100% 的值通常在性能上是等效的,因为评估的一小部分取决于计算机的当前状态和其他因素。

Improvements on the JavaScript engines of the five most popular browsers

结论

总的来说,可以肯定地说,减法等于加法,也等于乘法。除法大约是加法的五倍,并等于取模运算。Fortran 更有效地执行除法和取模运算,从而实现更快的执行。

通常,操作的优化版本(% + - *)应优先于原始版本(% + / )。此说法对于解释型脚本语言不适用,因为在解释型脚本语言中,由于解释步骤,操作成本要高得多。因此,在JavaScript中,通过最直接的解决方案可以获得最佳性能——跳过在其他语言中会导致优化的操作。

关注点

用所有这些不同的语言编写测试程序非常有趣。我很久没有编写 Fortran 了,并且惊讶于这种语言本身仍然感觉非常古老和过时。然而,在某些关键领域,例如除法,其性能仍然无与伦比。我无法想象这只能在 Fortran 编译器中实现,所以我想知道为什么其他编译器还没有类似的功能。

我对Google的V8 JavaScript引擎的强大性能也感到惊讶。当Chrome发布时,我用了一段时间,直到我发现自己爱上了Opera的独立浏览器。现在这项性能测试表明,开发者们在V8的核心功能——直接将提供的JavaScript代码编译成字节码——上并没有撒谎。当然,这对于网页来说非常酷,但是应该注意的是,算术操作时间通常不是JavaScript代码的关键方面,因为JavaScript并不用于高性能计算。JavaScript代码中的大多数操作是DOM操作或与浏览器的其他交互。因此,这项基准测试也并不能说明Google Chrome是最好的浏览器或最快的JavaScript浏览器。尽管如此,我仍然认为Chrome在DOM操作方面表现也不会太差。

参考文献

我发现以下链接很有用

  • StackOverflow 上关于使用 Windows API 获取 tickcount 的文章。[^]
  • Corey Goldberg 的 Java Stopwatch 类。[^]
  • 在 Fortran 中进行相对计时。[^]
  • 如何在 Fortran 中测量绝对时间。[^]
  • John Resig 谈 JavaScript 计时的准确性。[^]

历史

  • v1.0.0 | 首次发布 | 2011年12月23日
  • v1.1.0 | 更新(包含主要脚本语言和JavaScript更新) | 2012年1月17日
© . All rights reserved.