Java似乎比C++更快地执行基本算法。为什么?

6

介绍:

我使用两个相同的归并排序算法,测试了C++(使用Visual Studios C ++ 2010 express)和Java(使用NetBeans 7.0)的执行速度。我猜想C++的执行速度至少会稍微快一点,但测试结果显示C++的执行速度比Java慢4-10倍。我相信我已经为C++设置了所有速度优化,并且我正在发布而不是作为调试。这种速度差异为什么会发生?

代码:

Java:

public class PerformanceTest1
{
 /**
  * Sorts the array using a merge sort algorithm
  * @param array The array to be sorted
  * @return The sorted array
  */
 public static void sort(double[] array)
 {
      if(array.length > 1)
      {
           int centre;
           double[] left;
           double[] right;
           int arrayPointer = 0;
           int leftPointer = 0;
           int rightPointer = 0;

           centre = (int)Math.floor((array.length) / 2.0);

           left = new double[centre];
           right = new double[array.length - centre];

           System.arraycopy(array,0,left,0,left.length);
           System.arraycopy(array,centre,right,0,right.length);

           sort(left);
           sort(right);

           while((leftPointer < left.length) && (rightPointer < right.length))
           {
                if(left[leftPointer] <= right[rightPointer])
                {
                     array[arrayPointer] = left[leftPointer];
                     leftPointer += 1;
                }
                else
                {
                     array[arrayPointer] = right[rightPointer];
                     rightPointer += 1;
                }
                arrayPointer += 1;
           }
           if(leftPointer < left.length)
           {
                System.arraycopy(left,leftPointer,array,arrayPointer,array.length - arrayPointer);
           }
           else if(rightPointer < right.length)
           {
                System.arraycopy(right,rightPointer,array,arrayPointer,array.length - arrayPointer);
           }
      }
 }

 public static void main(String args[])
 {
      //Number of elements to sort
      int arraySize = 1000000;

      //Create the variables for timing
      double start;
      double end;
      double duration;

      //Build array
      double[] data = new double[arraySize];
      for(int i = 0;i < data.length;i += 1)
      {
           data[i] = Math.round(Math.random() * 10000);
      }

      //Run performance test
      start = System.nanoTime();
      sort(data);
      end = System.nanoTime();

      //Output performance results
      duration = (end - start) / 1E9;
      System.out.println("Duration: " + duration);
 }
}

C++:

#include <iostream>
#include <windows.h>
using namespace std;

//Mergesort
void sort1(double *data,int size)
{
if(size > 1)
{
    int centre;
    double *left;
    int leftSize;
    double *right;
    int rightSize;
    int dataPointer = 0;
    int leftPointer = 0;
    int rightPointer = 0;

    centre = (int)floor((size) / 2.0);
    leftSize = centre;
    left = new double[leftSize];
    for(int i = 0;i < leftSize;i += 1)
    {
        left[i] = data[i];
    }
    rightSize = size - leftSize;
    right = new double[rightSize];
    for(int i = leftSize;i < size;i += 1)
    {
        right[i - leftSize] = data[i];
    }

    sort1(left,leftSize);
    sort1(right,rightSize);

    while((leftPointer < leftSize) && (rightPointer < rightSize))
    {
        if(left[leftPointer] <= right[rightPointer])
        {
            data[dataPointer] = left[leftPointer];
            leftPointer += 1;
        }
        else
        {
            data[dataPointer] = right[rightPointer];
            rightPointer += 1;
        }
        dataPointer += 1;
    }
    if(leftPointer < leftSize)
    {
        for(int i = dataPointer;i < size;i += 1)
        {
            data[i] = left[leftPointer++];
        }
    }
    else if(rightPointer < rightSize)
    {
        for(int i = dataPointer;i < size;i += 1)
        {
            data[i] = right[rightPointer++];
        }
    }
            delete left;
            delete right;
}
}

