5 февр. 2016 г.

Java unsafe: copyMemory aligned vs unaligned

Давеча переводя проект на новые рельсы стала падать сборка - тесты проходят, а вот jvm крешится.

Первое подозрение упало на наш 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 комментария:

Ruslan Cheremin комментирует...

Так почему разницы между "8байт" и "8байт начиная с 3-го" не видно? По теории же получается, что во втором случае будет 2 чтения/записи против одного в первом? По мне так нужно больше промежуточных размеров/смещений, чтобы картина стала понятной.

Я помню года два-три назад большой тред в c-i по поводу unaligned accesses в ByteBuffers. Там, правда, крутилось все вокруг вопросов атомарности таких операций -- обсуждали какие гарантии спека должна (и должна ли) давать для операций с памятью через BB в многопоточном контексте, и все крутилось как раз вокруг того, что BB дает возможность делать unaligned access, а здесь обеспечить даже базовые гарантии JMM очень сложно, потому что далеко не все процессоры поддерживают атомарные невыровненные чтения/записи. Но вопросы производительности таких операций там тоже поднимались -- интелловские процы, вроде бы, умеют атомарно их делать, "но как они это делают...". Если не читал, попробуй найти, там, мне помнится, подробно разбирался вопрос

Vladimir Dolzhenko комментирует...

Меня тоже это удивило - более того, если посмотреть на бенчмарк он меряет в 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х поточном режиме

Vladimir Dolzhenko комментирует...

Кажется я понял в чем проблема: не валидность описанного 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 и посмотреть в ассемблерный код.

Vladimir Dolzhenko комментирует...

куда идет Unsafe.copyMemory: Copy::conjoint_memory_atomic

по строчкам

uintptr_t bits = (uintptr_t) src | (uintptr_t) dst | (uintptr_t) size;
и
(bits % sizeof(jlong) == 0)

как раз и выходит, что должны быть все выравнены по 8ке, чтобы использовать long-копирование