Почему выделение одного двумерного массива занимает больше времени, чем циклическое выделение нескольких одномерных массивов одинакового общего размера и формы?
Я думал, что будет быстрее создать напрямую, но на самом деле, добавление циклов занимает только половину времени. Что случилось, что так сильно замедлилось?
Вот тестовый код
@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
Версия виртуальной машины: JDK 1.8.0_212, 64-разрядная серверная виртуальная машина OpenJDK, 25.212-b04
Ответы
Ответ 1
В Java есть отдельная инструкция для выделения многомерных массивов байт-кода - multianewarray
.
- В тесте
newArray
используется байт-код multianewarray
;
newArray2
вызывает простой newarray
в цикле.
Проблема в том, что у HotSpot JVM нет быстрого пути* для байт-кода multianewarray
. Эта инструкция всегда выполняется во время выполнения виртуальной машины. Поэтому выделение не встроено в скомпилированный код.
Первый тест должен платить за снижение производительности при переключении между контекстами Java и VM Runtime. Кроме того, общий код распределения во время выполнения виртуальной машины (записанный в C++) не так оптимизирован, как встроенное распределение в JIT-скомпилированном коде, просто потому, что он универсальный, то есть не оптимизирован для конкретного типа объекта или для конкретного сайта вызова выполняет дополнительные проверки во время выполнения и т.д.
Вот результаты профилирования обоих тестов с помощью async-profiler. Я использовал JDK 11.0.4, но для JDK 8 картина выглядит аналогично.
В первом случае 99% времени тратится внутри OptoRuntime::multianewarray2_C
- кода C++ во время выполнения виртуальной машины.
Во втором случае большая часть графика зеленого цвета, что означает, что программа работает в основном в контексте Java, фактически выполняя JIT-скомпилированный код, оптимизированный специально для данного теста.
EDIT
* Просто для уточнения: в HotSpot multianewarray
не очень хорошо оптимизирован по дизайну. Достаточно дорого реализовать такую сложную операцию в обоих JIT-компиляторах должным образом, в то время как преимущества такой оптимизации были бы сомнительными: распределение многомерных массивов редко является узким местом производительности в типичном приложении.
Ответ 2
Примечание в Документах Oracle согласно инструкции multianewarray
гласит:
Может быть более эффективно использовать newarray
или anewarray
(§newarray, §anewarray) при создании массива одного измерения.
Далее:
В тесте newArray
используется инструкция байт-кода multianewarray
.
В тесте newArray2
используется инструкция байт-кода anewarray
.
И это то, что имеет значение. Давайте посмотрим статистику, полученную с помощью perf
Linux 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 [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...>
....[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)