【问题标题】:Execution of bubble sort is 5 times slower with --indy使用--indy,冒泡排序的执行速度要慢 5 倍
【发布时间】:2015-04-23 03:10:49
【问题描述】:

我写了一个冒泡排序的实现来玩一下 Groovy,看看 --indy 是否对性能有任何明显的影响。

本质上,它将一千个随机整数的列表排序一千次,并测量排序列表的平均执行时间。

列表中有一半是Integer[],另一半是ArrayList<Integer>

结果真的让我很困惑:

$ groovyc BubbleSort.groovy
$ time java -cp ~/.gvm/groovy/current/embeddable/groovy-all-2.4.3.jar:. BubbleSort
Average: 22.926ms
Min: 11.202ms
[...] 26.48s user 0.84s system 109% cpu 25.033 total

$ groovyc --indy BubbleSort.groovy
$ time java -cp ~/.gvm/groovy/current/embeddable/groovy-all-2.4.3-indy.jar:. BubbleSort
Average: 119.766ms
Min: 68.251ms
[...] 166.05s user 1.52s system 135% cpu 2:03.82 total

查看运行基准测试时的 CPU 使用率,使用--indy 编译时的 CPU 使用率比没有编译时高很多。

这引起了我的兴趣,因此我再次运行了基准测试 - 但这次启用了 Yourkit 代理和 CPU 跟踪。以下是记录的调用树:

没有--indy

--indy:

这里是性能图表 - 请注意,时间尺度是不同的,因为 --indy 代码要慢得多。

没有--indy(1s 刻度):

使用--indy(60 秒规模):

可以看出,不使用--indy 编译时,CPU 使用率稳定在一个内核的 100%(图中为 12.5%),但使用--indy 编译时,CPU 使用率在 12.5% 到 ~35% 之间变化很大。更令人困惑的是,Yourkit 只报告一个活动线程(而我的代码只使用主线程),但它仍然设法保持两个半核心被占用。

使用--indy 编译的代码在开始时也会占用大量内核时间,但一段时间后会下降并稳定在 0% - 此时代码似乎加快了一点(堆使用增长率增加)和 CPU 使用率增加。

谁能向我解释这种行为?

版本:

$ groovy -v
Groovy Version: 2.4.3 JVM: 1.8.0_45 Vendor: Oracle Corporation OS: Linux

$ java -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)

BubbleSort.groovy:

class BubbleSort {
    final def array

    BubbleSort(final def array) {
        this.array = array
    }

    private void swap(int a, int b) {
        def tmp = array[a];
        array[a] = array[b]
        array[b] = tmp;
    }

    private void rise(int index) {
        for(int i = index; i > 0; i--) {
            if(array[i] < array[i - 1]) {
                swap(i, i-1)
            } else {
                break
            }
        }
    }

    void sort() {
        for(int i = 1; i < array.size(); i++) {
            rise i
        }
    }

    final static Random random = new Random()

    static void main(String[] args) {
        def n = 1000
        def size = 1000
        // Warm up
        doBenchmark 100, size
        def results = doBenchmark n, size
        printf("Average: %.3fms%n", results.total / 1e6 / n)
        printf("Min: %.3fms%n", results.min / 1e6)
    }

    private static def doBenchmark(int n, int size) {
        long total = 0
        long min = Long.MAX_VALUE
        n.times {
            def array = (1..size).collect { random.nextInt() }
            if(it % 2) {
                array = array as Integer[]
            }
            def start = System.nanoTime()
            new BubbleSort<Integer>(array).sort()
            def end = System.nanoTime()
            def time = end - start
            total += time
            min = Math.min min, time
        }
        return [total: total, min: min]
    }
}

我对冒泡排序实现的优化不感兴趣,除非它们与 invokedynamic 行为相关 - 这里的目的不是编写性能最好的冒泡排序,而是要了解为什么 --indy 有如此大的负面性能影响。

更新:

我将我的代码转换为 JRuby 并尝试了同样的事情,结果相似,尽管没有 invokedynamic,JRuby 的速度几乎没有那么快:

$ JAVA_OPTS="-Djruby.compile.invokedynamic=false" jruby bubblesort.rb
Average: 78.714ms
Min: 35.000ms