void main()
{
//Number of elements to sort
int arraySize = 1000000;

//Create the variables for timing
LARGE_INTEGER start; //Starting time
LARGE_INTEGER end; //Ending time
LARGE_INTEGER freq; //Rate of time update
double duration; //end - start
QueryPerformanceFrequency(&freq); //Determinine the frequency of the performance counter (high precision system timer)

//Build array
double *temp2 = new double[arraySize];
QueryPerformanceCounter(&start);
srand((int)start.QuadPart);
for(int i = 0;i < arraySize;i += 1)
{
    double randVal = rand() % 10000;
    temp2[i] = randVal;
}

//Run performance test
QueryPerformanceCounter(&start);
sort1(temp2,arraySize);
QueryPerformanceCounter(&end);
    delete temp2;

//Output performance test results
duration = (double)(end.QuadPart - start.QuadPart) / (double)(freq.QuadPart);
cout << "Duration: " << duration << endl;

//Dramatic pause
system("pause");
}

观察结果:

当元素数量为10000时,C++执行时间大约是Java执行时间的4倍。 当元素数量为100000时,比率约为7:1。 当元素数量为10000000时,比率约为10:1。 当元素数量超过10000000时,Java执行完成,但C++执行停滞不前,需要手动终止进程。


43
我看到你的 C++ 代码中有 3 个 new[],但是没有 delete[]。你的内存即将耗尽。在你想要比较它们之前,学会如何正确使用两种语言。 - Xeo
16
啊!void main!我的眼睛!它们着火了! - Etienne de Martel
3
是的,糟糕的内存管理就是答案。 - CrazyDart
1
我同意Xeo的观点。为了更好的速度,避免使用new/delete并转向inplace merge。 - Peter G.
13
不公平将平庸的Java与写得糟糕的C++进行比较。 - Martin York
显示剩余16条评论
8个回答

16
我认为你在运行程序时可能犯了一个错误。当你在Visual C++ Express中按F5时,程序将在调试器下运行,速度会慢很多。在其他版本的Visual C++ 2010(比如我用的Ultimate版本)中,请尝试按CTRL+F5(即“开始运行而不带调试”),或者尝试直接运行可执行文件本身(在Express中),你会发现这种方式的差异。
我在我的机器上运行了你的程序,并进行了一次修改(添加了delete[] left; delete[] right;以消除内存泄漏;否则在32位模式下会耗尽内存!)。我有一台i7 950电脑。为了公平起见,我还将同样大小的数组传递给了Java中的Arrays.sort()和C++中的std::sort。我使用了一个大小为10,000,000的数组。
以下是结果(时间以秒为单位):
Java代码:            7.13
Java Arrays.sort:     0.93
32位模式 C++代码: 3.57 C++ std::sort 0.81
64位模式 C++代码: 2.77 C++ std::sort 0.76
因此,C++代码更快,即使是高度调整过的标准库,在Java和C++中都表现出对C++的轻微优势。
编辑:我刚意识到在你的原始测试中,你是在调试模式下运行C++代码。你应该切换到“Release”模式,并在不带调试器的情况下运行它(正如我在我的帖子中所解释的那样),以获得公正的结果。

天啊!Ctrl + F5 将执行速度提高了一个数量级以上!谢谢! - SpeedIsGood
3
@SpeedIsGood:我很高兴它有所帮助。注意你的代码和库代码之间存在很大的差距(无论是 Java 还是 C++)。这意味着在两种语言中都有许多可以改进性能的方法。我不知道 Java Arrays.sort,但 std::sort 完全是用 C++ 编写的,而且也是通用的!其他评论中已经提出了一些改进方法。 - AlefSin

10

我并不是职业(甚至业余)的C++程序员,但我注意到你在堆上分配了一个double类型的变量(double *temp2 = new double[arraySize];)。相比Java的初始化方法,这种方式代价更高,更重要的是,它造成了内存泄漏,因为你从未删除它,这可能解释了为什么你的C++实现会停顿,基本上是因为内存耗尽了。


5
首先,您尝试使用std::sort(或通常是mergesort的std::stable_sort)在C++中获得基准性能吗?
我无法评论Java代码,但对于C++代码:
- 与Java不同,C++中的new需要手动释放内存。每次递归都会泄漏内存。我建议改用std::vector,因为它可以为您管理所有内存,并且iterator,iterator构造函数甚至可以执行复制(可能比您的for循环优化更好)。这几乎肯定是性能差异的原因。 - 在Java中使用arraycopy,但在C++中不使用库设施(std::copy),尽管如果使用vector则无关紧要。 - 小细节:在函数中首次需要它们的位置上声明并初始化变量,而不是在函数顶部全部声明。 - 如果允许使用标准库的某些部分,则std::merge可以替换您的合并算法。
编辑:如果您真的使用delete left;清理内存,那可能就是问题所在。正确的语法应该是delete [] left;来释放数组。

  1. 我似乎在我的帖子中省略了那部分。它在我的原始代码中存在。我刚刚用删除更新了我的帖子。
  2. 我会尝试。谢谢。
  3. 当然。
  4. 实际上,我并没有执行合并操作;我只是在测试C++与Java的执行时间。
