почему мой собственный AtomicLong медленнее, чем тот, который предоставляется в JDK?

#java #java.util.concurrent #compare-and-swap #atomic-long

#java #java.util.concurrent #сравнение и обмен #atomic-long

Вопрос:

Я писал свой собственный AtomicLong класс и только что обнаружил, что функция, которая у меня была, намного медленнее, чем та, которая предоставлена в классе Unsafe. Мне интересно, почему?

Ниже приведены коды, которые у меня есть:

 public interface Counter {
    void increment();
    long get();
}


public class PrimitiveUnsafeSupportCounter implements Counter{

    private volatile long count = 0;
    private Unsafe unsafe;
    private long offset;

    public PrimitiveUnsafeSupportCounter() throws IllegalAccessException, NoSuchFieldException {
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        this.unsafe = (Unsafe) f.get(null);
        this.offset = this.unsafe.objectFieldOffset(PrimitiveUnsafeSupportCounter.class.getDeclaredField("count"));
    }

    @Override
    public void increment() {

        this.unsafe.getAndAddLong(this, this.offset, 1);
    }

    @Override
    public long get() {
        return this.count;
    }
}

public class CounterThread implements Runnable {

    private Counter counter;

    public CounterThread(Counter counter){
        this.counter = counter;
    }
    @Override
    public void run() {

        for (int i = 0; i < 100000; i   ){
            this.counter.increment();
        }
    }
}

class Test{

    public static void test(Counter counter) throws NoSuchFieldException, IllegalAccessException, InterruptedException {

        ExecutorService executor = Executors.newFixedThreadPool(1000);

        long start = System.currentTimeMillis();
        for (int i = 0 ; i < 1000; i  ){
            executor.submit(new CounterThread(counter));
        }

        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        long stop = System.currentTimeMillis();

        System.out.println(counter.get());
        System.out.println(stop - start);
    }

}

public class Main {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {

        Counter primitiveUnsafeSupportCounter = new PrimitiveUnsafeSupportCounter();
        Test.test(primitiveUnsafeSupportCounter);

    }

}
  

для завершения приведенных выше кодов требуется около 3000 мс.
однако это займет около 7000 мс, если я использовал приведенные ниже коды вместо this.unsafe.getAndAddLong(this, this.offset, 1); .

 long before;
do {
     before = this.unsafe.getLongVolatile(this, this.offset);
} while (!this.unsafe.compareAndSwapLong(this, this.offset, before, before   1));
  

Я просмотрел исходные коды getAndAddLong и обнаружил, что он делает почти то же самое, что и приведенные выше коды, так что я должен пропустить?

Комментарии:

1. Возможно, вы упускаете из виду, что JVM хорошо знаком с Unsafe классом и, вероятно, полностью оптимизировал машинный код для getAndAddLong метода. Ваш do-while цикл может не JIT-компилироваться так же, как полностью оптимизированный машинный код.

Ответ №1:

Это встроенная в JVM и написанная вручную циклическая версия имеет крайне неэффективный скомпилированный код для этой цели. На x86 вы можете иметь атомарную версию таких операций чтения-изменения-записи через lock префикс. См. Руководство Intel 8.1.2.2 Блокировка шины с программным управлением :

Чтобы явно принудительно использовать семантику БЛОКИРОВКИ, программное обеспечение может использовать префикс БЛОКИРОВКИ со следующими инструкциями, когда они используются для изменения ячейки памяти.

В частности, у вас может быть что-то вроде lock add op1 op2 . В вашем примере вы проверяете результат cmpxchg и выполняете некоторый переход, который явно медленнее. Также, насколько я помню, для энергозависимого доступа x86 требуется какой-то mfence or lock для обеспечения упорядочения памяти.