New posts
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Before/after equivalence check, no benchmarks: the same scripted operations run against the Unsafe version and the
|
||||
* replacement, and every observable result must match. Prints one line per check; exits 1 on the first mismatch.
|
||||
*
|
||||
* java Equivalence all three implementations (needs Unsafe memory access allowed or warned)
|
||||
* java Equivalence --new-only only VarHandle and FFM; run this one under --sun-misc-unsafe-memory-access=deny
|
||||
*/
|
||||
public class Equivalence {
|
||||
static int checks;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
boolean newOnly = args.length > 0 && args[0].equals("--new-only");
|
||||
|
||||
// ---- 1. field CAS: scripted single-thread ops ----
|
||||
List<Supplier<FieldCounter>> fields = new ArrayList<>();
|
||||
if (!newOnly) fields.add(UnsafeFieldCounter::new);
|
||||
fields.add(VarHandleFieldCounter::new);
|
||||
long[] ref = null;
|
||||
for (var f : fields) {
|
||||
long[] trace = fieldTrace(f.get());
|
||||
if (ref == null) ref = trace; else same("field trace", ref, trace);
|
||||
}
|
||||
System.out.println("field CAS: " + names(fields.size()) + " gave the same 2000-step trace");
|
||||
|
||||
// ---- 2. field CAS under contention: lost updates would show as a wrong total ----
|
||||
for (var f : fields) require("contended total", 400_000L, fieldTotal(f.get()));
|
||||
System.out.println("field getAndAdd: 4 threads x 100000 -> total 400000 on every implementation");
|
||||
|
||||
// ---- 3. off-heap array ----
|
||||
List<Supplier<OffHeapLongs>> offs = new ArrayList<>();
|
||||
if (!newOnly) offs.add(() -> new UnsafeOffHeapLongs(64));
|
||||
offs.add(() -> new FfmOffHeapLongs(64));
|
||||
long[] ref2 = null;
|
||||
for (var s : offs) {
|
||||
try (OffHeapLongs o = s.get()) {
|
||||
long[] trace = offHeapTrace(o);
|
||||
if (ref2 == null) ref2 = trace; else same("off-heap trace", ref2, trace);
|
||||
}
|
||||
}
|
||||
System.out.println("off-heap array: " + names(offs.size()) + " gave the same 2000-step trace");
|
||||
|
||||
for (var s : offs) {
|
||||
try (OffHeapLongs o = s.get()) {
|
||||
Thread[] ts = new Thread[4];
|
||||
for (int t = 0; t < ts.length; t++) { ts[t] = new Thread(() -> { for (int k = 0; k < 100_000; k++) o.getAndAdd(k % 4, 1); }); ts[t].start(); }
|
||||
for (Thread t : ts) t.join();
|
||||
long sum = 0; for (int i = 0; i < 4; i++) sum += o.get(i);
|
||||
require("off-heap contended sum", 400_000L, sum);
|
||||
}
|
||||
}
|
||||
System.out.println("off-heap getAndAdd: 4 threads x 100000 -> sum 400000 on every implementation");
|
||||
|
||||
// ---- 4. bulk copy ----
|
||||
for (var s : offs) {
|
||||
try (OffHeapLongs a = s.get(); OffHeapLongs b = s.get()) {
|
||||
for (int i = 0; i < a.length(); i++) a.set(i, i * 31L);
|
||||
b.copyFrom(a);
|
||||
for (int i = 0; i < a.length(); i++) require("copy[" + i + "]", i * 31L, b.get(i));
|
||||
a.clear();
|
||||
require("cleared", 0L, a.get(5));
|
||||
}
|
||||
}
|
||||
System.out.println("copyFrom / clear: identical on every implementation");
|
||||
|
||||
// ---- 5. byte[] views ----
|
||||
byte[] data = new byte[40];
|
||||
new Random(7).nextBytes(data);
|
||||
for (int off = 0; off <= 32; off++) {
|
||||
long viewed = ViewBytes.get(data, off);
|
||||
if (!newOnly) require("byte[] getLong @" + off, UnsafeBytes.get(data, off), viewed);
|
||||
}
|
||||
byte[] x = new byte[16], y = new byte[16];
|
||||
ViewBytes.put(x, 3, 0x0102030405060708L);
|
||||
if (!newOnly) { UnsafeBytes.put(y, 3, 0x0102030405060708L); require("byte[] put", java.util.Arrays.toString(y), java.util.Arrays.toString(x)); }
|
||||
System.out.println("byte[] long access at offsets 0..32: " + (newOnly ? "VarHandle view only" : "Unsafe and VarHandle view agree"));
|
||||
|
||||
// ---- 6. what the replacement adds: a bounds check instead of a read past the end ----
|
||||
try { ViewBytes.get(data, 36); System.out.println("view past end: NO EXCEPTION"); System.exit(1); }
|
||||
catch (IndexOutOfBoundsException e) { System.out.println("VarHandle view at offset 36 of a 40-byte array: " + e.getClass().getSimpleName()); }
|
||||
try (OffHeapLongs o = new FfmOffHeapLongs(4)) { o.get(4); System.out.println("ffm past end: NO EXCEPTION"); System.exit(1); }
|
||||
catch (IndexOutOfBoundsException e) { System.out.println("FFM read of element 4 in a 4-element segment: " + e.getClass().getSimpleName()); }
|
||||
OffHeapLongs closed = new FfmOffHeapLongs(4); closed.close();
|
||||
try { closed.get(0); System.out.println("ffm after close: NO EXCEPTION"); System.exit(1); }
|
||||
catch (IllegalStateException e) { System.out.println("FFM read after close: " + e.getClass().getSimpleName() + ": " + e.getMessage()); }
|
||||
|
||||
System.out.println("ALL " + checks + " CHECKS PASSED" + (newOnly ? " (new APIs only)" : ""));
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
static String names(int n) { return n == 2 ? "Unsafe and the replacement" : "the replacement alone"; }
|
||||
|
||||
static long fieldTotal(FieldCounter c) throws Exception {
|
||||
Thread[] ts = new Thread[4];
|
||||
for (int t = 0; t < ts.length; t++) { ts[t] = new Thread(() -> { for (int k = 0; k < 100_000; k++) c.getAndAdd(1); }); ts[t].start(); }
|
||||
for (Thread t : ts) t.join();
|
||||
return c.get();
|
||||
}
|
||||
|
||||
static long[] fieldTrace(FieldCounter c) {
|
||||
Random r = new Random(42);
|
||||
long[] out = new long[2000];
|
||||
for (int i = 0; i < out.length; i++) {
|
||||
switch (r.nextInt(3)) {
|
||||
case 0 -> c.set(r.nextInt(100));
|
||||
case 1 -> out[i] = c.cas(r.nextInt(100), r.nextInt(100)) ? 1 : 0;
|
||||
default -> out[i] = c.getAndAdd(r.nextInt(5));
|
||||
}
|
||||
out[i] = out[i] * 1_000_003L + c.get();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static long[] offHeapTrace(OffHeapLongs o) {
|
||||
Random r = new Random(99);
|
||||
long[] out = new long[2000];
|
||||
for (int i = 0; i < out.length; i++) {
|
||||
int idx = r.nextInt(o.length());
|
||||
switch (r.nextInt(3)) {
|
||||
case 0 -> o.set(idx, r.nextInt(1000));
|
||||
case 1 -> out[i] = o.cas(idx, r.nextInt(1000), r.nextInt(1000)) ? 1 : 0;
|
||||
default -> out[i] = o.getAndAdd(idx, r.nextInt(7));
|
||||
}
|
||||
out[i] = out[i] * 1_000_003L + o.get(idx);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static void same(String what, long[] a, long[] b) {
|
||||
for (int i = 0; i < a.length; i++) if (a[i] != b[i]) { System.out.println("MISMATCH " + what + " at step " + i); System.exit(1); }
|
||||
checks++;
|
||||
}
|
||||
static void require(String what, Object expected, Object actual) {
|
||||
checks++;
|
||||
if (!expected.equals(actual)) { System.out.println("MISMATCH " + what + ": expected " + expected + " got " + actual); System.exit(1); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import java.lang.foreign.Arena;
|
||||
import java.lang.foreign.MemorySegment;
|
||||
import java.lang.foreign.ValueLayout;
|
||||
import java.lang.invoke.VarHandle;
|
||||
|
||||
/** AFTER: the same array on java.lang.foreign (final in JDK 22). Bounds and lifetime are checked by the runtime. */
|
||||
public class FfmOffHeapLongs implements OffHeapLongs {
|
||||
// access coordinates: (MemorySegment, long byteOffset); this VarHandle exposes get/set/volatile/CAS/getAndAdd modes
|
||||
private static final VarHandle LONG = ValueLayout.JAVA_LONG.varHandle();
|
||||
private final Arena arena = Arena.ofShared(); // shared: other threads may touch the segment; closing is thread-safe
|
||||
private final MemorySegment seg;
|
||||
private final int length;
|
||||
|
||||
public FfmOffHeapLongs(int length) {
|
||||
this.length = length;
|
||||
this.seg = arena.allocate(8L * length, 8); // zero-initialised, 8-byte aligned
|
||||
}
|
||||
public int length() { return length; }
|
||||
public long get(int i) { return (long) LONG.getVolatile(seg, 8L * i); }
|
||||
public void set(int i, long v) { LONG.setVolatile(seg, 8L * i, v); }
|
||||
public boolean cas(int i, long e, long u) { return LONG.compareAndSet(seg, 8L * i, e, u); }
|
||||
public long getAndAdd(int i, long d) { return (long) LONG.getAndAdd(seg, 8L * i, d); }
|
||||
public void clear() { seg.fill((byte) 0); }
|
||||
public void copyFrom(OffHeapLongs o) {
|
||||
if (o instanceof FfmOffHeapLongs f) MemorySegment.copy(f.seg, 0, seg, 0, 8L * length); // one bulk copy, no loop
|
||||
else for (int i = 0; i < length; i++) set(i, o.get(i));
|
||||
}
|
||||
public void close() { arena.close(); }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/** A volatile long field that several threads bump with compare-and-swap. Three implementations live in sibling files. */
|
||||
public interface FieldCounter {
|
||||
long get();
|
||||
void set(long v);
|
||||
boolean cas(long expect, long update);
|
||||
long getAndAdd(long delta);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/** A fixed-size off-heap array of longs. Two implementations: Unsafe (before) and java.lang.foreign (after). */
|
||||
public interface OffHeapLongs extends AutoCloseable {
|
||||
int length();
|
||||
long get(int i);
|
||||
void set(int i, long v);
|
||||
boolean cas(int i, long expect, long update);
|
||||
long getAndAdd(int i, long delta);
|
||||
void clear();
|
||||
void copyFrom(OffHeapLongs other);
|
||||
@Override void close();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import java.lang.reflect.Field;
|
||||
import sun.misc.Unsafe;
|
||||
|
||||
/** The classic way every library gets an Unsafe: read the private static field "theUnsafe". */
|
||||
final class UnsafeAccess {
|
||||
private UnsafeAccess() {}
|
||||
|
||||
static Unsafe get() {
|
||||
try {
|
||||
Field f = Unsafe.class.getDeclaredField("theUnsafe");
|
||||
f.setAccessible(true);
|
||||
return (Unsafe) f.get(null);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import sun.misc.Unsafe;
|
||||
|
||||
/** BEFORE: reading and writing a long at any offset of a byte[] with the "bimodal" Unsafe.getLong(Object, long). No bounds check. */
|
||||
public final class UnsafeBytes {
|
||||
private UnsafeBytes() {}
|
||||
private static final Unsafe U = UnsafeAccess.get();
|
||||
private static final long BASE = U.arrayBaseOffset(byte[].class);
|
||||
|
||||
public static long get(byte[] a, int off) { return U.getLong(a, BASE + off); }
|
||||
public static void put(byte[] a, int off, long v) { U.putLong(a, BASE + off, v); }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import sun.misc.Unsafe;
|
||||
|
||||
/** BEFORE: a CAS field the way pre-JDK-9 code did it. objectFieldOffset + compareAndSwapLong are deprecated for removal (JEP 471). */
|
||||
public class UnsafeFieldCounter implements FieldCounter {
|
||||
private static final Unsafe U = UnsafeAccess.get();
|
||||
private static final long VALUE;
|
||||
static {
|
||||
try { VALUE = U.objectFieldOffset(UnsafeFieldCounter.class.getDeclaredField("value")); }
|
||||
catch (NoSuchFieldException e) { throw new ExceptionInInitializerError(e); }
|
||||
}
|
||||
private volatile long value;
|
||||
|
||||
public long get() { return value; }
|
||||
public void set(long v) { value = v; }
|
||||
public boolean cas(long expect, long update) { return U.compareAndSwapLong(this, VALUE, expect, update); }
|
||||
public long getAndAdd(long delta) { return U.getAndAddLong(this, VALUE, delta); }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import sun.misc.Unsafe;
|
||||
|
||||
/** BEFORE: raw malloc through Unsafe. Every call below except the constructor's helper is deprecated for removal. */
|
||||
public class UnsafeOffHeapLongs implements OffHeapLongs {
|
||||
private static final Unsafe U = UnsafeAccess.get();
|
||||
private final long address;
|
||||
private final int length;
|
||||
private boolean closed;
|
||||
|
||||
public UnsafeOffHeapLongs(int length) {
|
||||
this.length = length;
|
||||
this.address = U.allocateMemory(8L * length);
|
||||
U.setMemory(address, 8L * length, (byte) 0);
|
||||
}
|
||||
private long at(int i) {
|
||||
if (closed) throw new IllegalStateException("closed"); // with Unsafe nobody checks this: a stale address is a JVM crash, not an exception
|
||||
if (i < 0 || i >= length) throw new IndexOutOfBoundsException("index " + i);
|
||||
return address + 8L * i;
|
||||
}
|
||||
public int length() { return length; }
|
||||
public long get(int i) { return U.getLongVolatile(null, at(i)); }
|
||||
public void set(int i, long v) { U.putLongVolatile(null, at(i), v); }
|
||||
public boolean cas(int i, long e, long u) { return U.compareAndSwapLong(null, at(i), e, u); }
|
||||
public long getAndAdd(int i, long d) { return U.getAndAddLong(null, at(i), d); }
|
||||
public void clear() { U.setMemory(address, 8L * length, (byte) 0); }
|
||||
public void copyFrom(OffHeapLongs o) { for (int i = 0; i < length; i++) set(i, o.get(i)); }
|
||||
public void close() { if (!closed) { closed = true; U.freeMemory(address); } }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
|
||||
/** AFTER: the same field with a VarHandle (JEP 193, JDK 9). No Unsafe anywhere. */
|
||||
public class VarHandleFieldCounter implements FieldCounter {
|
||||
private static final VarHandle VALUE;
|
||||
static {
|
||||
try { VALUE = MethodHandles.lookup().findVarHandle(VarHandleFieldCounter.class, "value", long.class); }
|
||||
catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); }
|
||||
}
|
||||
private volatile long value;
|
||||
|
||||
public long get() { return value; }
|
||||
public void set(long v) { value = v; }
|
||||
public boolean cas(long expect, long update) { return VALUE.compareAndSet(this, expect, update); }
|
||||
public long getAndAdd(long delta) { return (long) VALUE.getAndAdd(this, delta); }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/** AFTER: the same access through a byte-array-view VarHandle (JDK 9). Bounds-checked; native byte order to match Unsafe. */
|
||||
public final class ViewBytes {
|
||||
private ViewBytes() {}
|
||||
private static final VarHandle VIEW = MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.nativeOrder());
|
||||
|
||||
public static long get(byte[] a, int off) { return (long) VIEW.get(a, off); }
|
||||
public static void put(byte[] a, int off, long v) { VIEW.set(a, off, v); }
|
||||
}
|
||||
Reference in New Issue
Block a user