【问题标题】:Why is my Java Merge-Sort faster than my C++ implementation?为什么我的 Java Merge-Sort 比我的 C++ 实现快?
【发布时间】:2017-05-17 10:14:48
【问题描述】:

我在 Java 和 C++ 中实现了合并排序,并尝试尽可能相似地实现它们。两种算法都有效,我对它们进行了多次测试。问题是我的 Java 实现比我的 C++ 实现快得多,我想知道为什么。我不敢相信 Java 会更快,所以我想我在其中一个实现中犯了一个错误。为了测量运行时间,我创建了一个类“Person”,它有两个字符串属性(名字、姓氏)。在 C++ 中我使用了std::vector<Person*>,在 Java 中我使用了ArrayList<Person>。此外,我在 C++ 中重载了operator< 来比较两个人(比较姓氏,如果相等则比较名字)。在 Java 中,我实现了接口Comparable<Person> 来比较两个人。

您能找到我的代码中的错误或 Java 更快或 C++ 更慢的原因吗?任何帮助将不胜感激。

我的 Java 代码:

public void mergeSort(List<T> list) {
    if (list.size() <= 1) {
        return;
    }

    int subLength = (int) (list.size() / 2);
    List<T> first = new ArrayList<T>(list.subList(0, subLength));
    List<T> second = new ArrayList<T>(list.subList(subLength, list.size()));


    mergeSort(first);
    mergeSort(second);

    merge(first, second, list);
    return;
}

private void merge(List<T> first, List<T> second, List<T> result) {
    int firstPos = 0, secondPos = 0, resultPos = 0;

    while (firstPos < first.size() && secondPos < second.size()) {
        if (first.get(firstPos).compareTo(second.get(secondPos)) < 0) {
            result.set(resultPos, first.get(firstPos));
            firstPos++;
        } else {
            result.set(resultPos, second.get(secondPos));
            secondPos++;
        }
        resultPos++;
    }

    for (int i = firstPos; i < first.size(); i++) {
        result.set(resultPos, first.get(i));
        resultPos++;
    }
    for (int i = secondPos; i < second.size(); i++) {
        result.set(resultPos, second.get(i));
        resultPos++; 
    }
}

我的 C++ 代码:

注意:我使用了两个模板方法来使合并排序可用于 Person 和 Person*。

template<typename T>
    T * ptr(T & obj) { return &obj; } 

    template<typename T>
    T * ptr(T * obj) { return obj; }


void mergeSort(std::vector<T> &list) {
        if (list.size() <= 1) {
            return;
        }

        int subLength = (int)(list.size() / 2);
        std::vector<T> first(list.begin(), list.begin() + subLength);
        std::vector<T> second(list.begin() + subLength, list.end());

        mergeSort(first);
        mergeSort(second);

        merge(first, second, list);

    }
void merge(const std::vector<T> &first, const std::vector<T> &second, std::vector<T> &result) {
        int firstPos = 0, secondPos = 0, resultPos = 0;
        while (firstPos < first.size() && secondPos < second.size()) {
            if (*ptr(first[firstPos]) < *ptr(second[secondPos])) {
                result[resultPos] = first[firstPos];
                firstPos++;
            }
            else {
                result[resultPos] = second[secondPos];
                secondPos++;
            }
            resultPos++;
        }

        for (int i = firstPos; i < first.size(); i++) {
            result[resultPos] = first[i];
            resultPos++;
        }
        for (int i = secondPos; i < second.size(); i++) {
            result[resultPos] = second[i];
            resultPos++;
        }
    }

编辑 1 和 2:

我的设置配置:

我使用了 100 万人、1000 万人和 2000 万人来测试实施。我用多少人测试它并不重要,Java 总是更快。

我通过以下方式对其进行测试:我创建人员并初始化我的MergeSort-class。然后我开始测量并调用我的mergeSort-方法。分类完成后,我停止测量。 (删除发生在时间测量之后)。对于 Java 中的时间测量,我使用 System.nanoTime(),在 C++ 中我使用 chrono::high_resolution_clock::time_point

当然,我在“发布”模式下编译了 C++(优化:O2,首选更快的代码)。

我的测试电脑:

  • 英特尔 i5 2500
  • 8GB DDR3 内存
  • Windows 10 教育版

编辑3:

有一件事我忘了提。为了使用简单的数据类型和对象,我以一种通用的方式实现了该算法。当我在 Java 中使用 std::vector&lt;int&gt; 和 ArrayList&lt;Integer&gt; 时,我的 C++ 实现要快得多。我的第一次尝试是使用std::vector&lt;Person&gt;,但速度更慢。所以我的猜测是它制作了深拷贝而不是浅拷贝,这就是我切换到Person* 的原因,因为我认为当复制发生时,只会复制指针。

