What is the most efficient way to reinterpret underlying bit patterns and write it into and array or field?

Viewed 396

Using Unsafe.putXXX one can put primitive type into an array or object field.

But code like the following generate errors.

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)

Also the bytes may be write as:

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;

Is there an efficient way to write reinterpret different types and write them into fields and arrays possibly avoiding Unsafe but as efficient.

Any special cases where the compiler or JIT will recognise the intent and optimise accordingly.

2 Answers

For byte[] in particular you can use 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));
}

There are some hoops you have to jump through with VarHandles to make them just as fast as Unsafe;

  1. Make sure the VarHandle itself is constant, this can be done by putting it in a static final field and accessing it from there.
  2. Make sure the VarHandle invocation is exact. Here this would mean casting the second argument to a long since the VarHandle excepts a long as well. (in the latest JDK you can use VarHandle::withInvokeExactBehavior to enforce that).

This could be made easier by wrapping the VarHandle set and get calls in helper methods that do the cast:

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);
}    

The canonical way, using ByteBuffer, is also the fastest way as of Java 17. Measure for yourself (and/or consider other options) if you're not on Java 17 yet.

ByteBuffer byteBuffer = ByteBuffer.allocate(bytesLength);
byteBuffer.asLongBuffer().put(longs);
return byteBuffer.array();

I did an experiment with serializing float[] to byte[] and depending on the required byte order and Java version different methods came out differently, but ByteBuffer and MemorySegment and Unsafe or libraries using them were on top. VarHandles are not very far behind, but they're still loopy.

On OpenJDK 17 using G1, on my MBP 16" 2019, the results were:

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

Note that if you're not on Java 17 yet, your results may be very different! The ObjectOutputStream was way slower before Java 17, and two of the four *byteBuffer variants were 2* as slow too. Therefore, measure for your particular JVM and JVM config. In general, though, ByteBuffer or MemorySegment is the way to go going forward.

Related