tuning - profiler java




Por que a alocação de uma única matriz 2D leva mais tempo do que um loop alocando várias matrizes 1D do mesmo tamanho e forma total? (2)

Em Java, há uma instrução separada de bytecode para alocar matrizes multidimensionais - multianewarray .

  • newArray benchmark newArray usa bytecode de multianewarray ;
  • newArray2 chama newArray2 simples no loop.

O problema é que o HotSpot JVM não possui um atalho * para bytecode de multianewarray . Esta instrução é sempre executada no tempo de execução da VM. Portanto, a alocação não está embutida no código compilado.

O primeiro benchmark deve pagar uma multa por desempenho ao alternar entre os contextos Java e VM Runtime. Além disso, o código de alocação comum no tempo de execução da VM (escrito em C ++) não é tão otimizado quanto a alocação embutida no código compilado por JIT, apenas porque é genérico , ou seja, não otimizado para o tipo de objeto específico ou para o site de chamada específico, executa verificações adicionais de tempo de execução, etc.

Aqui estão os resultados da criação de perfil dos dois benchmarks com o async-profiler . Eu usei o JDK 11.0.4, mas para o JDK 8 a imagem é semelhante.

No primeiro caso, 99% do tempo é gasto no OptoRuntime::multianewarray2_C - o código C ++ no tempo de execução da VM.

No segundo caso, a maior parte do gráfico é verde, o que significa que o programa é executado principalmente no contexto Java, na verdade executando código compilado por JIT otimizado especificamente para o benchmark fornecido.

EDITAR

* Apenas para esclarecer: no HotSpot, a multianewarray não é otimizada muito bem pelo design. É bastante caro implementar adequadamente uma operação tão complexa nos dois compiladores JIT, enquanto os benefícios dessa otimização seriam questionáveis: a alocação de matrizes multidimensionais raramente é um gargalo de desempenho em um aplicativo típico.

Eu pensei que seria mais rápido criar diretamente, mas, na verdade, adicionar loops leva apenas metade do tempo. O que aconteceu que diminuiu tanto?

Aqui está o código do teste

@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;
    }

}

Os resultados do teste são os seguintes.

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

O ambiente de teste é o seguinte

Versão JMH: 1.21

Versão da VM: JDK 1.8.0_212, VM do servidor OpenJDK de 64 bits, 25.212-b04


Uma observação no Oracle Docs sob a instrução multianewarray diz:

Pode ser mais eficiente usar nova newarray ou nova anewarray ( §newarray , §anewarray ) ao criar uma matriz de uma única dimensão.

Mais distante:

newArray benchmark newArray usa anewarray instrução de bytecode de uma anewarray .

newArray2 benchmark newArray2 usa instruções de bytecode de multianewarray .

E é isso que faz a diferença. Vamos ver as estatísticas obtidas usando o perf Linux perfiler.

Para o benchmark newArray , as regiões mais quentes 1 no código gerado são:

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

E para o 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...>

Como podemos ver, para o benchmark newArray mais lento, a maior parte do tempo é gasta em:

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

Enquanto o newArray2 usa o OptoRuntime::new_array_C , gasta muito menos tempo alocando a memória para matrizes.

Estatísticas de bônus obtidas usando o perfnorm profiler:

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

Observe a diferença no número de ciclos e instruções.

1 - As regiões de código que levam mais tempo de execução.





performance