Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 108 additions & 70 deletions src/java.base/share/classes/jdk/internal/util/ByteArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,12 @@

package jdk.internal.util;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;
import jdk.internal.misc.Unsafe;
import jdk.internal.vm.annotation.ForceInline;

/**
* Utility methods for packing/unpacking primitive values in/out of byte arrays
* using {@linkplain ByteOrder#BIG_ENDIAN big endian order} (aka. "network order").
* using {@linkplain java.nio.ByteOrder#BIG_ENDIAN big endian order} (aka. "network order").
* <p>
* All methods in this class will throw an {@linkplain NullPointerException} if {@code null} is
* passed in as a method parameter for a byte array.
Expand All @@ -41,12 +40,21 @@ public final class ByteArray {
private ByteArray() {
}

private static final VarHandle SHORT = create(short[].class);
private static final VarHandle CHAR = create(char[].class);
private static final VarHandle INT = create(int[].class);
private static final VarHandle FLOAT = create(float[].class);
private static final VarHandle LONG = create(long[].class);
private static final VarHandle DOUBLE = create(double[].class);
/**
* The {@code Unsafe} can be functionality replaced by
* {@linkplain java.lang.invoke.MethodHandles#byteArrayViewVarHandle byteArrayViewVarHandle},
* but it's not feasible in practices, because {@code ByteArray} and {@code ByteArrayLittleEndian}
* can be used in fundamental classes, {@code VarHandle} exercise many other
* code at VM startup, this could lead a recursive calls when fundamental
* classes is used in {@code VarHandle}.
Copy link
Contributor

@AlanBateman AlanBateman Jul 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is confusing, esp. "not feasible in practices". If this code is changed then the comment can be very simple to say that it uses Unsafe to allow it be used in early startup and for in the implementation of classes such as VarHandle.

*/
static final Unsafe UNSAFE = Unsafe.getUnsafe();

@ForceInline
static long arrayOffset(byte[] array, int typeBytes, int offset) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, this is the really interesting thing that this class does - e.g. it introduces a way to translate a (logical) offset into a byte array into a physical offset that can be used for unsafe. After you have an helper method like this, it seems like the client can just do what it wants by using Unsafe directly (which would remove the need for having this class) ? Was some experiment of that kind done (e.g. replacing usage of ByteArray with Unsafe + helpers) - or does it lead to code that is too cumbersome on the client?

