#java #arrays #performance #unsafe #reinterpret-cast
#java #массивы #Производительность #небезопасно #переинтерпретировать-приведение
Вопрос:
Используя Unsafe.putXXX
один, можно поместить примитивный тип в массив или поле объекта.
Но код, подобный следующему, генерирует ошибки.
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
VarHandle varHandle = MethodHandles.arrayElementVarHandle(long[].class);
byte[] array = new byte[32];
printArray(array);
varHandle.set(array, 1, 5);
printArray(array);
System.out.println(varHandle.get(array, 1));
}
private static void printArray(byte[] array) {
System.out.println(Arrays.toString(array));
}
}
Exception in thread "main" java.lang.ClassCastException: Cannot cast [B to [J
at java.base/java.lang.Class.cast(Class.java:3780)
at Main.main(Main.java:15)
Также bytes
может быть записано как:
byte[] array = new byte[32];
long v = 5,
int i = 8;
int high = (int) (v >>> 32);
int low = (int) v;
array[i 0] = (byte) (high >>> 24);
array[i 1] = (byte) (high >>> 16);
array[i 2] = (byte) (high >>> 8);
array[i 3] = (byte) high;
array[i 4] = (byte) (low >>> 24);
array[i 5] = (byte) (low >>> 16);
array[i 6] = (byte) (low >>> 8);
array[i 7] = (byte) low;
Существует ли эффективный способ записи переосмысления различных типов и записи их в поля и массивы, возможно, избегая Unsafe
, но столь же эффективный.
Любые особые случаи, когда компилятор или JIT распознают намерение и соответствующим образом оптимизируют.
Ответ №1:
byte[]
В частности, вы можете использовать MethodHandles::byteArrayViewVarHandle
:
public static void main(String[] args) {
VarHandle varHandle = MethodHandles.byteArrayViewVarHandle(long[].class,
ByteOrder.nativeOrder());
byte[] array = new byte[32];
printArray(array);
varHandle.set(array, 1, 5);
printArray(array);
System.out.println(varHandle.get(array, 1));
}
private static void printArray(byte[] array) {
System.out.println(Arrays.toString(array));
}
Есть несколько препятствий, которые вам нужно преодолеть с помощью VarHandles, чтобы сделать их такими же быстрыми, как и небезопасными;
- Убедитесь, что сам дескриптор переменной является постоянным, это можно сделать, поместив его в статическое конечное поле и получив к нему доступ оттуда.
- Убедитесь, что вызов VarHandle является точным. Здесь это означало бы приведение второго аргумента к long, поскольку VarHandle также исключает long . (в последней версии JDK вы можете использовать
VarHandle::withInvokeExactBehavior
для обеспечения этого).
Это можно упростить, обернув набор переменных и вызовы get во вспомогательные методы, которые выполняют приведение:
private static final VarHandle LONG_ARR_HANDLE
= MethodHandles.byteArrayViewVarHandle(long[].class,
ByteOrder.nativeOrder());
public static void setLong(byte[] bytes, int index, long value) {
LONG_ARR_HANDLE.set(bytes, index, value);
}
public static long getLong(byte[] bytes, int index) {
return (long) LONG_ARR_HANDLE.get(bytes, index);
}
Комментарии:
1. 1 Спасибо за ответ. Какова производительность в отношении использования
Unsafe
? Это так же эффективно или это самый эффективный способ сделать это?2. Есть несколько препятствий, которые вам нужно преодолеть с помощью VarHandles, чтобы сделать их такими же эффективными, как и Unsafe; 1) Убедитесь, что сам VarHandle является постоянным, это можно сделать, поместив его в
static final
поле и получив к нему доступ оттуда. 2) Убедитесь, что вызов VarHandle является точным. Здесь это означало бы приведение второго аргумента к along
, поскольку VarHandle также исключает along
. Это можно упростить, обернув вызов набора VarHandle в вспомогательный метод, который выполняет приведение.3. Чем массив не
byte[]
является? Скажитеlong[] array
, и я хочу написатьbyte
.4. Если массив не является a
byte[]
, вам придется использовать API доступа к памяти, который инкубируется в последней версии JDK (16). Там вы можете преобразоватьlong[]
в aMemorySegment
, а затем получить к нему доступ, используяbyte
VarHandle доступа к памяти.
Ответ №2:
Канонический способ, используя ByteBuffer
, также является самым быстрым способом с Java 17. Оцените сами (и / или рассмотрите другие варианты), если вы еще не на Java 17.
ByteBuffer byteBuffer = ByteBuffer.allocate(bytesLength);
byteBuffer.asLongBuffer().put(longs);
return byteBuffer.array();
Я провел эксперимент с сериализацией float[]
byte[]
и в зависимости от требуемого порядка байтов и версии Java разные методы выходили по-разному, но ByteBuffer
библиотеки and MemorySegment
и Unsafe
or, использующие их, были на высоте. Переменные дескрипторы не очень отстают, но они все еще зациклены.
На OpenJDK 17 с использованием G1 на моем MBP 16 «2019 результаты были:
Benchmark (size) Mode Cnt Score Error Units
beByteBuffer 512 avgt 5 193.351 ± 21.366 ns/op
beByteBufferWrap 512 avgt 5 197.477 ± 43.743 ns/op
beDataOutputStream 512 avgt 5 1590.887 ± 60.744 ns/op
beKryo 512 avgt 5 861.624 ± 11.927 ns/op
beManualUnpacking 512 avgt 5 861.573 ± 10.108 ns/op
beObjectOutputStream 512 avgt 5 2168.386 ± 12.950 ns/op
beVarHandle 512 avgt 5 225.611 ± 1.839 ns/op
leByteBuffer 512 avgt 5 155.345 ± 1.229 ns/op
leByteBufferWrap 512 avgt 5 154.523 ± 0.853 ns/op
leDataOutputStream 512 avgt 5 55414.147 ± 9390.669 ns/op
leKryoUnsafe 512 avgt 5 156.019 ± 18.235 ns/op
leManualUnpacking 512 avgt 5 880.687 ± 12.856 ns/op
leMemorySegment 512 avgt 5 148.483 ± 0.897 ns/op
leUnsafeCopyMemory 512 avgt 5 149.107 ± 2.011 ns/op
leVarHandle 512 avgt 5 225.615 ± 1.357 ns/op
Обратите внимание, что если вы еще не на Java 17, ваши результаты могут сильно отличаться! До ObjectOutputStream
Java 17 он был намного медленнее, и два из четырех *byteBuffer
вариантов тоже были на 2 * медленнее. Поэтому измеряйте для вашей конкретной JVM и конфигурации JVM. В общем, тем не менее, ByteBuffer
или MemorySegment
это путь вперед.
Комментарии:
1. @Holger Ага, извините, в этом случае это действительно вводило в заблуждение. Я измерял более старые версии, сообщил об ошибке в коде ByteBuffer, и она была исправлена в Java 17. Я немного изменил ответ прямо сейчас и добавил ссылку на ошибку.
2. Отличное дополнение. Очень полезно знать.
3. Но, если я правильно прочитал, ввод одного значения, подобного
byteBuffer.putLong(…)
, не был затронут?4. Описание ошибки немного запутанное, извините за это. На самом деле я сообщил об этом как о двух разных ошибках, и инженер, обрабатывающий их, объединил их вместе, так что это немного сбивает с толку. Я не измерял отдельные
putLong()
вызовы, поскольку меня особенно интересовала переосмысление целых массивов. Ошибка заключалась в том, что JVM не использовала внутреннийcopyMemory
механизм для пакетного метода, но я не знаю, как работает put-single. Не стесняйтесь взглянуть на код сравнения и попытаться добавить больше опций.