-
Notifications
You must be signed in to change notification settings - Fork 6.2k
8310843: Reimplement ByteArray and ByteArrayLittleEndian with Unsafe #14636
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
30289cc
ffcf508
9797ef4
340ad72
6b14fa7
8a507b9
20b7c87
8c3a208
e826902
4ac2632
217caa3
2b5a4b0
609170c
7cfef77
cb56e73
6560d35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
@@ -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}. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
Glavo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @ForceInline | ||
| static long arrayOffset(byte[] array, int typeBytes, int offset) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: And then replace: With
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Unsafe-based writing will be used by
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Integer/Long are very core classes so I assume they can use Unsafe if needed, they probably want as few dependences as possible.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I finally see it. 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I've tried to run the benchmark in test/micro/java/io/DataInputStream.java. This is the baseline: And this is with a patch similar to the one I shared above, to use ByteBuffer internally: 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: 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just some feedback about this discussion:
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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I believe this is beyond the scope of this PR. As for what do we do in the JDK, I can see few options:
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:
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).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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:
|
||
| 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 | ||
|
|
@@ -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; | ||
| } | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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)); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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)); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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. | ||
|
|
@@ -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)); | ||
| } | ||
|
|
||
| /* | ||
|
|
@@ -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); | ||
| } | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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)); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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)); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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)); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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)); | ||
| } | ||
|
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.