New posts

This commit is contained in:
2026-11-06 21:43:48 +05:30
parent ecceaa8b32
commit f7b6c1efca
33 changed files with 1258 additions and 0 deletions
+141
View File
@@ -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); }
}
}
+29
View File
@@ -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(); }
}
+7
View File
@@ -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);
}
+11
View File
@@ -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();
}
+17
View File
@@ -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);
}
}
}
+11
View File
@@ -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); }
}
+12
View File
@@ -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); }
}