$ JAVA_OPTS="-Djruby.compile.invokedynamic=true" jruby bubblesort.rb
Average: 136.287ms
Min: 92.000ms

更新 2:

如果我删除将列表更改为Integer[] 的代码,一半的时间性能会显着提高,尽管没有--indy 仍然更快:

$ groovyc BubbleSort.groovy
$ java -cp ~/.gvm/groovy/current/embeddable/groovy-all-2.4.3.jar:. BubbleSort
Average: 29.794ms
Min: 26.577ms

$ groovyc --indy BubbleSort.groovy
$ java -cp ~/.gvm/groovy/current/embeddable/groovy-all-2.4.3-indy.jar:. BubbleSort
Average: 37.506ms
Min: 33.555ms

如果我对 JRuby 做同样的事情,invokedynamic 会更快:

$ JAVA_OPTS="-Djruby.compile.invokedynamic=false" jruby bubblesort.rb
Average: 34.388ms
Min: 31.000ms

$ JAVA_OPTS="-Djruby.compile.invokedynamic=true" jruby bubblesort.rb
Average: 20.785ms
Min: 18.000ms

【问题讨论】:

  • 使用1.8.0_311.7.0_76 运行您的第一个示例;它们的运行速度都一样快(1.8 的 indy 稍快,1.7 的速度稍慢)
  • 您的预热与您测量的代码不同,因此还不够。一旦你解决了这个问题,我建议也使用10000 元素和100000 元素运行,我看看它是如何发展的……
  • 将基准测试移到单独的方法中,因此预热和基准测试执行相同的代码 - 完全没有变化。
  • 列表大小加倍具有将两个基准测试的时间翻两番的预期结果 - 将使更长的基准测试在一夜之间运行,以查看更多项目对 --indy 的影响(我会尝试找出一个合适的数字,理论上可以在我明天早上上班前不久完成)
  • 在 1.7.0.80 上没有--indy 的性能没有变化。使用--indy,它在几百次迭代后会明显变慢(我们说的是数量级)。

标签: groovy java-8 jruby invokedynamic


【解决方案1】:

答案其实很简单,Groovy 还没有 PIC。

...或者你可以说我们通常有一个大小为 1 的内联缓存。这意味着每次更改列表数组类型时,它都会使之前确实存在的所有缓存失效,并且缓存的版本被丢弃。这对于普通的 Groovy 与 indy 几乎相同,只是普通的 Groovy 使用运行时生成的类,而 indy 使用 invokedynamic/lambda 形式。但 lambda 形式最初较慢,而峰值性能更好。基本上你所做的是让热点从头开始进行大多数方法调用,防止它一直应用优化。当然这不是你的错,而是 Groovy 的错,因为它还没有 PIC。只是为了让这一点非常清楚......这不是语言的问题,它只是我还没有实现的东西。

另一方面,JRuby 有一个 PIC,因此不必一直承受创建新方法句柄的开销。

【讨论】:

  • 这是有道理的,但我仍然对为什么--indy 在不更改列表类型时仍然较慢感到困惑。当然,HotSpot 应该在 100 轮(调用 DefaultGroovyMethods#getAt(List&lt;T&gt;, int)putAt(List&lt;T&gt;, int, T) 大约 5000 万次)后进行足够优化,以让 --indy 更快?
  • 我建议您使用--disableopt=all 运行程序并进行比较。这将禁用至少在调用riseswap 时肯定有助于提高程序性能的原始优化。 Primopts 可以在某些情况下添加受保护的直接方法调用。虽然守卫成本,但直接方法调用可以使事情比平时快得多。我通常也会在没有分层编译的情况下运行 (-XX:-TieredCompilation),因为它会增加预热时间。在这些变化之后,indy 应该更快或者至少同样快。
  • 从长远来看,我可能会在 indy 中添加一些 primopts 或修改版本
  • -XX:-TieredCompilation--disableopt=all 的执行时间足够接近,我认为它们是相等的。 ~49.8 --indy 和 ~49.2 没有。考虑到只运行了 1000 次迭代,我认为这已经足够接近了。
猜你喜欢
  • 2017-03-18
  • 1970-01-01
  • 1970-01-01
  • 2013-03-08
  • 2021-08-21
  • 2019-10-04
  • 2022-10-21
  • 2021-11-28
  • 2013-04-22
相关资源
最近更新 更多