java - 단일 2D 배열을 할당하는 것이 동일한 총 크기와 모양의 여러 1D 배열을 할당하는 루프보다 시간이 더 걸리는 이유는 무엇입니까?




performance (2)

직접 작성하는 것이 더 빠를 것이라고 생각했지만 실제로 루프를 추가하는 데 절반의 시간이 걸립니다. 너무 느려진 일은 무엇입니까?

테스트 코드는 다음과 같습니다

@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


Java에는 다차원 배열 ( multianewarray 을 할당하기위한 별도의 바이트 코드 명령어가 있습니다.

  • newArray 벤치 마크는 multianewarray 바이트 코드를 사용합니다.
  • newArray2 는 루프에서 간단한 newarray 를 호출합니다.

문제는 HotSpot JVM에 multianewarray 바이트 코드에 대한 빠른 경로가 없다는 것 입니다. 이 명령어는 항상 VM 런타임에서 실행됩니다. 따라서 할당은 컴파일 된 코드에 인라인되지 않습니다.

첫 번째 벤치 마크는 Java와 VM 런타임 컨텍스트 간 전환에 따른 성능 저하를 초래해야합니다. 또한 VM 런타임의 공통 할당 코드 (C ++로 작성)는 JIT 컴파일 된 코드의 인라인 할당만큼 최적화되지 않습니다. 즉, 특정 객체 유형이나 특정 호출 사이트에 대해 최적화되지 않았기 때문입니다. 추가 런타임 검사 등을 수행합니다.

다음은 async-profiler 하여 두 벤치 마크를 프로파일 링 한 결과입니다. JDK 11.0.4를 사용했지만 JDK 8의 경우 그림이 비슷해 보입니다.

첫 번째 경우, 99 %의 시간이 OptoRuntime::multianewarray2_C -VM 런타임의 C ++ 코드에서 소비됩니다.

두 번째 경우, 대부분의 그래프는 녹색입니다. 즉, 프로그램은 대부분 Java 컨텍스트에서 실행되므로 실제로 주어진 벤치 마크에 맞게 최적화 된 JIT 컴파일 된 코드를 실행합니다.

편집하다

* 명확히하기 위해 : HotSpot에서 multianewarray 는 디자인에 의해 잘 최적화되지 않았습니다. 두 JIT 컴파일러 모두에서 이러한 복잡한 작업을 올바르게 구현하는 데 비용이 많이 드는 반면, 이러한 최적화의 이점은 의문의 여지가 있습니다. 다차원 배열의 할당은 일반적인 응용 프로그램에서 성능 병목 현상이 거의 없습니다.


multianewarray 지침에 따라 Oracle Docs 의 메모에 다음과 같이 표시됩니다.

단일 차원의 배열을 만들 때 newarray 또는 anewarray ( §newarray , §anewarray )를 사용하는 것이 더 효율적일 수 있습니다.

더욱이:

newArray 벤치 마크는 anewarray 바이트 코드 명령어를 사용합니다.

newArray2 벤치 마크는 multianewarray 바이트 코드 명령어를 사용합니다.

그리고 그것이 차이를 만드는 것입니다. perf Linux 프로파일 러를 사용하여 얻은 통계를 보자.

newArray 벤치 마크의 경우 생성 된 코드에서 가장 인기있는 영역 1 은 다음과 같습니다.

....[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  [email protected]
  0.17%           libjvm.so  [email protected]
  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...>

그리고 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...>

보다시피 느리게 newArray 벤치 마크를 위해 대부분의 시간이 소요됩니다.

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

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

사이클 수와 지침의 차이에 유의하십시오.

1-실행 시간이 가장 많이 소요되는 코드 영역





performance