为什么分配单个2D数组要比分配总大小和形状相同的多个1D数组的循环要花更长的时间?


73

我认为直接创建起来会更快,但是实际上,添加循环只需要一半的时间。发生了什么,放慢了这么多?

这是测试代码

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class Test_newArray {
    private static int num = 10000;
    private static int length = 10;

    @Benchmark
    public static int[][] newArray() {
        return new int[num][length];
    }

    @Benchmark
    public static int[][] newArray2() {
        int[][] temps = new int[num][];
        for (int i = 0; i < temps.length; i++) {
            temps[i] = new int[length];
        }
        return temps;
    }

}

测试结果如下。

Benchmark                Mode  Cnt    Score   Error  Units
Test_newArray.newArray   avgt   25  289.254 ± 4.982  us/op
Test_newArray.newArray2  avgt   25  114.364 ± 1.446  us/op

测试环境如下

JMH版本:1.21

VM版本:JDK 1.8.0_212,OpenJDK 64位服务器VM,25.212-b04


5
只是猜测,也许是因为,在情况下int[num][length]num x length应该分配连续大小的空间,而在情况下int[num][]
则要

你有没有在您的环境,当你改变会发生什么的任何数据numlength
NPE

@mangusta Java中没有二维数组,我相信您的猜测是错误的。stackoverflow.com/a/6631081/1570854
Lesiak

2
如果使用来运行JMH -prof perfasm,您可能会获得一些有用的见解。例如,我可以ObjArrayKlass::multi_allocate在第一种方法的输出中看到很多内容,但是在第二种方法中却没有。我的猜测:反射开销?
knittl

3
@Bohemian JMH不会单独执行每个基准测试(即在其自己的JVM中)并为您处理预热吗?默认情况下,我相信每个基准测试使用5个分支,每个分支运行5个预热迭代和5个测量迭代。
Slaw

Answers:


83

在Java中,有一个单独的字节码指令用于分配多维数组- multianewarray

  • newArray基准测试使用multianewarray字节码;
  • newArray2newarray在循环中调用simple 。

问题是,热点JVM有没有快速路径*multianewarray字节码。该指令始终在VM运行时中执行。因此,分配未内联在已编译的代码中。

第一个基准测试必须付出在Java和VM Runtime上下文之间切换的性能损失。同样,VM运行时(用C ++编写)中的公共分配代码没有像JIT编译的代码中的内联分配那样优化,只是因为它是通用的,即没有针对特定的对象类型或特定的调用站点进行优化,执行其他运行时检查等。

这是使用async-profiler对两个基准进行概要分析的结果。我使用的是JDK 11.0.4,但是对于JDK 8来说,图片看起来很相似。

newArray

newArray2

在第一种情况下,内部花费了99%的时间OptoRuntime::multianewarray2_C-VM运行时中的C ++代码。

在第二种情况下,大多数图形是绿色的,这意味着该程序主要在Java上下文中运行,实际上执行了针对给定基准专门优化的JIT编译代码。

编辑

*需要澄清的是:HotSpotmultianewarray在设计上没有很好地优化。在两个JIT编译器中正确实现这样一个复杂的操作相当昂贵,而这种优化的好处令人怀疑:在典型应用程序中,多维数组的分配很少会成为性能瓶颈。


7
很好的答案,尤其是包括火焰图–我希望它们更常见。
kaan

1
谢谢您的回答。这是对我的疑问的一个很好的答案。问题是休息时间的娱乐性。这不是真正地优化细节。
user10339780

18

该说明下Oracle文档中的注释multianewarray说明:

创建单个维度的数组时,使用newarrayanewarray§newarray§anewarray)可能会更高效。

进一步:

newArray基准测试使用multianewarray字节码指令。

newArray2基准测试使用anewarray字节码指令。

这就是与众不同的地方。让我们看看使用perfLinux Profiler获得的统计信息。

对于newArray基准,内联后最热门的方法是:

....[Hottest Methods (after inlining)]..............................................................
 22.58%           libjvm.so  MemAllocator::allocate
 14.80%           libjvm.so  ObjArrayAllocator::initialize
 12.92%           libjvm.so  TypeArrayKlass::multi_allocate
 10.98%           libjvm.so  AccessInternal::PostRuntimeDispatch<G1BarrierSet::AccessBarrier<2670710ul, G1BarrierSet>, (AccessInternal::BarrierType)1, 2670710ul>::oop_access_barrier
  7.38%           libjvm.so  ObjArrayKlass::multi_allocate
  6.02%           libjvm.so  MemAllocator::Allocation::notify_allocation_jvmti_sampler
  5.84%          ld-2.27.so  __tls_get_addr
  5.66%           libjvm.so  CollectedHeap::array_allocate
  5.39%           libjvm.so  Klass::check_array_allocation_length
  4.76%        libc-2.27.so  __memset_avx2_unaligned_erms
  0.75%        libc-2.27.so  __memset_avx2_erms
  0.38%           libjvm.so  __tls_get_addr@plt
  0.17%           libjvm.so  memset@plt
  0.10%           libjvm.so  G1ParScanThreadState::copy_to_survivor_space
  0.10%   [kernel.kallsyms]  update_blocked_averages
  0.06%   [kernel.kallsyms]  native_write_msr
  0.05%           libjvm.so  G1ParScanThreadState::trim_queue
  0.05%           libjvm.so  Monitor::lock_without_safepoint_check
  0.05%           libjvm.so  G1FreeCollectionSetTask::G1SerialFreeCollectionSetClosure::do_heap_region
  0.05%           libjvm.so  OtherRegionsTable::occupied
  1.92%  <...other 288 warm methods...>

