developer tip

단일 2D 배열을 할당하는 것이 전체 크기와 모양이 동일한 여러 1D 배열을 할당하는 루프보다 더 오래 걸리는 이유는 무엇입니까?

copycodes 2020. 11. 21. 10:29
반응형

단일 2D 배열을 할당하는 것이 전체 크기와 모양이 동일한 여러 1D 배열을 할당하는 루프보다 더 오래 걸리는 이유는 무엇입니까?


직접 만드는 것이 더 빠르다고 생각했지만 실제로 루프를 추가하는 데 걸리는 시간은 절반에 불과합니다. 너무 느려진 무슨 일이 있었나요?

다음은 테스트 코드입니다.

@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바이트 코드를 사용합니다 .
  • newArray2newarray루프에서 단순 호출합니다 .

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

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

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

newArray

newArray2

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

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

편집하다

* 명확히하기 위해 : HotSpot multianewarray에서는 설계 상 잘 최적화되지 않았습니다. 두 JIT 컴파일러 모두에서 이러한 복잡한 작업을 올바르게 구현하는 것은 비용이 많이 들지만 이러한 최적화의 이점은 의심 스러울 것입니다. 다차원 배열 할당은 일반적인 응용 프로그램에서 성능 병목 현상이 거의 발생하지 않습니다.


에서 메모 오라클 문서 세 이하 multianewarray명령은 말합니다 :

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

더욱이:

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

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

그리고 그것이 차이를 만드는 것입니다. perfLinux 프로파일 러 를 사용하여 얻은 통계를 살펴 보겠습니다 .

를 들어 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)

참고 URL : https://stackoverflow.com/questions/58158445/why-does-allocating-a-single-2d-array-take-longer-than-a-loop-allocating-multipl

반응형