diff --git a/src/java.base/share/classes/java/lang/Integer.java b/src/java.base/share/classes/java/lang/Integer.java index 41487a469b6a0..67c9dd29a8473 100644 --- a/src/java.base/share/classes/java/lang/Integer.java +++ b/src/java.base/share/classes/java/lang/Integer.java @@ -28,6 +28,7 @@ import jdk.internal.misc.CDS; import jdk.internal.misc.VM; import jdk.internal.util.DecimalDigits; +import jdk.internal.util.HexDigits; import jdk.internal.vm.annotation.ForceInline; import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.Stable; @@ -287,7 +288,24 @@ public static String toUnsignedString(int i, int radix) { * @since 1.0.2 */ public static String toHexString(int i) { - return toUnsignedString0(i, 4); + int mag = Integer.SIZE - Integer.numberOfLeadingZeros(i); + int len = Math.max(((mag + 3) >> 2), 1); + long x = HexDigits.hex8(i); + if (COMPACT_STRINGS) { + byte[] chars = new byte[len]; + do { + chars[--len] = (byte) x; + x >>>= 8; + } while (len > 0); + return new String(chars, LATIN1); + } else { + byte[] chars = new byte[len << 1]; + do { + StringUTF16.putChar(chars, --len, (byte) x); + x >>>= 8; + } while (len > 0); + return new String(chars, UTF16); + } } /** diff --git a/src/java.base/share/classes/java/lang/Long.java b/src/java.base/share/classes/java/lang/Long.java index 2fb2d18a78c80..87177cdef41c7 100644 --- a/src/java.base/share/classes/java/lang/Long.java +++ b/src/java.base/share/classes/java/lang/Long.java @@ -34,7 +34,9 @@ import java.util.Optional; import jdk.internal.misc.CDS; +import jdk.internal.misc.Unsafe; import jdk.internal.util.DecimalDigits; +import jdk.internal.util.HexDigits; import jdk.internal.vm.annotation.ForceInline; import jdk.internal.vm.annotation.IntrinsicCandidate; import jdk.internal.vm.annotation.Stable; @@ -43,6 +45,7 @@ import static java.lang.String.COMPACT_STRINGS; import static java.lang.String.LATIN1; import static java.lang.String.UTF16; +import static jdk.internal.misc.Unsafe.ARRAY_BYTE_BASE_OFFSET; /** * The {@code Long} class is the {@linkplain @@ -311,7 +314,38 @@ private static BigInteger toUnsignedBigInteger(long i) { * @since 1.0.2 */ public static String toHexString(long i) { - return toUnsignedString0(i, 4); + int mag = Long.SIZE - Long.numberOfLeadingZeros(i); + int len = Math.max(((mag + 3) >> 2), 1); + long x = HexDigits.hex8(i); + if (COMPACT_STRINGS) { + byte[] chars = new byte[len]; + if (len > 8) { + len -= 8; + Unsafe.getUnsafe().putLongUnaligned(chars, ARRAY_BYTE_BASE_OFFSET + len, x, true); + x = HexDigits.hex8(i >>> 32); + } + do { + chars[--len] = (byte) x; + x >>>= 8; + } while (len > 0); + return new String(chars, LATIN1); + } else { + byte[] chars = new byte[len << 1]; + byte b; + if (len > 8) { + for (int j = 0; j < 8; j++) { + b = (byte) x; + StringUTF16.putChar(chars, --len, b); + x >>>= 8; + } + x = HexDigits.hex8(i >>> 32); + } + do { + StringUTF16.putChar(chars, --len, (byte) x); + x >>>= 8; + } while (len > 0); + return new String(chars, UTF16); + } } /** diff --git a/src/java.base/share/classes/java/util/UUID.java b/src/java.base/share/classes/java/util/UUID.java index 5961fce9cb23c..8fb07cba04341 100644 --- a/src/java.base/share/classes/java/util/UUID.java +++ b/src/java.base/share/classes/java/util/UUID.java @@ -32,6 +32,7 @@ import jdk.internal.access.JavaLangAccess; import jdk.internal.access.SharedSecrets; import jdk.internal.util.ByteArrayLittleEndian; +import jdk.internal.util.HexDigits; /** * A class that represents an immutable universally unique identifier (UUID). @@ -468,16 +469,16 @@ public String toString() { buf[23] = '-'; // Although the UUID byte ordering is defined to be big-endian, ByteArrayLittleEndian is used here to optimize - // for the most common architectures. hex8 reverses the order internally. - ByteArrayLittleEndian.setLong(buf, 0, hex8(mostSigBits >>> 32)); - long x0 = hex8(mostSigBits); + // for the most common architectures. + ByteArrayLittleEndian.setLong(buf, 0, Long.reverseBytes(HexDigits.hex8(mostSigBits >>> 32))); + long x0 = Long.reverseBytes(HexDigits.hex8(mostSigBits)); ByteArrayLittleEndian.setInt(buf, 9, (int) x0); ByteArrayLittleEndian.setInt(buf, 14, (int) (x0 >>> 32)); - long x1 = hex8(leastSigBits >>> 32); + long x1 = Long.reverseBytes(HexDigits.hex8(leastSigBits >>> 32)); ByteArrayLittleEndian.setInt(buf, 19, (int) (x1)); ByteArrayLittleEndian.setInt(buf, 24, (int) (x1 >>> 32)); - ByteArrayLittleEndian.setLong(buf, 28, hex8(leastSigBits)); + ByteArrayLittleEndian.setLong(buf, 28, Long.reverseBytes(HexDigits.hex8(leastSigBits))); try { return jla.uncheckedNewStringNoRepl(buf, StandardCharsets.ISO_8859_1); @@ -486,83 +487,6 @@ public String toString() { } } - /** - * Efficiently converts 8 hexadecimal digits to their ASCII representation using SIMD-style vector operations. - * This method processes multiple digits in parallel by treating a long value as eight 8-bit lanes, - * achieving significantly better performance compared to traditional loop-based conversion. - * - *

The conversion algorithm works as follows: - *

-     * 1. Input expansion: Each 4-bit hex digit is expanded to 8 bits
-     * 2. Vector processing:
-     *    - Add 6 to each digit: triggers carry flag for a-f digits
-     *    - Mask with 0x10 pattern to isolate carry flags
-     *    - Calculate ASCII adjustment: (carry << 1) + (carry >> 1) - (carry >> 4)
-     *    - Add ASCII '0' base (0x30) and original value
-     * 3. Byte order adjustment for final output
-     * 
- * - *

Performance characteristics: - *

- * - *

ASCII conversion mapping: - *

- * - * @param input A long containing 8 hex digits (each digit must be 0-15) - * @return A long containing 8 ASCII bytes representing the hex digits - * - * @implNote The implementation leverages CPU vector processing capabilities through - * long integer operations. The algorithm is based on the observation that - * ASCII hex digits have a specific pattern that can be computed efficiently - * using carry flag manipulation. - * - * @example - *
-     * Input:  0xABCDEF01
-     * Output: 3130666564636261 ('1','0','f','e','d','c','b','a' in ASCII)
-     * 
- * - * @see Long#reverseBytes(long) - */ - private static long hex8(long i) { - // Expand each 4-bit group into 8 bits, spreading them out in the long value: 0xAABBCCDD -> 0xA0A0B0B0C0C0D0D - i = Long.expand(i, 0x0F0F_0F0F_0F0F_0F0FL); - - /* - * This method efficiently converts 8 hexadecimal digits simultaneously using vector operations - * The algorithm works as follows: - * - * For input values 0-15: - * - For digits 0-9: converts to ASCII '0'-'9' (0x30-0x39) - * - For digits 10-15: converts to ASCII 'a'-'f' (0x61-0x66) - * - * The conversion process: - * 1. Add 6 to each 4-bit group: i + 0x0606_0606_0606_0606L - * 2. Mask to get the adjustment flags: & 0x1010_1010_1010_1010L - * 3. Calculate the offset: (m << 1) + (m >> 1) - (m >> 4) - * - For 0-9: offset = 0 - * - For a-f: offset = 39 (to bridge the gap between '9' and 'a' in ASCII) - * 4. Add ASCII '0' base (0x30) and the original value - * 5. Reverse byte order for correct positioning - */ - long m = (i + 0x0606_0606_0606_0606L) & 0x1010_1010_1010_1010L; - - // Calculate final ASCII values and reverse bytes for proper ordering - return Long.reverseBytes( - ((m << 1) + (m >> 1) - (m >> 4)) - + 0x3030_3030_3030_3030L // Add ASCII '0' base to all digits - + i // Add original values - ); - } - /** * Returns a hash code for this {@code UUID}. * diff --git a/src/java.base/share/classes/jdk/internal/util/HexDigits.java b/src/java.base/share/classes/jdk/internal/util/HexDigits.java index 45b3970405922..4278712df7f64 100644 --- a/src/java.base/share/classes/jdk/internal/util/HexDigits.java +++ b/src/java.base/share/classes/jdk/internal/util/HexDigits.java @@ -109,4 +109,78 @@ public static short digitPair(int i, boolean ucase) { ? (short) (v - ((v & 0b0100_0000_0100_0000) >> 1)) : v; } + + /** + * Efficiently converts 8 hexadecimal digits to their ASCII representation using SIMD-style vector operations. + * This method processes multiple digits in parallel by treating a long value as eight 8-bit lanes, + * achieving significantly better performance compared to traditional loop-based conversion. + * + *

The conversion algorithm works as follows: + *

+     * 1. Input expansion: Each 4-bit hex digit is expanded to 8 bits
+     * 2. Vector processing:
+     *    - Add 6 to each digit: triggers carry flag for a-f digits
+     *    - Mask with 0x10 pattern to isolate carry flags
+     *    - Calculate ASCII adjustment: (carry << 1) + (carry >> 1) - (carry >> 4)
+     *    - Add ASCII '0' base (0x30) and original value
+     * 3. Byte order adjustment for final output
+     * 
+ * + *

Performance characteristics: + *

+ * + *

ASCII conversion mapping: + *

+ * + * @param input A long containing 8 hex digits (each digit must be 0-15) + * @return A long containing 8 ASCII bytes representing the hex digits + * + * @implNote The implementation leverages CPU vector processing capabilities through + * long integer operations. The algorithm is based on the observation that + * ASCII hex digits have a specific pattern that can be computed efficiently + * using carry flag manipulation. + * + * @example + *
+     * Input:  0xABCDEF01
+     * Output: 3130666564636261 ('1','0','f','e','d','c','b','a' in ASCII)
+     * 
+ * + */ + public static long hex8(long i) { + // Expand each 4-bit group into 8 bits, spreading them out in the long value: 0xAABBCCDD -> 0xA0A0B0B0C0C0D0D + i = Long.expand(i, 0x0F0F_0F0F_0F0F_0F0FL); + + /* + * This method efficiently converts 8 hexadecimal digits simultaneously using vector operations + * The algorithm works as follows: + * + * For input values 0-15: + * - For digits 0-9: converts to ASCII '0'-'9' (0x30-0x39) + * - For digits 10-15: converts to ASCII 'a'-'f' (0x61-0x66) + * + * The conversion process: + * 1. Add 6 to each 4-bit group: i + 0x0606_0606_0606_0606L + * 2. Mask to get the adjustment flags: & 0x1010_1010_1010_1010L + * 3. Calculate the offset: (m << 1) + (m >> 1) - (m >> 4) + * - For 0-9: offset = 0 + * - For a-f: offset = 39 (to bridge the gap between '9' and 'a' in ASCII) + * 4. Add ASCII '0' base (0x30) and the original value + * 5. Reverse byte order for correct positioning + */ + long m = (i + 0x0606_0606_0606_0606L) & 0x1010_1010_1010_1010L; + + // Calculate final ASCII values and reverse bytes for proper ordering + return ((m << 1) + (m >> 1) - (m >> 4)) + + 0x3030_3030_3030_3030L // Add ASCII '0' base to all digits + + i; // Add original values + } } diff --git a/test/micro/org/openjdk/bench/java/lang/Integers.java b/test/micro/org/openjdk/bench/java/lang/Integers.java index 43ceb5d18d207..3e158a33fabfc 100644 --- a/test/micro/org/openjdk/bench/java/lang/Integers.java +++ b/test/micro/org/openjdk/bench/java/lang/Integers.java @@ -44,7 +44,7 @@ @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @State(Scope.Thread) -@Warmup(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @Fork(3) public class Integers { @@ -58,6 +58,9 @@ public class Integers { private int[] intsSmall; private int[] intsBig; private int[] res; + private int[] hexsTiny; + private int[] hexsSmall; + private int[] hexsBig; @Setup public void setup() { @@ -68,11 +71,18 @@ public void setup() { intsSmall = new int[size]; intsBig = new int[size]; res = new int[size]; + hexsTiny = new int[size]; + hexsSmall = new int[size]; + hexsBig = new int[size]; for (int i = 0; i < size; i++) { strings[i] = "" + (r.nextInt(10000) - (5000)); intsTiny[i] = r.nextInt(99); intsSmall[i] = 100 * i + i + 103; intsBig[i] = ((100 * i + i) << 24) + 4543 + i * 4; + + hexsTiny[i] = r.nextInt(0xFF); + hexsSmall[i] = 0x100 * i + i + 0x103; + hexsBig[i] = ((0x100 * i + i) << 24) + 0x4543 + i * 4; } } @@ -114,6 +124,27 @@ public void toStringBig(Blackhole bh) { } } + @Benchmark + public void toHexStringTiny(Blackhole bh) { + for (int i : hexsTiny) { + bh.consume(Integer.toHexString(i)); + } + } + + @Benchmark + public void toHexStringSmall(Blackhole bh) { + for (int i : hexsSmall) { + bh.consume(Integer.toHexString(i)); + } + } + + @Benchmark + public void toHexStringBig(Blackhole bh) { + for (int i : hexsBig) { + bh.consume(Integer.toHexString(i)); + } + } + /** Performs expand on small values */ @Benchmark public void expand(Blackhole bh) { diff --git a/test/micro/org/openjdk/bench/java/lang/Longs.java b/test/micro/org/openjdk/bench/java/lang/Longs.java index 765d00e9fb90e..77ed5ce681598 100644 --- a/test/micro/org/openjdk/bench/java/lang/Longs.java +++ b/test/micro/org/openjdk/bench/java/lang/Longs.java @@ -54,6 +54,8 @@ public class Longs { private String[] strings; private long[] longArraySmall; private long[] longArrayBig; + private long[] hexsArraySmall; + private long[] hexsArrayBig; @Setup public void setup() { @@ -63,10 +65,14 @@ public void setup() { res = new long[size]; longArraySmall = new long[size]; longArrayBig = new long[size]; + hexsArraySmall = new long[size]; + hexsArrayBig = new long[size]; for (int i = 0; i < size; i++) { strings[i] = "" + (random.nextLong(10000) - 5000); longArraySmall[i] = 100L * i + i + 103L; longArrayBig[i] = ((100L * i + i) << 32) + 4543 + i * 4L; + hexsArraySmall[i] = 0x100L * i + i + 0x103L; + hexsArrayBig[i] = ((0x100L * i + i) << 32) + 0x4543 + i * 4L; } } @@ -93,6 +99,20 @@ public void toStringBig(Blackhole bh) { } } + @Benchmark + public void toHexStringSmall(Blackhole bh) { + for (long value : hexsArraySmall) { + bh.consume(Long.toHexString(value)); + } + } + + @Benchmark + public void toHexStringBig(Blackhole bh) { + for (long value : hexsArrayBig) { + bh.consume(Long.toHexString(value)); + } + } + /** Performs expand on small values */ @Benchmark public void expand(Blackhole bh) {