为什么我的Go数组排序代码比Java慢得多?

时间:2019-03-20 14:30:29

标签: java sorting go

将我的一个计算密集型后端程序从Java迁移到Go后,我发现性能下降而不是提高。我经过了一些测试,发现数组排序代码似乎是罪魁祸首(我在程序中大量使用了它)。我已经编写了下面的两个简化程序进行比较,以某种方式看来,Go的内置排序功能比Java的Arrays.sort方法要慢得多?

package main

import (
    "fmt"
    "math/rand"
    "sort"
    "time"
)

func main() {
    fmt.Println("Starting")
    const x = 1000000
    const y = x * 10
    var s [y]float64
    s1 := rand.NewSource(time.Now().UnixNano())
    r1 := rand.New(s1)
    start1 := time.Now()
    for i := 0; i < y; i++ {
        s[i] = r1.Float64()
    }
    end1 := time.Since(start1)
    ss := s[:]
    start2 := time.Now()
    sort.Float64s(ss)
    end2 := time.Since(start2)
    fmt.Println(end1)
    fmt.Println(end2)
    fmt.Println("Number: ", ss[x])
}

并产生如下结果:

Starting
136.6331ms  // The time taken to generate 10,000,000 random numbers
3.456781s   // The time taken to sort the 10,000,000 random numbers
Number:  0.10000285497001288

在这里使用Java程序

import java.util.*;

class RSTest {
    public static void main(String[] args) {
        System.out.println("Starting");
        int x = 1000000;
        int y = x * 10;
        Random gen = new Random(System.currentTimeMillis());
        double[] s = new double[y];
        long start1 = System.nanoTime();
        for (int i = 0; i < y; i++) {
            s[i] = gen.nextDouble();
        }
        long end1 = System.nanoTime();
        long start2 = System.nanoTime();
        Arrays.sort(s);
        long end2 = System.nanoTime();
        System.out.println((end1 - start1) / (1000000000.0));
        System.out.println((end2 - start2) / (1000000000.0));
        System.out.println(s[x]);
    }
}

结果是这样的

Starting
0.2252634  // The time taken to generate 10,000,000 random numbers
1.0303157  // The time taken to sort the 10,000,000 random numbers
0.0999513608326642

Go程序大约需要130毫秒才能生成1000万个随机数并将其分配给数组,而Jave大约需要230毫秒才能生成1000万个随机数并将其分配给数组,我认为这是我期望通过改进获得的从Java到Go。

但是对于排序部分,Java仅用1秒钟就对1000万个随机数进行了排序,但是花了3.5秒钟才对1000万个随机数进行了排序?从多次测试来看,这是非常一致的。

那么这是否意味着Go的内置sort函数确实比Java的Arrays.sort方法差很多?还是我使用Go的sort函数错误?还是我的程序有问题?

谢谢。

注意:这是来自Go 1.11和Java 8,这是我在服务器上运行的当前版本。另外,请注意,我在此处发布的这两个程序纯粹是出于测试目的,我已经在几分钟内编写了这些程序,因此可能(或者,当然,肯定是)包含了一些对实际生产系统没有多大意义的代码。 / p>

一些更新:

由于@nussjustin的建议,我尝试进行了排序。切片取得了一些有希望的结果。

由于我目前不在办公室并且使用较慢的笔记本电脑,因此上述两项测试的基准结果现在是这样的:

对于Java Arrays.sort测试

Starting
0.3590694
1.6030528 // The time taken to sort the 10,000,000 random numbers
0.10000905418967532

用于Go排序。Float64s测试

Go
Starting
233.1957ms
5.4633992s // The time taken to sort the 10,000,000 random numbers
Number:  0.10002801819954663

现在用sort.Slice修改Go测试之后

package main

import (
    "fmt"
    "math/rand"
    "sort"
    "time"
)

func main() {
    fmt.Println("Starting")
    const x = 1000000
    const y = x * 10
    var s [y]float64
    s1 := rand.NewSource(time.Now().UnixNano())
    r1 := rand.New(s1)
    start1 := time.Now()
    for i := 0; i < y; i++ {
        s[i] = r1.Float64()
    }
    end1 := time.Since(start1)
    ss := s[:]
    start2 := time.Now()
    sort.Slice(ss, func(i, j int) bool { return ss[i] < ss[j] })
    end2 := time.Since(start2)
    fmt.Println(end1)
    fmt.Println(end2)
    fmt.Println("Number: ", ss[x])
}

