Search code examples
javajava-ffm

Does VarHandle.compareAndSet throw an UnsupportedOperationException when used with longs on 32-bit architectures?


Per the MemoryLayout Access mode restrictions docs:

On 32-bit platforms, access modes get and set for long, double and MemorySegment are supported but might lead to word tearing, as described in Section 17.7. of The Java Language Specification.

However, I can't find any info on what happens when you attempt to call compareAndSet for a JAVA_LONG layout type using a VarHandle on 32-bit architectures. The method docs say it can throw an UnsupportedOperationException in some instances, for example if trying to use a JAVA_LONG_UNALIGNED layout. Will it also throw that exception if run on 32-bit architectures, or what happens? I would test this myself but do not have a 32-bit computer available.

The only reason I care is it would be desirable to give users trying to run our application on a 32-bit machine a good error message for why that is not possible (if indeed that is the case). Unfortunately there does not seem to be a good way to detect this so perhaps this method throwing an exception will have to be as good of an error message as they can get.


Solution

  • I can confirm two things:

    1. VarHandle.compareAndSet indeed throws an UnsupportedOperationException when used with longs on 32-bit architectures
    2. ValueLayout.ADDRESS.byteSize() indeed provides a robust method of checking whether you are on a 32- or 64-bit system, as @Slaw speculated!

    To test this, I set up a 32-bit i386 Debian QEMU instance on my 64-bit host system. I ran this test program:

    import java.lang.foreign.Arena;
    import java.lang.foreign.MemorySegment;
    import java.lang.foreign.SegmentAllocator;
    import java.lang.foreign.ValueLayout;
    import java.lang.foreign.MemoryLayout;
    import java.lang.invoke.MethodHandles;
    import java.lang.invoke.VarHandle;
    import java.util.function.Function;
    
    public class Test {
    
      public static String dump(long size, Function<Long, String> getter) {
        String acc = "[";
        long i = 0;
        do {
          if (i > 0) acc += ", ";
          acc += getter.apply(i);
          i += 1;
        } while (i < size);
        acc += "]";
        return acc;
      }
    
      public static void main(String[] args) {
        final SegmentAllocator allocator = Arena.ofAuto();
        final long size = 10L;
    
        MemoryLayout address = ValueLayout.ADDRESS;
        System.out.println("Memory byte size: " + address.byteSize());
    
        // int 4 byte/32-bit ops
        System.out.println("Allocating int array");
        final ValueLayout.OfInt intLayout = ValueLayout.JAVA_INT;
        final MemorySegment intSegment = allocator.allocate(intLayout, size);
        final VarHandle intHandle = MethodHandles.insertCoordinates(intLayout.arrayElementVarHandle(), 0, intSegment, 0L);
        System.out.println(dump(size, (i) -> Integer.toString(intSegment.getAtIndex(intLayout, Math.toIntExact(i)))));
        System.out.println("Setting int array");
        intHandle.set(2, 10);
        intHandle.set(4, 12);
        intHandle.set(8, 2);
        System.out.println("CAS int array");
        intHandle.compareAndSet(2, 10, 20);
        System.out.println(dump(size, (i) -> Integer.toString(intSegment.getAtIndex(intLayout, Math.toIntExact(i)))));
    
        // long 8 byte/64-bit ops
        System.out.println("Allocating long array");
        final ValueLayout.OfLong longLayout = ValueLayout.JAVA_LONG;
        final MemorySegment longSegment = allocator.allocate(longLayout, size);
        final VarHandle longHandle = MethodHandles.insertCoordinates(longLayout.arrayElementVarHandle(), 0, longSegment, 0L);
        System.out.println(dump(size, (i) -> Long.toString(longSegment.getAtIndex(longLayout, i))));
        System.out.println("Setting long array");
        longHandle.set(2L, 10L);
        longHandle.set(4L, 12L);
        longHandle.set(8L, 2L);
        System.out.println("CAS long array");
        longHandle.compareAndSet(2L, 10L, 20L);
        System.out.println(dump(size, (i) -> Long.toString(longSegment.getAtIndex(longLayout, i))));
      }
    }
    

    Here is the output on my 64-bit host system:

    Memory byte size: 8
    Allocating int array
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    Setting int array
    CAS int array
    [0, 0, 20, 0, 12, 0, 0, 0, 2, 0]
    Allocating long array
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    Setting long array
    CAS long array
    [0, 0, 20, 0, 12, 0, 0, 0, 2, 0]
    

    And here is the output on the 32-bit QEMU instance:

    Memory byte size: 4
    Allocating int array
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    Setting int array
    CAS int array
    [0, 0, 20, 0, 12, 0, 0, 0, 2, 0]
    Allocating long array
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    Setting long array
    CAS long array
    Exception in thread "main" java.lang.UnsupportedOperationException: Unsupported access mode for alignment: 4
        at java.base/java.lang.invoke.VarHandleSegmentViewBase.newUnsupportedAccessModeForAlignment(VarHandleSegmentViewBase.java:63)
        at java.base/java.lang.invoke.VarHandleSegmentAsLongs.offsetNonPlain(VarHandleSegmentAsLongs.java:88)
        at java.base/java.lang.invoke.VarHandleSegmentAsLongs.compareAndSet(VarHandleSegmentAsLongs.java:191)
        at Test.main(Test.java:56)