diff --git a/src/java.base/share/classes/jdk/internal/foreign/SegmentBulkOperations.java b/src/java.base/share/classes/jdk/internal/foreign/SegmentBulkOperations.java index 11f1914a707..d928f8fc425 100644 --- a/src/java.base/share/classes/jdk/internal/foreign/SegmentBulkOperations.java +++ b/src/java.base/share/classes/jdk/internal/foreign/SegmentBulkOperations.java @@ -28,7 +28,9 @@ package jdk.internal.foreign; import jdk.internal.misc.ScopedMemoryAccess; import jdk.internal.util.Architecture; import jdk.internal.util.ArraysSupport; +import jdk.internal.util.ByteArrayLittleEndian; import jdk.internal.vm.annotation.ForceInline; +import jdk.internal.vm.annotation.Stable; import java.lang.foreign.MemorySegment; @@ -156,6 +158,94 @@ public final class SegmentBulkOperations { } } + private static final @Stable int[] POWERS_OF_31 = new int[]{ + 0x0000001f, 0x000003c1, 0x0000745f, 0x000e1781, + 0x01b4d89f, 0x34e63b41, 0x67e12cdf, 0x94446f01}; + + /** + * {@return a 32-bit hash value calculated from the content in the provided + * {@code segment} between the provided offsets} + *

+ * The method is implemented as a 32-bit polynomial hash function equivalent to: + * {@snippet lang=java : + * final long length = toOffset - fromOffset; + * segment.checkBounds(fromOffset, length); + * int result = 1; + * for (long i = fromOffset; i < toOffset; i++) { + * result = 31 * result + segment.get(JAVA_BYTE, i); + * } + * return result; + * } + * but is potentially more performant. + * + * @param segment from which a content hash should be computed + * @param fromOffset starting offset (inclusive) in the segment + * @param toOffset ending offset (non-inclusive) in the segment + * @throws WrongThreadException if this method is called from a thread {@code T}, + * such that {@code srcSegment.isAccessibleBy(T) == false} + * @throws IllegalStateException if the {@linkplain MemorySegment#scope() scope} + * associated with {@code segment} is not + * {@linkplain MemorySegment.Scope#isAlive() alive} + * @throws IndexOutOfBoundsException if either {@code fromOffset} or {@code toOffset} + * are {@code > segment.byteSize} + * @throws IndexOutOfBoundsException if either {@code fromOffset} or {@code toOffset} + * are {@code < 0} + * @throws IndexOutOfBoundsException if {@code toOffset - fromOffset} is {@code < 0} + */ + @ForceInline + public static int contentHash(AbstractMemorySegmentImpl segment, long fromOffset, long toOffset) { + final long length = toOffset - fromOffset; + segment.checkBounds(fromOffset, length); + if (length == 0) { + // The state has to be checked explicitly for zero-length segments + segment.scope.checkValidState(); + return 1; + } + int result = 1; + final long longBytes = length & ((1L << 62) - 8); + final long limit = fromOffset + longBytes; + for (; fromOffset < limit; fromOffset += 8) { + long val = SCOPED_MEMORY_ACCESS.getLongUnaligned(segment.sessionImpl(), segment.unsafeGetBase(), segment.unsafeGetOffset() + fromOffset, !Architecture.isLittleEndian()); + result = result * POWERS_OF_31[7] + + ((byte) (val >>> 56)) * POWERS_OF_31[6] + + ((byte) (val >>> 48)) * POWERS_OF_31[5] + + ((byte) (val >>> 40)) * POWERS_OF_31[4] + + ((byte) (val >>> 32)) * POWERS_OF_31[3] + + ((byte) (val >>> 24)) * POWERS_OF_31[2] + + ((byte) (val >>> 16)) * POWERS_OF_31[1] + + ((byte) (val >>> 8)) * POWERS_OF_31[0] + + ((byte) val); + } + int remaining = (int) (length - longBytes); + // 0...0X00 + if (remaining >= 4) { + int val = SCOPED_MEMORY_ACCESS.getIntUnaligned(segment.sessionImpl(), segment.unsafeGetBase(), segment.unsafeGetOffset() + fromOffset, !Architecture.isLittleEndian()); + result = result * POWERS_OF_31[3] + + ((byte) (val >>> 24)) * POWERS_OF_31[2] + + ((byte) (val >>> 16)) * POWERS_OF_31[1] + + ((byte) (val >>> 8)) * POWERS_OF_31[0] + + ((byte) val); + fromOffset += 4; + remaining -= 4; + } + // 0...00X0 + if (remaining >= 2) { + short val = SCOPED_MEMORY_ACCESS.getShortUnaligned(segment.sessionImpl(), segment.unsafeGetBase(), segment.unsafeGetOffset() + fromOffset, !Architecture.isLittleEndian()); + result = result * POWERS_OF_31[1] + + ((byte) (val >>> 8)) * POWERS_OF_31[0] + + ((byte) val); + fromOffset += 2; + remaining -= 2; + } + // 0...000X + if (remaining == 1) { + byte val = SCOPED_MEMORY_ACCESS.getByte(segment.sessionImpl(), segment.unsafeGetBase(), segment.unsafeGetOffset() + fromOffset); + result = result * POWERS_OF_31[0] + + val; + } + return result; + } + @ForceInline public static long mismatch(AbstractMemorySegmentImpl src, long srcFromOffset, long srcToOffset, AbstractMemorySegmentImpl dst, long dstFromOffset, long dstToOffset) { diff --git a/test/jdk/java/foreign/TestSegmentBulkOperationsContentHash.java b/test/jdk/java/foreign/TestSegmentBulkOperationsContentHash.java new file mode 100644 index 00000000000..96b73e7eee6 --- /dev/null +++ b/test/jdk/java/foreign/TestSegmentBulkOperationsContentHash.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary Test SegmentBulkOperations::contentHash + * @modules java.base/jdk.internal.foreign + * @run junit TestSegmentBulkOperationsContentHash + */ + +import jdk.internal.foreign.AbstractMemorySegmentImpl; +import jdk.internal.foreign.SegmentBulkOperations; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +final class TestSegmentBulkOperationsContentHash { + + @ParameterizedTest + @MethodSource("sizes") + @Disabled + void testHashValues(int len) { + try (var arena = Arena.ofConfined()) { + for (int i = Byte.MIN_VALUE; i <= Byte.MAX_VALUE ; i++) { + var segment = arena.allocate(len); + segment.fill((byte) i); + int hash = hash(segment); + int expected = Arrays.hashCode(segment.toArray(ValueLayout.JAVA_BYTE)); + assertEquals(expected, hash, Arrays.toString(segment.toArray(ValueLayout.JAVA_BYTE))); + } + } + } + + @ParameterizedTest + @MethodSource("sizes") + void testOffsets(int len) { + if (len < 2) { + return; + } + try (var arena = Arena.ofConfined()) { + for (int i = Byte.MIN_VALUE; i <= Byte.MAX_VALUE ; i++) { + var segment = arena.allocate(len); + segment.fill((byte) i); + int hash = hash(segment, 1, segment.byteSize() - 1); + MemorySegment slice = segment.asSlice(1, segment.byteSize() - 2); + byte[] arr = slice.toArray(ValueLayout.JAVA_BYTE); + System.out.println(Arrays.toString(arr)); + int expected = Arrays.hashCode(arr); + assertEquals(expected, hash, Arrays.toString(segment.toArray(ValueLayout.JAVA_BYTE))); + } + } + } + + @ParameterizedTest + @MethodSource("sizes") + void testOutOfBounds(int len) { + try (var arena = Arena.ofConfined()) { + var segment = arena.allocate(len); + assertThrows(IndexOutOfBoundsException.class, + () -> hash(segment, 0, segment.byteSize() + 1)); + assertThrows(IndexOutOfBoundsException.class, + () -> hash(segment, 0, -1)); + assertThrows(IndexOutOfBoundsException.class, + () -> hash(segment, -1, 0)); + if (len > 2) { + assertThrows(IndexOutOfBoundsException.class, + () -> hash(segment, 2, 1)); + } + } + } + + @ParameterizedTest + @MethodSource("sizes") + void testConfinement(int len) { + try (var arena = Arena.ofConfined()) { + var segment = arena.allocate(len); + AtomicReference ex = new AtomicReference<>(); + CompletableFuture future = CompletableFuture.runAsync(() -> { + try { + hash(segment); + } catch (RuntimeException e) { + ex.set(e); + } + }); + future.join(); + assertInstanceOf(WrongThreadException.class, ex.get()); + } + } + + @ParameterizedTest + @MethodSource("sizes") + void testScope(int len) { + var arena = Arena.ofConfined(); + var segment = arena.allocate(len); + arena.close(); + assertThrows(IllegalStateException.class, () -> hash(segment)); + } + + private static int hash(MemorySegment segment) { + return hash(segment, 0, segment.byteSize()); + } + + private static int hash(MemorySegment segment, long fromOffset, long toOffset) { + return SegmentBulkOperations.contentHash((AbstractMemorySegmentImpl) segment, fromOffset, toOffset); + } + + private static final int MAX_SIZE = 1 << 10; + + private static Stream sizes() { + return IntStream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 23, 32, 63, 128, 256, 511, MAX_SIZE) + .boxed() + .map(Arguments::of); + } + +} diff --git a/test/micro/org/openjdk/bench/java/lang/foreign/SegmentBulkHash.java b/test/micro/org/openjdk/bench/java/lang/foreign/SegmentBulkHash.java new file mode 100644 index 00000000000..927a7d3fb1f --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/foreign/SegmentBulkHash.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +package org.openjdk.bench.java.lang.foreign; + +import jdk.internal.foreign.AbstractMemorySegmentImpl; +import jdk.internal.foreign.SegmentBulkOperations; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import static java.lang.foreign.ValueLayout.JAVA_BYTE; + +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(value = 3, jvmArgs = {"--add-exports=java.base/jdk.internal.foreign=ALL-UNNAMED"}) +public class SegmentBulkHash { + + @Param({"8", "64"}) + public int ELEM_SIZE; + + byte[] array; + AbstractMemorySegmentImpl heapSegment; + AbstractMemorySegmentImpl nativeSegment; + + @Setup + public void setup() { + // Always use the same alignment regardless of size + nativeSegment = (AbstractMemorySegmentImpl) Arena.ofAuto().allocate(ELEM_SIZE, 16); + var rnd = new Random(42); + for (int i = 0; i < ELEM_SIZE; i++) { + nativeSegment.set(JAVA_BYTE, i, (byte) rnd.nextInt(Byte.MIN_VALUE, Byte.MAX_VALUE)); + } + array = nativeSegment.toArray(JAVA_BYTE); + heapSegment = (AbstractMemorySegmentImpl) MemorySegment.ofArray(array); + } + + @Benchmark + public int array() { + return Arrays.hashCode(array); + } + + @Benchmark + public int heapSegment() { + return SegmentBulkOperations.contentHash(heapSegment, 0, ELEM_SIZE); + } + + @Benchmark + public int nativeSegment() { + return SegmentBulkOperations.contentHash(nativeSegment, 0, ELEM_SIZE); + } + + @Benchmark + public int nativeSegmentJava() { + int result = 1; + for (long i = 0; i < ELEM_SIZE; i++) { + result = 31 * result + nativeSegment.get(JAVA_BYTE, i); + } + return result; + } + +} +