Первое подозрение упало на наш alloc / dealloc поверх Unsafe (да-да, кровь-кишки-камаз) - но в итоге все же стало ясно, что наши руки чистые и вина реально на C2 компиляторе и трова стала тому причина - на эту тему две ошибки в C2 компиляторе: JDK-8081379 и по мотивам этого же JDK-6675699.
Однако, пока я винил наш alloc/dealloc смотрел - а не пытаемся ли мы деаллоцировать чего не нужного, соответственно стал писать адреса выделенной памяти и размеры.
Удивило, что очень много объектов без выравнивания по длине - например, 27 байт или там 253.
Читаем sun.misc.Unsafe#copyMemory и тут ребята заботливо пишут:
/**
* Sets all bytes in a given block of memory to a copy of another
* block.
*
* This method determines each block's base address by means of two parameters,
* and so it provides (in effect) a double-register addressing mode,
* as discussed in {@link #getInt(Object,long)}. When the object reference is null,
* the offset supplies an absolute base address.
*
* The transfers are in coherent (atomic) units of a size determined
* by the address and length parameters. If the effective addresses and
* length are all even modulo 8, the transfer takes place in 'long' units.
* If the effective addresses and length are (resp.) even modulo 4 or 2,
* the transfer takes place in units of 'int' or 'short'.
*
* @since 1.7
*/
Более того, мистер Коваль накинул еще:
мол не только же длина, но и смещение играют роль - н.р если даже читать long, но он будет смещен на 1 байт относительно слова - то в итоге будут прочитаны два long.
обновление 2015-02-13:
Итак, куда же идет Unsafe.copyMemory ?
Это нативный jvm вызов:
копаем в сторону unsafe.cpp: Unsafe_CopyMemory
потом Copy::conjoint_memory_atomic
и вот от момент откровения:
uintptr_t bits = (uintptr_t) src | (uintptr_t) dst | (uintptr_t) size;
if (bits % sizeof(jlong) == 0) {
Copy::conjoint_jlongs_atomic((jlong*) src, (jlong*) dst, size / sizeof(jlong));
} else if (bits % sizeof(jint) == 0) {
Copy::conjoint_jints_atomic((jint*) src, (jint*) dst, size / sizeof(jint));
} else if (bits % sizeof(jshort) == 0) {
Copy::conjoint_jshorts_atomic((jshort*) src, (jshort*) dst, size / sizeof(jshort));
} else {
// Not aligned, so no need to be atomic.
Copy::conjoint_jbytes((void*) src, (void*) dst, size);
}
Если кратко, то bits тогда и только тогда будет кратен размеру long, когда и адрес источника, и адрес цели, и длина блока будут кратны размеру long.
benchmark.
проверяет копирование в многопоточном режиме - 4 нитки.
Изначально производилось копирование из одного общего (для всех нитей) куска памяти в другой общий кусок памяти - и от этого получались странные цифры, что копирование 8 байт со смещением 0 такое же, как и копирование 8 байт со смещением 3 байта.
В моем приложении другой шаблон использования - копирование происходит из общего куска памяти в свой (н-р локальный массив байт) локальный кусок памяти.
Результаты для 4х ниток:
Benchmark Mode Samples Score Error Units copyMemory253Bytes avgt 15 13.023 ± 0.376 ns/op copyMemory253BytesOffset3 avgt 15 13.189 ± 0.512 ns/op copyMemory256Bytes avgt 15 11.091 ± 0.304 ns/op copyMemory256BytesOffset7 avgt 15 13.185 ± 0.066 ns/op copyMemory27Bytes avgt 15 9.034 ± 0.058 ns/op copyMemory32Bytes avgt 15 5.567 ± 0.154 ns/op copyMemory32BytesOffset3 avgt 15 7.779 ± 0.080 ns/op copyMemory4Bytes avgt 15 6.801 ± 0.068 ns/op copyMemory4BytesOffset3 avgt 15 7.760 ± 0.091 ns/op copyMemory8Bytes avgt 15 6.763 ± 0.044 ns/op copyMemory8BytesOffset1 avgt 15 8.398 ± 0.154 ns/op copyMemory8BytesOffset3 avgt 15 8.368 ± 0.083 ns/op copyMemory8BytesOffset5 avgt 15 8.396 ± 0.079 ns/op readByteOffset0 avgt 15 3.067 ± 0.153 ns/op readByteOffset3 avgt 15 3.024 ± 0.044 ns/op readIntOffset0 avgt 15 3.001 ± 0.037 ns/op readIntOffset3 avgt 15 3.051 ± 0.150 ns/op readLongOffset0 avgt 15 2.997 ± 0.042 ns/op readLongOffset3 avgt 15 2.993 ± 0.056 ns/opи для 1 нитки
Benchmark Mode Samples Score Error Units copyMemory253Bytes avgt 15 13.392 ± 0.580 ns/op copyMemory253BytesOffset3 avgt 15 12.903 ± 0.304 ns/op copyMemory256Bytes avgt 15 11.195 ± 0.405 ns/op copyMemory256BytesOffset7 avgt 15 13.446 ± 0.422 ns/op copyMemory27Bytes avgt 15 9.136 ± 0.266 ns/op copyMemory32Bytes avgt 15 5.513 ± 0.190 ns/op copyMemory32BytesOffset3 avgt 15 7.830 ± 0.273 ns/op copyMemory4Bytes avgt 15 6.826 ± 0.204 ns/op copyMemory4BytesOffset3 avgt 15 7.757 ± 0.223 ns/op copyMemory8Bytes avgt 15 6.897 ± 0.221 ns/op copyMemory8BytesOffset1 avgt 15 8.534 ± 0.276 ns/op copyMemory8BytesOffset3 avgt 15 8.482 ± 0.285 ns/op copyMemory8BytesOffset5 avgt 15 8.403 ± 0.262 ns/op readByteOffset0 avgt 15 2.971 ± 0.104 ns/op readByteOffset3 avgt 15 3.009 ± 0.115 ns/op readIntOffset0 avgt 15 3.001 ± 0.125 ns/op readIntOffset3 avgt 15 3.032 ± 0.113 ns/op readLongOffset0 avgt 15 3.010 ± 0.100 ns/op readLongOffset3 avgt 15 3.060 ± 0.094 ns/opТ.е действительно для копирования очень важно, чтобы всё было по восьмерке, тогда как для чтения это не имеет значения. ... meten is weten // голл. поговорка: измерение это знание
4 комментария:
Так почему разницы между "8байт" и "8байт начиная с 3-го" не видно? По теории же получается, что во втором случае будет 2 чтения/записи против одного в первом? По мне так нужно больше промежуточных размеров/смещений, чтобы картина стала понятной.
Я помню года два-три назад большой тред в c-i по поводу unaligned accesses в ByteBuffers. Там, правда, крутилось все вокруг вопросов атомарности таких операций -- обсуждали какие гарантии спека должна (и должна ли) давать для операций с памятью через BB в многопоточном контексте, и все крутилось как раз вокруг того, что BB дает возможность делать unaligned access, а здесь обеспечить даже базовые гарантии JMM очень сложно, потому что далеко не все процессоры поддерживают атомарные невыровненные чтения/записи. Но вопросы производительности таких операций там тоже поднимались -- интелловские процы, вроде бы, умеют атомарно их делать, "но как они это делают...". Если не читал, попробуй найти, там, мне помнится, подробно разбирался вопрос
Меня тоже это удивило - более того, если посмотреть на бенчмарк он меряет в 4х нитях, перемерял в разных режимах - с большим разогревом и временем измерения для достоверности - и еще более удивительные результаты
1 thread
Benchmark Mode Samples Score Error Units
copyMemory4Bytes avgt 30 6.824 ± 0.129 ns/op
copyMemory4BytesOffset3 avgt 30 7.817 ± 0.170 ns/op
copyMemory8Bytes avgt 30 6.991 ± 0.184 ns/op
copyMemory8BytesOffset1 avgt 30 8.588 ± 0.198 ns/op
copyMemory8BytesOffset3 avgt 30 8.586 ± 0.193 ns/op
copyMemory8BytesOffset5 avgt 30 8.593 ± 0.168 ns/op
2 threads
Benchmark Mode Samples Score Error Units
copyMemory4Bytes avgt 30 8.211 ± 0.074 ns/op
copyMemory4BytesOffset3 avgt 30 8.363 ± 0.082 ns/op
copyMemory8Bytes avgt 30 8.215 ± 0.068 ns/op
copyMemory8BytesOffset1 avgt 30 8.715 ± 0.094 ns/op
copyMemory8BytesOffset3 avgt 30 8.826 ± 0.082 ns/op
copyMemory8BytesOffset5 avgt 30 8.904 ± 0.097 ns/op
4 threads
Benchmark Mode Samples Score Error Units
copyMemory4Bytes avgt 30 8.441 ± 0.126 ns/op
copyMemory4BytesOffset3 avgt 30 8.393 ± 0.086 ns/op
copyMemory8Bytes avgt 30 8.343 ± 0.078 ns/op
copyMemory8BytesOffset1 avgt 30 8.786 ± 0.108 ns/op
copyMemory8BytesOffset3 avgt 30 8.714 ± 0.081 ns/op
copyMemory8BytesOffset5 avgt 30 8.763 ± 0.084 ns/op
т.е в однопоточном режиме преимущество более, чем заметно - изначальный benchmark всё же в 4х поточном режиме
Кажется я понял в чем проблема: не валидность описанного benchmark'а - многопоточное копирование из одного общего куска памяти в другой общий же кусок памяти. В нашем случае происходит копирование из одного общего куска в другой, но свой (новый массив байт н-р) кусок.
2 threads:
Benchmark Mode Samples Score Error Units
o.g.b.CopyPerfTest.copyMemory4Bytes avgt 15 6.634 ± 0.072 ns/op
o.g.b.CopyPerfTest.copyMemory4BytesOffset3 avgt 15 7.643 ± 0.116 ns/op
o.g.b.CopyPerfTest.copyMemory8Bytes avgt 15 6.672 ± 0.070 ns/op
o.g.b.CopyPerfTest.copyMemory8BytesOffset1 avgt 15 8.227 ± 0.072 ns/op
o.g.b.CopyPerfTest.copyMemory8BytesOffset3 avgt 15 8.226 ± 0.084 ns/op
4 threads:
Benchmark Mode Samples Score Error Units
o.g.b.CopyPerfTest.copyMemory4Bytes avgt 15 6.988 ± 0.359 ns/op
o.g.b.CopyPerfTest.copyMemory4BytesOffset3 avgt 15 7.891 ± 0.152 ns/op
o.g.b.CopyPerfTest.copyMemory8Bytes avgt 15 6.822 ± 0.061 ns/op
o.g.b.CopyPerfTest.copyMemory8BytesOffset1 avgt 15 8.444 ± 0.065 ns/op
o.g.b.CopyPerfTest.copyMemory8BytesOffset3 avgt 15 8.407 ± 0.052 ns/op
o.g.b.CopyPerfTest.copyMemory8BytesOffset5 avgt 15 8.396 ± 0.058 ns/op
o.g.b.CopyPerfTest.copyMemory8BytesOffset5 avgt 15 8.275 ± 0.113 ns/op
Хотя конечно для пущей наглядности надо бы загнуть в исходники Unsafe и посмотреть в ассемблерный код.
куда идет Unsafe.copyMemory: Copy::conjoint_memory_atomic
по строчкам
uintptr_t bits = (uintptr_t) src | (uintptr_t) dst | (uintptr_t) size;
и
(bits % sizeof(jlong) == 0)
как раз и выходит, что должны быть все выравнены по 8ке, чтобы использовать long-копирование
Отправить комментарий