....[Distribution by Source]....
 87.61%           libjvm.so
  5.84%          ld-2.27.so
  5.56%        libc-2.27.so
  0.92%   [kernel.kallsyms]
  0.03%      perf-27943.map
  0.03%              [vdso]
  0.01%  libpthread-2.27.so
................................
100.00%  <totals>

对于newArray2

....[Hottest Methods (after inlining)]..............................................................
 93.45%      perf-28023.map  [unknown]
  0.26%           libjvm.so  G1ParScanThreadState::copy_to_survivor_space
  0.22%   [kernel.kallsyms]  update_blocked_averages
  0.19%           libjvm.so  OtherRegionsTable::is_empty
  0.17%        libc-2.27.so  __memset_avx2_erms
  0.16%        libc-2.27.so  __memset_avx2_unaligned_erms
  0.14%           libjvm.so  OptoRuntime::new_array_C
  0.12%           libjvm.so  G1ParScanThreadState::trim_queue
  0.11%           libjvm.so  G1FreeCollectionSetTask::G1SerialFreeCollectionSetClosure::do_heap_region
  0.11%           libjvm.so  MemAllocator::allocate_inside_tlab_slow
  0.11%           libjvm.so  ObjArrayAllocator::initialize
  0.10%           libjvm.so  OtherRegionsTable::occupied
  0.10%           libjvm.so  MemAllocator::allocate
  0.10%           libjvm.so  Monitor::lock_without_safepoint_check
  0.10%   [kernel.kallsyms]  rt2800pci_rxdone_tasklet
  0.09%           libjvm.so  G1Allocator::unsafe_max_tlab_alloc
  0.08%           libjvm.so  ThreadLocalAllocBuffer::fill
  0.08%          ld-2.27.so  __tls_get_addr
  0.07%           libjvm.so  G1CollectedHeap::allocate_new_tlab
  0.07%           libjvm.so  TypeArrayKlass::allocate_common
  4.15%  <...other 411 warm methods...>

....[Distribution by Source]....
 93.45%      perf-28023.map
  4.31%           libjvm.so
  1.64%   [kernel.kallsyms]
  0.42%        libc-2.27.so
  0.08%          ld-2.27.so
  0.06%              [vdso]
  0.04%  libpthread-2.27.so
................................
100.00%  <totals>

如我们所见,对于最慢newArray的时间,大部分时间都花在了jvm代码中(总计87.61%):

22.58%  libjvm.so  MemAllocator::allocate
14.80%  libjvm.so  ObjArrayAllocator::initialize
12.92%  libjvm.so  TypeArrayKlass::multi_allocate
 7.38%  libjvm.so  ObjArrayKlass::multi_allocate
   ...

尽管newArray2使用OptoRuntime::new_array_C,则花费更少的时间为数组分配内存。在jvm代码中花费的总时间仅为4.31%。

使用perfnorm分析器获得的奖金统计信息:

Benchmark                        Mode  Cnt        Score    Error  Units
newArray                         avgt    4      448.018 ± 80.029  us/op
newArray:CPI                     avgt             0.359            #/op
newArray:L1-dcache-load-misses   avgt         10399.712            #/op
newArray:L1-dcache-loads         avgt       1032985.924            #/op
newArray:L1-dcache-stores        avgt        590756.905            #/op
newArray:cycles                  avgt       1132753.204            #/op
newArray:instructions            avgt       3159465.006            #/op

Benchmark                        Mode  Cnt        Score    Error  Units
newArray2                        avgt    4      125.531 ± 50.749  us/op
newArray2:CPI                    avgt             0.532            #/op
newArray2:L1-dcache-load-misses  avgt         10345.720            #/op
newArray2:L1-dcache-loads        avgt         85185.726            #/op
newArray2:L1-dcache-stores       avgt        103096.223            #/op
newArray2:cycles                 avgt        346651.432            #/op
newArray2:instructions           avgt        652155.439            #/op

注意循环次数和指令数的差异。


环境:

Ubuntu 18.04.3 LTS

java version "12.0.2" 2019-07-16
Java(TM) SE Runtime Environment (build 12.0.2+10)
Java HotSpot(TM) 64-Bit Server VM (build 12.0.2+10, mixed mode, sharing)

8
如果您对这些结果进行详细说明会非常好,因为我什么都没收到;)
Andrew Tobilko

3
@Andrew谢谢您的评论。我用其他一些详细信息更新了我的答案。如果您还有任何特定问题,请随时提出。
Oleksandr Pyrohov
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.