Каков наиболее эффективный способ переинтерпретировать базовые битовые шаблоны и записать их в массив или поле and?

#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, чтобы сделать их такими же быстрыми, как и небезопасными;

  1. Убедитесь, что сам дескриптор переменной является постоянным, это можно сделать, поместив его в статическое конечное поле и получив к нему доступ оттуда.
  2. Убедитесь, что вызов 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 является точным. Здесь это означало бы приведение второго аргумента к a long , поскольку VarHandle также исключает a long . Это можно упростить, обернув вызов набора VarHandle в вспомогательный метод, который выполняет приведение.

3. Чем массив не byte[] является? Скажите long[] array , и я хочу написать byte .

4. Если массив не является a byte[] , вам придется использовать API доступа к памяти, который инкубируется в последней версии JDK (16). Там вы можете преобразовать long[] в a MemorySegment , а затем получить к нему доступ, используя 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. Не стесняйтесь взглянуть на код сравнения и попытаться добавить больше опций.