Also, were ByteBuffers considered as an alternative? (I'm not suggesting MemorySegment as those depend on VarHandle again, but a heap ByteBuffer is just a thin wrapper around an array which uses Unsafe). ByteBuffer will have a bound check, but so does your code (which call checkIndex). I believe that, at least in hot code, wrapping a ByteBuffer around a byte array should be routinely scalarized, as there's no control flow inside these little methods.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, a byte buffer is big endian, so some extra code would be required. But maybe that's another helper function:

@ForceInline
ByteBuffer asBuffer(byte[] array) { return ByteBuffer.wrap(array).order(ByteOrder.nativeOrder()); }

And then replace:

ByteArray.getChar(array, 42)

With

asBuffer(array).getChar(42);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also... in a lot of cases where ByteArray is used (DataXYZStream, ObjectXYZStream) the array being used is a field in the class. So the byte buffer creation can definitively be amortized (or the code changed to work on buffers instead of arrays).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Unsafe-based writing will be used by Integer.toString and Long.toString as well; in those cases, will creating a ByteBuffer wrapper be overkill?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Unsafe-based writing will be used by Integer.toString and Long.toString as well; in those cases, will creating a ByteBuffer wrapper be overkill?

Integer/Long are very core classes so I assume they can use Unsafe if needed, they probably want as few dependences as possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, I ran LoopOverNonConstantHeap on the 3700x platform, and the performance of ByteBuffer was also poor:

I finally see it.

Benchmark                           (polluteProfile)  Mode  Cnt  Score   Error  Units
LoopOverNonConstantHeap.BB_get                 false  avgt   30  1.801 ± 0.020  ns/op
LoopOverNonConstantHeap.unsafe_get             false  avgt   30  0.567 ± 0.007  

It seems that, between updating JMH and rebuilding the JDK from scratch, something did the trick.

While I knew that random access on a BB is slower than Unsafe (as there's an extra check), whereas looped access is as fast (as C2 is good at hoisting the checks outside the loop, as shown in the benchmark). Note also that we are in the nanosecond realm, so each instruction here counts.

Is there any benchmark for DataInput/Output stream that can be used? I mean, it would be interesting to understand how these numbers translate when running the stuff that is built on top.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any benchmark for DataInput/Output stream that can be used? I mean, it would be interesting to understand how these numbers translate when running the stuff that is built on top.

I've tried to run the benchmark in test/micro/java/io/DataInputStream.java. This is the baseline:

Benchmark                     Mode  Cnt  Score   Error  Units
DataInputStreamTest.readChar  avgt   20  7.583 ± 0.026  us/op
DataInputStreamTest.readInt   avgt   20  3.804 ± 0.045  us/op

And this is with a patch similar to the one I shared above, to use ByteBuffer internally:

Benchmark                     Mode  Cnt  Score   Error  Units
DataInputStreamTest.readChar  avgt   20  7.594 ± 0.106  us/op
DataInputStreamTest.readInt   avgt   20  3.795 ± 0.030  us/op

There does not seem to be any extra overhead. That said, access occurs in a counted loop, and in these cases we know buffer/segment access is optimized quite well.

I believe the question here is: do we have benchmark which are representative of the kind of gain that would be introduced by micro-optimizing ByteArray? It can be quite tricky to estimate real benefits from synthetic benchmark on the ByteArray class, especially when fetching a single element outside of a loop - as those are not representative of how the clients will use this. I note that the original benchmark made by Per used a loop with two iterations to assess the cost of the ByteArray operations:

http://minborgsjavapot.blogspot.com/2023/01/java-21-performance-improvements.html

If I change the benchmark to do 2 iterations, I see this:

Benchmark                      Mode  Cnt       Score       Error   Units
ByteArray.readByte            thrpt    5  704199.172 ± 34101.508  ops/ms
ByteArray.readByteFromBuffer  thrpt    5  474321.828 ±  6588.471  ops/ms
ByteArray.readInt             thrpt    5  662411.181 ±  4470.951  ops/ms
ByteArray.readIntFromBuffer   thrpt    5  496900.429 ±  3705.737  ops/ms
ByteArray.readLong            thrpt    5  665138.063 ±  5944.814  ops/ms
ByteArray.readLongFromBuffer  thrpt    5  517781.548 ± 27106.331  ops/ms

The more the iterations, the less the cost (and you don't need many iterations to break even). This probably explains why the DataInputStream benchmark doesn't change - there's 1024 iterations in there.

I guess all this is to say that excessively focussing on microbenchmark of a simple class such as ByteArray in conditions that are likely unrealistic (e.g. single access) is IMHO the wrong way to look at things, as ByteArray is mostly used by classes that most definitively will read more than one value at a time (including classfile API).

So, also IMHO, we should try to measure the use cases we care about of the higher-level API we care about (I/O streams, classfile) and then see if adding Unsafe/VarHandle/ByteBuffer access in here is going to lead to any benefit at all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some feedback about this discussion:

  • I agree that the DataInput/OutputStreams should maybe use ByteBuffer directly as they use buffering already. So the patch above looks fine. In my project Apache Lucene (which has many performance critical methods like this), we have already implemented ByteBuffer based access like this for all IO-stream-based classes (we call then DataInput/DataOutput). I don't know why you have seen differences in using a ByteBuffer as final field in the class. That's common and used in most frameworks out there (NETTY,...) and is bullet proof (unless theres a bug in optimizer which sometimes happened in the past).
  • We noticed that wrapping a byte array on each access by ByteBuffer causes a lot of overhead and GC activity if used in hot loops. In addition we have seen cases where it is not optimized anymore (not sure why). @mcimadamore: You remember the similar discussions about the MemorySegment slices and copying them around between heap/foreign? Maybe inside the JDK you can do better by using @ForceInline. Our code can't do this, so we try to avoid creating instances of classes in such low-level code.
  • The original VarHandle approach is now used in Lucene's code at all places (basically the idea to use VarHandles for this class was suggested by me a while back). We often have byte arrays and can't wrap them as ByteBuffer on each call (because its not always inlined). For code outside of the JDK this looks like the best approach to have fast access to short/int/long values at specific (not necessarily aligned) positions. We have seen LZ4 compression getting much faster after changing the code from manually constructing logs/floats from bytes like in the reference code. With ByteBuffer it was often getting slower (depending on how it was called, I think because we can't do @ForceInline in code outside the JDK.

Generally: A class like this is very nice and also very much needed in code outside the JDK. A lot of code like encoding/decoding network bytes or compression algorithms often has the pattern that they want to read primitive types from byte arrays from. The overhead with wrapping looks bad in code and also causes long startup times and sometimes also OOM (if used multithreaded from different threads hammering the byte array accessors). Also you don not want to write a LZ4 decompressor using ByteBuffer as its only source of data... :-(

So have you thought of making this low-level classes public so we outside users no longer need to deal with VarHandles?

Maybe java.util.ByteArrays with solely static methods. The internal implementation of such a useful basic utility class could definitly be using Unsafe internally, so I would leave out the discussion here. If you use Unsafe there are no surprises! Personally I have no problem with the current implementation in this PR! I would just put little/big endian impl in the same class and move it to java.util (this is just my comment about this, coming from a library which does this low level stuff all the time).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So have you thought of making this low-level classes public so we outside users no longer need to deal with VarHandles?

I believe this is beyond the scope of this PR.

As for what do we do in the JDK, I can see few options:

  1. We keep things as they are in current mainline.
  2. We keep changes in this PR.
  3. We rewrite most uses of ByteArray in java.io to use BB and remove ByteArray
  4. We remove ByteArray and provide some static helper function to generate an unsafe offset from an array

I agree with @uschindler that wrapping stuff in ByteBuffer "on the fly" might be problematic for code that is not inlined, so I don't think we should do that.

I have to admit that I'm a little unclear as to what the goal of this PR is. Initially, it started as an "improve startup" effort, which then morphed into a "let's make ByteArray" more usable, even for other clients (like classfile API), or Long::toString. I'm unsure about the latter use cases, because (a) Long/Integer are core classes and should probably use Unsafe directly, where needed and (b) for classfile API, using ByteBuffer seems a good candidate on paper (of course there is the unknown of how well the byte buffer access will optimize in the classfile API code - but if there's more than one access on the same buffer, we should be more than ok).

I'd like to add some more words of caution against the synthetic benchmarks that we tried above. These benchmarks are quite peculiar, for at least two reasons:

  • we only ever access one element
  • the accessed offset is always zero

No general API can equal Unsafe under this set of conditions. When playing with the benchmark I realize that every little thing mattered (we're really measuring the number of instructions emitted by C2) - for instance, the fact that when access occurs with a byte buffer, the underlying array and limit have to be fetched from their fields has a cost. Also, the fact that ByteBuffer has a hierarchy has an even bigger cost (as C2 has to make sure you are really invoking HeapByteBuffer). The mutable endianness state in byte buffer also adds up to the noise. The above is what ends up in a big fat "2x slower" label.

That said, all these "factors" are only relevant because we're looking at a single buffer operation. In fact, all such costs can be easily be amortized as soon as there more than one access. Or as soon as you start accessing offsets that are not known statically (unlike in the benchmark).

So, there's a question of what's the code idiom that leads to the absolute fastest code (and I agree that Unsafe + static wrappers seems the best here). And then there's the question of "but, what do we need to get the performance number/startup behavior we want". I feel the important question is the second, but we keep arguing about the former.

And, to assess that second question, we need to understand better what the goals are (which, so far, seems a bit fuzzy).

Copy link
Member

@uschindler uschindler Jul 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So have you thought of making this low-level classes public so we outside users no longer need to deal with VarHandles?

I believe this is beyond the scope of this PR.

Sure, I brought this up here but yes, it is not really the scope of this PR. It is just another idea that this class could be of more wide use although outside of this PR and also outside of Lucene. Actually it would be nice to have it public, but I know this involves creating a JEP and so on. If there's interest I could start on proposing something like this on mailing list, later creating a JEP or whatever else is needed.

P.S.: Actually for a 3rd party user the whole thing is not much complicated. You only need a class to allocate the VarHandles and then use them from code, you don't even need the wrapper methods (although the ymake it nicer to read and you don't need the cast of return value). As there is no security involved, one can have those VarHandles as public static fields in some utility class: https://lucene.apache.org/core/9_7_0/core/org/apache/lucene/util/BitUtil.html; Usage of them is quite simple then: https://github.com/apache/lucene/blob/59c56a0aed9a43d24c676376b5d50c5c6518e3bc/lucene/core/src/java/org/apache/lucene/store/ByteArrayDataInput.java#L96 (there are many of those throughout Lucene's code)

So I agree with your ideas, we have to decide what is best for this PR. I tend to think that those 2 options are good:

  • Use ByteBuffer in classfile API
  • commit the PR as proposed here (looks fine to me).

return (long) Preconditions.checkIndex(offset, array.length - typeBytes + 1, Preconditions.AIOOBE_FORMATTER)
+ Unsafe.ARRAY_BYTE_BASE_OFFSET;
}

/*
* Methods for unpacking primitive values from byte arrays starting at
Expand All @@ -62,6 +70,7 @@ private ByteArray() {
* the range [0, array.length - 1]
* @see #setBoolean(byte[], int, boolean)
*/
@ForceInline
public static boolean getBoolean(byte[] array, int offset) {
return array[offset] != 0;
}
Expand All @@ -78,8 +87,12 @@ public static boolean getBoolean(byte[] array, int offset) {
* the range [0, array.length - 2]
* @see #setChar(byte[], int, char)
*/
@ForceInline
public static char getChar(byte[] array, int offset) {
return (char) CHAR.get(array, offset);
return UNSAFE.getCharUnaligned(
array,
arrayOffset(array, Character.BYTES, offset),
true);
}

/**
Expand All @@ -95,8 +108,12 @@ public static char getChar(byte[] array, int offset) {
* the range [0, array.length - 2]
* @see #setShort(byte[], int, short)
*/
@ForceInline
public static short getShort(byte[] array, int offset) {
return (short) SHORT.get(array, offset);
return UNSAFE.getShortUnaligned(
array,
arrayOffset(array, Short.BYTES, offset),
true);
}

/**
Expand All @@ -112,8 +129,9 @@ public static short getShort(byte[] array, int offset) {
* the range [0, array.length - 2]
* @see #setUnsignedShort(byte[], int, int)
*/
@ForceInline
public static int getUnsignedShort(byte[] array, int offset) {
return Short.toUnsignedInt((short) SHORT.get(array, offset));
return Short.toUnsignedInt(getShort(array, offset));
}

/**
Expand All @@ -128,48 +146,47 @@ public static int getUnsignedShort(byte[] array, int offset) {
* the range [0, array.length - 4]
* @see #setInt(byte[], int, int)
*/
@ForceInline
public static int getInt(byte[] array, int offset) {
return (int) INT.get(array, offset);
return UNSAFE.getIntUnaligned(
array,
arrayOffset(array, Integer.BYTES, offset),
true);
}

/**
* {@return a {@code float} from the provided {@code array} at the given {@code offset}
* {@return an {@code unsigned int} from the provided {@code array} at the given {@code offset}
* using big endian order}.
* <p>
* Variants of {@linkplain Float#NaN } values are canonized to a single NaN value.
* <p>
* There are no access alignment requirements.
*
* @param array to get a value from.
* @param offset where extraction in the array should begin
* @return an {@code long} representing an unsigned int from the array
* @throws IndexOutOfBoundsException if the provided {@code offset} is outside
* the range [0, array.length - 4]
* @see #setFloat(byte[], int, float)
* @see #setUnsignedInt(byte[], int, long)
*/
public static float getFloat(byte[] array, int offset) {
// Using Float.intBitsToFloat collapses NaN values to a single
// "canonical" NaN value
return Float.intBitsToFloat((int) INT.get(array, offset));
@ForceInline
public static long getUnsignedInt(byte[] array, int offset) {
return Integer.toUnsignedLong(getInt(array, offset));
}

/**
* {@return a {@code float} from the provided {@code array} at the given {@code offset}
* using big endian order}.
* <p>
* Variants of {@linkplain Float#NaN } values are silently read according
* to their bit patterns.
* <p>
* There are no access alignment requirements.
*
* @param array to get a value from.
* @param offset where extraction in the array should begin
* @throws IndexOutOfBoundsException if the provided {@code offset} is outside
* the range [0, array.length - 4]
* @see #setFloatRaw(byte[], int, float)
* @see #setFloat(byte[], int, float)
*/
public static float getFloatRaw(byte[] array, int offset) {
// Just gets the bits as they are
return (float) FLOAT.get(array, offset);
@ForceInline
public static float getFloat(byte[] array, int offset) {
return Float.intBitsToFloat(getInt(array, offset));
}

/**
Expand All @@ -184,16 +201,18 @@ public static float getFloatRaw(byte[] array, int offset) {
* the range [0, array.length - 8]
* @see #setLong(byte[], int, long)
*/
@ForceInline
public static long getLong(byte[] array, int offset) {
return (long) LONG.get(array, offset);
return UNSAFE.getLongUnaligned(
array,
arrayOffset(array, Long.BYTES, offset),
true);
}

/**
* {@return a {@code double} from the provided {@code array} at the given {@code offset}
* using big endian order}.
* <p>
* Variants of {@linkplain Double#NaN } values are canonized to a single NaN value.
* <p>
* There are no access alignment requirements.
*
* @param array to get a value from.
Expand All @@ -202,30 +221,9 @@ public static long getLong(byte[] array, int offset) {
* the range [0, array.length - 8]
* @see #setDouble(byte[], int, double)
*/
@ForceInline
public static double getDouble(byte[] array, int offset) {
// Using Double.longBitsToDouble collapses NaN values to a single
// "canonical" NaN value
return Double.longBitsToDouble((long) LONG.get(array, offset));
}

/**
* {@return a {@code double} from the provided {@code array} at the given {@code offset}
* using big endian order}.
* <p>
* Variants of {@linkplain Double#NaN } values are silently read according to
* their bit patterns.
* <p>
* There are no access alignment requirements.
*
* @param array to get a value from.
* @param offset where extraction in the array should begin
* @throws IndexOutOfBoundsException if the provided {@code offset} is outside
* the range [0, array.length - 8]
* @see #setDoubleRaw(byte[], int, double)
*/
public static double getDoubleRaw(byte[] array, int offset) {
// Just gets the bits as they are
return (double) DOUBLE.get(array, offset);
return Double.longBitsToDouble(getLong(array, offset));
}

/*
Expand All @@ -244,6 +242,7 @@ public static double getDoubleRaw(byte[] array, int offset) {
* the range [0, array.length]
* @see #getBoolean(byte[], int)
*/
@ForceInline
public static void setBoolean(byte[] array, int offset, boolean value) {
array[offset] = (byte) (value ? 1 : 0);
}
Expand All @@ -261,8 +260,13 @@ public static void setBoolean(byte[] array, int offset, boolean value) {
* the range [0, array.length - 2]
* @see #getChar(byte[], int)
*/
@ForceInline
public static void setChar(byte[] array, int offset, char value) {
CHAR.set(array, offset, value);
UNSAFE.putCharUnaligned(
array,
arrayOffset(array, Character.BYTES, offset),
value,
true);
}

/**
Expand All @@ -278,8 +282,13 @@ public static void setChar(byte[] array, int offset, char value) {
* the range [0, array.length - 2]
* @see #getShort(byte[], int)
*/
@ForceInline
public static void setShort(byte[] array, int offset, short value) {
SHORT.set(array, offset, value);
UNSAFE.putShortUnaligned(
array,
arrayOffset(array, Short.BYTES, offset),
value,
true);
}

/**
Expand All @@ -295,8 +304,9 @@ public static void setShort(byte[] array, int offset, short value) {
* the range [0, array.length - 2]
* @see #getUnsignedShort(byte[], int)
*/
@ForceInline
public static void setUnsignedShort(byte[] array, int offset, int value) {
SHORT.set(array, offset, (short) (char) value);
setShort(array, offset, (short) (char) value);
}

/**
Expand All @@ -312,8 +322,31 @@ public static void setUnsignedShort(byte[] array, int offset, int value) {
* the range [0, array.length - 4]
* @see #getInt(byte[], int)
*/
@ForceInline
public static void setInt(byte[] array, int offset, int value) {
INT.set(array, offset, value);
UNSAFE.putIntUnaligned(
array,
arrayOffset(array, Integer.BYTES, offset),
value,
true);
}

/**
* Sets (writes) the provided {@code value} using big endian order into
* the provided {@code array} beginning at the given {@code offset}.
* <p>
* There are no access alignment requirements.
*
* @param array to set (write) a value into
* @param offset where setting (writing) in the array should begin
* @param value value to set in the array
* @throws IndexOutOfBoundsException if the provided {@code offset} is outside
* the range [0, array.length - 4]
* @see #getUnsignedInt(byte[], int)
*/
@ForceInline
public static void setUnsignedInt(byte[] array, int offset, long value) {
setInt(array, offset, (int) value);
}

/**
Expand All @@ -331,10 +364,11 @@ public static void setInt(byte[] array, int offset, int value) {
* the range [0, array.length - 2]
* @see #getFloat(byte[], int)
*/
@ForceInline
public static void setFloat(byte[] array, int offset, float value) {
// Using Float.floatToIntBits collapses NaN values to a single
// "canonical" NaN value
INT.set(array, offset, Float.floatToIntBits(value));
setInt(array, offset, Float.floatToIntBits(value));
}

/**
Expand All @@ -351,11 +385,12 @@ public static void setFloat(byte[] array, int offset, float value) {
* @param value value to set in the array
* @throws IndexOutOfBoundsException if the provided {@code offset} is outside
* the range [0, array.length - 2]
* @see #getFloatRaw(byte[], int)
* @see #getFloat(byte[], int)
*/
@ForceInline
public static void setFloatRaw(byte[] array, int offset, float value) {
// Just sets the bits as they are
FLOAT.set(array, offset, value);
setInt(array, offset, Float.floatToRawIntBits(value));
}

/**
Expand All @@ -371,8 +406,13 @@ public static void setFloatRaw(byte[] array, int offset, float value) {
* the range [0, array.length - 4]
* @see #getLong(byte[], int)
*/
@ForceInline
public static void setLong(byte[] array, int offset, long value) {
LONG.set(array, offset, value);
UNSAFE.putLongUnaligned(
array,
arrayOffset(array, Long.BYTES, offset),
value,
true);
}

/**
Expand All @@ -390,10 +430,11 @@ public static void setLong(byte[] array, int offset, long value) {
* the range [0, array.length - 2]
* @see #getDouble(byte[], int)
*/
@ForceInline
public static void setDouble(byte[] array, int offset, double value) {
// Using Double.doubleToLongBits collapses NaN values to a single
// "canonical" NaN value
LONG.set(array, offset, Double.doubleToLongBits(value));
setLong(array, offset, Double.doubleToLongBits(value));
}

/**
Expand All @@ -410,15 +451,12 @@ public static void setDouble(byte[] array, int offset, double value) {
* @param value value to set in the array
* @throws IndexOutOfBoundsException if the provided {@code offset} is outside
* the range [0, array.length - 2]
* @see #getDoubleRaw(byte[], int)
* @see #getDouble(byte[], int)
*/
@ForceInline
public static void setDoubleRaw(byte[] array, int offset, double value) {
// Just sets the bits as they are
DOUBLE.set(array, offset, value);
}

private static VarHandle create(Class<?> viewArrayClass) {
return MethodHandles.byteArrayViewVarHandle(viewArrayClass, ByteOrder.BIG_ENDIAN);
setLong(array, offset, Double.doubleToRawLongBits(value));
}

}
Loading