- SpeedIsGood
@SpeedIsGood 但请注意我的编辑,涉及到数组删除语法(更不用说数据复制等问题了)。 - Mark B
存在越界问题:data[data.size()] - Heng Li

4

您的版本泄漏了太多内存,导致时间无意义。

我确定时间都花在了折磨内存分配器上。
重写它,使用标准的C++对象进行内存管理std::vector,并查看效果如何。

个人认为Java版本仍然会获胜(仅限微弱优势)。因为JIT允许机器特定的优化,而虽然C++可以进行机器特定的优化,但通常只会进行通用架构的优化(除非提供确切的架构标志)。

  • 注意:不要忘记打开优化编译选项。

只是清理一下您的C++代码:
我没有尝试制作一个好的归并排序(只是重新编写了一下),以符合C++的风格。

void sort1(std::vector<double>& data)
{
    if(data.size() > 1)
    {
        std::size_t         centre    = data.size() / 2;
        std::size_t         lftSize   = centre;
        std::size_t         rhtSize   = data.size() - lftSize;

        // Why are we allocating new arrays here??
        // Is the whole point of the merge sort to do it in place?
        // I forget bbut I think you need to go look at a knuth book.
        //
        std::vector<double> lft(data.begin(),           data.begin() + lftSize);
        std::vector<double> rht(data.begin() + lftSize, data.end());

        sort1(lft);
        sort1(rht);
        std::size_t dataPointer   = 0;
        std::size_t lftPointer    = 0;
        std::size_t rhtPointer    = 0;

        while((lftPointer < lftSize) && (rhtPointer < rhtSize))
        {                                                                               
            data[dataPointer++] = (lft[lftPointer] <= rht[rhtPointer])
                                    ?  lft[lftPointer++]
                                    :  rht[rhtPointer++];
        }
        std::copy(lft.begin() + lftPointer, lft.end(), &data[dataPointer]);
        std::copy(rht.begin() + rhtPointer, rht.end(), &data[dataPointer]);
    }
}

思考归并排序。我会尝试这样做:
我还没有测试过它,所以它可能不能正确地工作。但这是一种尝试,不需要分配大量内存来进行排序。相反,它使用单个临时区域,并在排序完成后将结果复制回去。

void mergeSort(double* begin, double* end, double* tmp)
{
    if (end - begin <= 1)
    {   return;
    }

    std::size_t size    = end - begin;
    double*     middle  = begin +  (size / 2);

    mergeSort(begin, middle, tmp);
    mergeSort(middle, end, tmp);

    double* lft    = begin;
    double* rht    = middle;
    double* dst    = tmp;
    while((lft < middle) && (rht < end))
    {
        *dst++  = (*lft < *rht)
                        ? *lft++
                        : *rht++;
    }
    std::size_t count   = dst - tmp;
    memcpy(begin,          tmp, sizeof(double) * count);
    memcpy(begin + count,  lft, sizeof(double) * (middle - lft));
    memcpy(begin + count,  rht, sizeof(double) * (end    - rht));
}

void sort2(std::vector<double>& data)
{
    double*     left    = &data[0];
    double*     right   = &data[data.size()];

    std::vector<double> tmp(data.size());

    mergeSort(left,right, &tmp[0]);
}

谢谢,我会测试一下。然而,我对优化(或大部分C++)并不是很熟悉,你可以猜到。我想我已经在Visual Studios C++中启用了/O2优化。我还能做什么来优化我的代码? - SpeedIsGood
我有一个关于向量的问题。作为一个完全的C++新手,我寻求了一些关于执行速度的资料,其中一个告诉我,由于C++显然不能像Java那样处理泛型,使用向量的代码将不会像使用双指针的代码那样快速执行。这是真的吗?谢谢。 - SpeedIsGood
3
在使用任何现代编译器的发布模式下,通过double*或std::vector<double>访问数据的速度将完全相同。请注意,Java泛型和C++模板非常不同。模板比泛型提供了更多的功能(例如元编程和表达式模板),使它们非常适合构建高性能数值代码。 - AlefSin
@AlefSin:谢谢,这真的很有用。向量非常实用。 - SpeedIsGood