【问题讨论】:

  • 您的性能测试设置是什么?你的样本量是多少?时间是怎么计算的?
  • 这可能属于代码审查。
  • 一个列表,在 C++ 中是std::list
  • 恕我直言 vector 是在这里使用的最佳容器(查看代码!)std::list 只会增加开销。 (顺便说一句 *ptr(...) 可以安全地省略)
  • @alain 仅在您超出预分配空间的情况下使用。但是在这种情况下,您可以看到 OP 没有向向量添加新元素,并且在大多数情况下,您实际上可以预测条目的总数,以便您可以创建所需长度的向量

标签: java c++ runtime mergesort


【解决方案1】:

TL;DR - Java 版本的数组复制要少得多。

ArrayList 的 ArrayList constructor (see line 167) 在传递 Collection 时使用 Collection.toArray() 和可选的 Arrays.copyOf(如果需要)。 如果是ArrayList,则无需复制 - toArray() 返回对底层数组的引用。

还要注意if (elementData.getClass() != Object[].class) 将导致数组不再被复制。

ArrayList 对象上的 Java List.subList 不会复制任何底层数组,它只返回一个由原始数组支持但仅限于所需元素的 ArrayList。

因此 - 在某些情况下,整个机制使用实际上只是引用原始数组的子列表 - 不需要复制。

对 C++ 不太熟悉,但我怀疑有很多数组复制和分配正在进行,而 Java 不需要这样做。

添加 - 正如@ThomasKläger 正确指出的那样,ArrayList.toArray 实际上确实返回了数组的防御性副本 - 所以我在上面错了。

【讨论】:

  • 感谢您的回答。我会试着弄清楚是否真的做了那么多的分配和复制。我会在尝试时将此答案标记为“已接受”。
  • 确实有很多C++版本的复制。在mergeSort() 行std::vector&lt;T&gt; first(list.begin(), list.begin() + subLength); 和下一个调用向量范围构造函数,其中Constructs a container with as many elements as the range [first,last), with each element emplace-constructed from its corresponding element in that range, in the same order.。换句话说,这会通过 nlogn 复制操作增加运行时间。
  • "toArray() 返回对底层数组的引用" - 不,它不会,它会创建底层数组的副本(参见您引用的源代码,第 361 行)。这在ArrayList.toArray()的JavaDoc中也有明确说明:也就是说,这个方法必须分配一个新的数组
  • 当我调用std::vector&lt;T&gt; first(list.begin(), list.begin() + subLength); 时,不会只复制指向Persons 的指针吗?
  • @ThomasKläger - 感谢您的提醒。
【解决方案2】:

我在您的陈述中看到的 1st 件事之一:

在时间测量之后进行删除

您说的是删除Person 对象,您显然不是说容器,例如C++ 在堆栈上创建和清理的first 和second:

std::vector<T> first(list.begin(), list.begin() + subLength);
std::vector<T> second(list.begin() + subLength, list.end());

当 Java 在堆上创建它们并且直到它解决它们才清理它们(所以在你停止计时之后):

List<T> first = new ArrayList<T>(list.subList(0, subLength));
List<T> second = new ArrayList<T>(list.subList(subLength, list.size()));

因此,您将 C++ 与容器清理和 Java 一起计时。


我在这里要问一下,编写自己的归并排序有什么意义?最好的 Java 和 C++ 代码将使用该语言已经提供的排序算法。如果您希望至少对优化算法进行计时。

另外,我不会在时间比较上投入大量精力。 C++ 会更快,通常也需要更多的工作来编写。如果速度对你来说足够重要,以至于你可能想要使用 C++。如果开发时间为王,那么您将希望使用 Java。

【讨论】:

  • 你说得对,我不会在真正的应用程序中使用这些算法,因为我知道语言本身提供的算法显然会比我的更快。我的目标是找出实际使用中的速度差异,所以我决定自己实现一个算法来做到这一点。当然,我不能保证测量是 100% 公平的,但我会尽量做到公平。你认为造成时差的原因是C++在方法末尾删除了std::vector容器吗?
  • @killexe 显然,我看不到的代码中可能存在更多问题。 ptr 函数在 C++ 代码中也是额外的,但编译器应该简化它。但是,是的,在您提供的代码中,这是我看到的显着差异。无论哪种方式,我都感谢您花时间在 C++ 代码中使用引用参数等等。这对眼睛很容易:)
猜你喜欢
  • 1970-01-01
  • 2018-08-12
  • 1970-01-01
  • 2016-09-12
  • 1970-01-01
  • 2011-05-28
  • 2015-11-02
  • 1970-01-01
  • 2020-03-18
相关资源
最近更新 更多