结果是对sort.Float64s的巨大改进,但仍不及Java的数组排序

Starting
281.4262ms
3.6745684s // The time taken to sort the 10,000,000 random numbers
Number:  0.10010604106864159

我认为有人抱怨测试只有一种分布(后来删除了他的评论),我也测试了对随机数的正态分布进行排序(尽管我会说在对均匀分布进行排序方面存在巨大的性能差异)随机数的划分已经是一个坏兆头,因为对随机数的均匀分布进行排序的算法应该已经很成熟了

我只是将随机数生成器从均匀分布替换为正态分布

开始:

s[i] = r1.NormFloat64()

Java:

s[i] = gen.nextGaussian();

Java的Arrays.sort方法的结果是

Starting
1.4126348
1.6118655
-1.2820310313627319

然后转到排序。切片

Starting
434.9106ms
3.8936811s
Number:  -1.2818667132095363

所以Go是sort.Slice仍然比Java的Arrays.sort慢大约两倍,这与均匀分配随机数相同。好处是,在生成随机数的正态分布中,Go比Java快三倍,而在生成均匀的数字分布时要快70%。

2 个答案:

答案 0 :(得分:2)

感谢@JimB和@nussjustin的建议,我自己编写了一个简单的quicksort实现,它成功了!

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func qsort(s []float64) []float64 {
    if len(s) < 2 {
        return s
    }

    left, right := 0, len(s)-1

    pivot := 0

    s[pivot], s[right] = s[right], s[pivot]

    for i := range s {
        if s[i] < s[right] {
            s[left], s[i] = s[i], s[left]
            left++
        }
    }

    s[left], s[right] = s[right], s[left]

    qsort(s[:left])
    qsort(s[left+1:])

    return s
}

func main() {
    fmt.Println("Starting")
    const x = 1000000
    const y = x * 10
    var s [y]float64
    s1 := rand.NewSource(time.Now().UnixNano())
    r1 := rand.New(s1)
    start1 := time.Now()
    for i := 0; i < y; i++ {
        s[i] = r1.NormFloat64()
    }
    end1 := time.Since(start1)
    ss := s[:]
    start2 := time.Now()
    ss = qsort(ss)
    end2 := time.Since(start2)
    fmt.Println(end1)
    fmt.Println(end2)
    fmt.Println("Number: ", ss[x])
}

通过这种超级原油快速分类,现在我可以实现以下结果

Starting
276.763ms
1.589941s
Number:  -1.281875446690731 

现在,它的速度始终比Java的Arrays.sort方法快15%左右!

我还实现了一种专门用于Java中double数组的quicksort方法来替换Arrays.sort方法,以查看是否可以获得任何性能提升,性能最终与Arrays.sort相同,仍然大约为10%到15比Go慢%。似乎Arrays.sort已经以某种方式达到了Java可能的最佳性能,并且通过剥离抽象并不会获得任何收益。

所以我想上一课是,如果您想在Go的排序中表现出色,然后自己实现一个quicksort函数,请不要使用内置的sort函数,甚至不要使用sort.Slice的速度要比自写的慢大约两倍sort函数和sort.Float64s慢三倍以上(有时是四倍)!

我猜这些结果最终会使那些评论者对他们所谓的“无效基准”一词不屑一顾。就像我说过的那样,从Java迁移到Go之后的性能下降对于我的生产系统来说是相当真实的,如果无法尽快修复它,我将处于紧要关头,现在希望在替换所有这些排序函数之后,我们终于可以看到一个改善了我们的生产系统的性能,使我今晚可以安然入睡:)

答案 1 :(得分:-2)

二十年前,Java 1.1运行缓慢。从那时起,成千上万的人开始致力于解决该问题。如今,Java代码通常与C ++相当或更快。借助Java 12和GraalVM,我们都会看到另一个提升。

Java中的错误代码可能很慢,但C ++的情况同样如此。 Java没有头脑,您必须使用自己的:-)

要回答这个问题,代码看起来正确。我的猜测是,Java's sort implementation实际上已经过优化,可以处理来自数千个用例的数据。只需看一下长度即可:与500 in Go相比,约有3000行带有大量的角落案例。