2
一些重要的事情。
Java经过高度优化,一旦代码执行一次后,JIT编译器就会将代码作为本地代码执行。
在Java中,System.arraycopy要比单独复制每个元素快得多。尝试用memcpy替换此复制,您将看到它运行更快。
编辑: 请查看此帖子:C++ performance vs. Java/C#

3
有几个人提到使用memcpy或类似的东西;如果数组是基本类型,我会期望好的C++编译器将循环优化为至少同样好的代码。 - James Kanze
链接的线程有点误导,因为尽管JIT编译器在理论上可以比静态编译器产生更快的代码,但今天这种情况并不普遍存在,至少在可表示的问题集中是如此。无论如何,在这种特定情况下,并不是性能的原因,甚至部分原因也不是。 - Konrad Rudolph
@Konrad:如果编译时目标架构不是很明确或者别名分析不够好(例如HotSpot与GNU GCC在SciMark2的LU任务上),JIT编译器可以生成比静态编译器更快的代码。 - J D
@Jon 如果架构不知道,那么您需要在现场编译C++代码,因此可以利用基于配置文件的优化。这与HotSpot执行的优化完全相同,唯一的区别是没有JIT在后台运行,因此如果配置文件数据代表性强,则始终会更快(假设配置文件数据代表性强)。几乎所有关于JITting的网络信息都已过时,因为它没有考虑到基于配置文件的优化,因为这只在Microsoft的C++编译器中“最近”可用。 - Konrad Rudolph
@ Konrad:“如果架构未知,则无论如何都需要在现场编译C ++代码。”实际上,通常不会精确地知道架构,因此将C / C ++编译为通用x86的最低公共分母,而不是针对AMD Barcelona或Intel Nehalem进行优化。相反,JIT针对当前机器进行优化。 - J D
@Jon 对于大多数情况来说,最低公共分母是可以的。但是 如果 你需要如此之快的速度以至于 HotSpot 优化会产生影响,那么你肯定会为每个目标平台单独编译。 - Konrad Rudolph

1

仅从您的代码中看是很难确定原因的,但我猜测问题可能出在处理递归上而不是实际计算。尝试使用一些依赖迭代而非递归的排序算法,并分享性能比较的结果。


0
尝试将全局向量作为缓冲区,并尽量不分配大量内存。这将比您的代码运行得更快,因为它使用了一些技巧(仅使用一个缓冲区并在程序启动时分配内存,因此内存不会被分段)。
#include <cstdio>
#define N 500001

int a[N];
int x[N];
int n;

void merge (int a[], int l, int r)
{
    int m = (l + r) / 2;
    int i, j, k = l - 1;
    for (i = l, j = m + 1; i <= m && j <= r;)
        if (a[i] < a[j])
            x[++k] = a[i++];
        else
            x[++k] = a[j++];
    for (; i <= m; ++i)
        x[++k] = a[i];
    for (; j <= r; ++j)
        x[++k] = a[j];
    for (i = l; i <= r; ++i)
        a[i] = x[i];
}

void mergeSort (int a[], int l, int r)
{
    if (l >= r)
        return;
    int m = (l + r) / 2;
    mergeSort (a, l, m);
    mergeSort (a, m + 1, r);
    merge (a, l, r);
}

int main ()
{
    int i;
    freopen ("algsort.in", "r", stdin);
    freopen ("algsort.out", "w", stdout);
    scanf ("%d\n", &n);
    for (i = 1; i <= n; ++i)
        scanf ("%d ", &a[i]);
    mergeSort (a, 1, n);
    for (i = 1; i <= n; ++i)
        printf ("%d ", a[i]);
    return 0;
}

0
我不知道为什么Java在这里运行得如此之快。
我将它与内置的Arrays.sort()进行了比较,结果又快了4倍。(它不创建任何对象)。
通常,如果Java更快的测试是因为Java更擅长删除没有任何作用的代码。
也许你可以在结尾处使用memcpy而不是循环。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接