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

2022-08-31 13:51:31

我以为直接创建会更快,但实际上,添加循环只需要一半的时间。发生了什么事情使速度放缓了这么多?

这是测试代码

@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


答案 1

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

  • newArray基准测试使用字节码;multianewarray
  • newArray2在循环中调用简单。newarray

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

第一个基准测试必须支付在 Java 和 VM 运行时上下文之间切换的性能损失。此外,VM 运行时中的公共分配代码(以 C++ 编写)不如 JIT 编译代码中的内联分配优化,只是因为它是通用的,即未针对特定对象类型或特定调用站点进行优化,它会执行额外的运行时检查等。

下面是使用异步探查器分析这两个基准测试的结果。我使用JDK 11.0.4,但对于JDK 8,图片看起来很相似。

newArray

newArray2

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

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

编辑

*只是为了澄清:在HotSpot中,通过设计没有很好地优化。在两个JIT编译器中正确实现如此复杂的操作是相当昂贵的,而这种优化的好处是值得怀疑的:在典型的应用程序中,多维数组的分配很少是性能瓶颈。multianewarray


答案 2

Oracle Docs中的说明中的注释说:multianewarray

在创建单一维度的数组时,使用 或 (§newarray§anewarray) 可能会更有效。newarrayanewarray

进一步:

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

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

这就是与众不同的原因。让我们看看使用 Linux 探查器获得的统计信息。perf

对于基准测试,内联后最热门的方法是: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>

正如我们所看到的,对于较慢的,大部分时间都花在jvm代码中(总共87.61%):newArray

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
   ...

而 使用 ,则为数组分配内存所花费的时间要少得多。在 jvm 代码中花费的总时间仅为 4.31%。newArray2OptoRuntime::new_array_C

使用探查器获得的奖金统计数据: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)

推荐