diff --git a/code/libraries/array/src/main/java/nu/marginalia/array/LongArray.java b/code/libraries/array/src/main/java/nu/marginalia/array/LongArray.java index cb334b07..c58014d0 100644 --- a/code/libraries/array/src/main/java/nu/marginalia/array/LongArray.java +++ b/code/libraries/array/src/main/java/nu/marginalia/array/LongArray.java @@ -6,6 +6,7 @@ import nu.marginalia.array.algo.LongArraySort; import nu.marginalia.array.algo.LongArrayTransformations; import nu.marginalia.array.delegate.ShiftedLongArray; import nu.marginalia.array.page.SegmentLongArray; +import nu.marginalia.array.page.UnsafeLongArray; import java.lang.foreign.Arena; @@ -15,7 +16,7 @@ public interface LongArray extends LongArrayBase, LongArrayTransformations, Long @Deprecated static LongArray allocate(long size) { - return SegmentLongArray.onHeap(Arena.ofShared(), size); + return UnsafeLongArray.onHeap(Arena.ofShared(), size); } default LongArray shifted(long offset) { diff --git a/code/libraries/array/src/main/java/nu/marginalia/array/LongArrayFactory.java b/code/libraries/array/src/main/java/nu/marginalia/array/LongArrayFactory.java index 584a1605..28854b2d 100644 --- a/code/libraries/array/src/main/java/nu/marginalia/array/LongArrayFactory.java +++ b/code/libraries/array/src/main/java/nu/marginalia/array/LongArrayFactory.java @@ -1,6 +1,7 @@ package nu.marginalia.array; import nu.marginalia.array.page.SegmentLongArray; +import nu.marginalia.array.page.UnsafeLongArray; import java.io.IOException; import java.lang.foreign.Arena; @@ -9,43 +10,43 @@ import java.nio.file.Path; public class LongArrayFactory { public static LongArray onHeapConfined(long size) { - return SegmentLongArray.onHeap(Arena.ofConfined(), size); + return UnsafeLongArray.onHeap(Arena.ofConfined(), size); } public static LongArray onHeapShared(long size) { - return SegmentLongArray.onHeap(Arena.ofShared(), size); + return UnsafeLongArray.onHeap(Arena.ofShared(), size); } public static LongArray mmapForReadingConfined(Path filename) throws IOException { - return SegmentLongArray.fromMmapReadOnly(Arena.ofConfined(), filename, + return UnsafeLongArray.fromMmapReadOnly(Arena.ofConfined(), filename, 0, Files.size(filename) / 8); } public static LongArray mmapForReadingShared(Path filename) throws IOException { - return SegmentLongArray.fromMmapReadOnly(Arena.ofShared(), filename, + return UnsafeLongArray.fromMmapReadOnly(Arena.ofShared(), filename, 0, Files.size(filename) / 8); } public static LongArray mmapForModifyingConfined(Path filename) throws IOException { - return SegmentLongArray.fromMmapReadWrite(Arena.ofConfined(), filename, + return UnsafeLongArray.fromMmapReadWrite(Arena.ofConfined(), filename, 0, Files.size(filename)); } public static LongArray mmapForModifyingShared(Path filename) throws IOException { - return SegmentLongArray.fromMmapReadWrite(Arena.ofShared(), filename, + return UnsafeLongArray.fromMmapReadWrite(Arena.ofShared(), filename, 0, Files.size(filename) / 8); } public static LongArray mmapForWritingConfined(Path filename, long size) throws IOException { - return SegmentLongArray.fromMmapReadWrite(Arena.ofConfined(), filename, + return UnsafeLongArray.fromMmapReadWrite(Arena.ofConfined(), filename, 0, size); } public static LongArray mmapForWritingShared(Path filename, long size) throws IOException { - return SegmentLongArray.fromMmapReadWrite(Arena.ofShared(), filename, + return UnsafeLongArray.fromMmapReadWrite(Arena.ofShared(), filename, 0, size); } } diff --git a/code/libraries/array/src/main/java/nu/marginalia/array/page/UnsafeLongArray.java b/code/libraries/array/src/main/java/nu/marginalia/array/page/UnsafeLongArray.java new file mode 100644 index 00000000..4e5ea9ce --- /dev/null +++ b/code/libraries/array/src/main/java/nu/marginalia/array/page/UnsafeLongArray.java @@ -0,0 +1,181 @@ +package nu.marginalia.array.page; + +import nu.marginalia.array.ArrayRangeReference; +import nu.marginalia.array.LongArray; +import sun.misc.Unsafe; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.nio.ByteBuffer; +import java.nio.LongBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import static java.lang.foreign.ValueLayout.JAVA_LONG; + +/** Variant of SegmentLongArray that uses Unsafe to access the memory. + * */ +public class UnsafeLongArray implements PartitionPage, LongArray { + + private static final Unsafe unsafe = UnsafeProvider.getUnsafe(); + + @Nullable + private final Arena arena; + private final MemorySegment segment; + private boolean closed; + + UnsafeLongArray(MemorySegment segment, + @Nullable Arena arena) { + this.segment = segment; + this.arena = arena; + } + + public static UnsafeLongArray onHeap(Arena arena, long size) { + return new UnsafeLongArray(arena.allocate(WORD_SIZE*size, 8), arena); + } + + public static UnsafeLongArray fromMmapReadOnly(Arena arena, Path file, long offset, long size) throws IOException { + return new UnsafeLongArray( + mmapFile(arena, file, offset, size, FileChannel.MapMode.READ_ONLY, StandardOpenOption.READ), + arena); + } + + public static UnsafeLongArray fromMmapReadWrite(Arena arena, Path file, long offset, long size) throws IOException { + + return new UnsafeLongArray( + mmapFile(arena, file, offset, size, FileChannel.MapMode.READ_WRITE, + StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE), + arena); + } + + private static MemorySegment mmapFile(Arena arena, + Path file, + long offset, + long size, + FileChannel.MapMode mode, + OpenOption... openOptions) throws IOException + { + try (var channel = (FileChannel) Files.newByteChannel(file, openOptions)) { + + return channel.map(mode, + JAVA_LONG.byteSize() * offset, + JAVA_LONG.byteSize() * size, + arena); + } + catch (IOException ex) { + throw new IOException("Failed to map file " + file + " (" + offset + ":" + size + ")", ex); + } + } + + @Override + public LongArray range(long start, long end) { + return new UnsafeLongArray( + segment.asSlice( + start * JAVA_LONG.byteSize(), + (end-start) * JAVA_LONG.byteSize()), + null); + } + + @Override + public LongArray shifted(long start) { + return new UnsafeLongArray( + segment.asSlice(start * JAVA_LONG.byteSize()), + null); + } + + @Override + public long get(long at) { + try { + return unsafe.getLong(segment.address() + at * JAVA_LONG.byteSize()); + } + catch (IndexOutOfBoundsException ex) { + throw new IndexOutOfBoundsException("@" + at + "(" + 0 + ":" + segment.byteSize()/8 + ")"); + } + } + + @Override + public void get(long start, long end, long[] buffer) { + for (int i = 0; i < end - start; i++) { + buffer[i] = unsafe.getLong(segment.address() + (start + i) * JAVA_LONG.byteSize()); + } + } + + @Override + public void set(long at, long val) { + unsafe.putLong(segment.address() + at * JAVA_LONG.byteSize(), val); + } + + @Override + public void set(long start, long end, LongBuffer buffer, int bufferStart) { + for (int i = 0; i < end - start; i++) { + unsafe.putLong(segment.address() + (start + i) * JAVA_LONG.byteSize(), buffer.get(bufferStart + i)); + } + } + + @Override + public synchronized void close() { + if (arena != null && !closed) { + arena.close(); + } + closed = true; + } + + @Override + public long size() { + return segment.byteSize() / JAVA_LONG.byteSize(); + } + + @Override + public ByteBuffer getByteBuffer() { + return segment.asByteBuffer(); + } + + @Override + public void write(Path filename) throws IOException { + try (var arena = Arena.ofConfined()) { + var destSegment = UnsafeLongArray.fromMmapReadWrite(arena, filename, 0, segment.byteSize()); + + destSegment.segment.copyFrom(segment); + destSegment.force(); + } + } + + @Override + public void force() { + if (segment.isMapped()) { + segment.force(); + } + } + + public ArrayRangeReference directRangeIfPossible(long start, long end) { + return new ArrayRangeReference<>(this, start, end); + } + + @Override + public void transferFrom(FileChannel source, long sourceStart, long arrayStart, long arrayEnd) throws IOException { + + final int stride = 1024*1204*128; // Copy 1 GB at a time 'cause byte buffers are 'a byte buffering + + long ss = sourceStart; + for (long as = arrayStart; as < arrayEnd; as += stride, ss += stride) { + long ae = Math.min(as + stride, arrayEnd); + + long index = as * JAVA_LONG.byteSize(); + long length = (ae - as) * JAVA_LONG.byteSize(); + + var bufferSlice = segment.asSlice(index, length).asByteBuffer(); + + long startPos = ss * JAVA_LONG.byteSize(); + while (bufferSlice.position() < bufferSlice.capacity()) { + source.read(bufferSlice, startPos + bufferSlice.position()); + } + } + + } + +} diff --git a/code/libraries/array/src/main/java/nu/marginalia/array/page/UnsafeProvider.java b/code/libraries/array/src/main/java/nu/marginalia/array/page/UnsafeProvider.java new file mode 100644 index 00000000..4d7bed71 --- /dev/null +++ b/code/libraries/array/src/main/java/nu/marginalia/array/page/UnsafeProvider.java @@ -0,0 +1,15 @@ +package nu.marginalia.array.page; + +import sun.misc.Unsafe; + +public class UnsafeProvider { + public static Unsafe getUnsafe() { + try { + var field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } +}