Наименее навязчивый барьер компиляции для Java на x86

Если бы у меня был процесс Java, взаимодействующий с каким-либо другим процессом через общий ByteBuffer или аналогичный, что было бы наименее навязчивым эквивалентом барьера компилятора в C / C ++? Переносимости не требуется - меня особенно интересует x86.

Например, у меня есть 2 процесса чтения и записи в область памяти в соответствии с псевдокодом:

p1:
    i = 0
    while true:
      A = 0
      //Write to B
      A = ++i

p2:
    a1 = A
    //Read from B
    a2 = A

    if a1 == a2 and a1 != 0:
      //Read was valid

Из-за строгого упорядочения памяти на x86 (загрузка в отдельные места не переупорядочивается, а чтение в отдельные не переупорядоченные места) для этого не требуется никакого барьера памяти в C ++, только барьер компиляции между каждой записью и между каждым чтением (т.е. asm volatile).

Как добиться того же ограничения порядка в Java наименее затратным способом. Есть ли что-нибудь менее навязчивое, чем писать в volatile?


person jmetcalfe    schedule 02.02.2013    source источник


Ответы (2)


sun.misc.Unsafe.putOrdered должен делать то, что вы хотите - хранилище с блокировкой, подразумеваемой на x86 с помощью volatile. Я полагаю, что компилятор не будет перемещать инструкции вокруг него.

Это то же самое, что и lazySet для AtomicInteger и его друзей, но его нельзя использовать напрямую с ByteBuffer.

В отличие от классов volatile или AtomicThings, этот метод применяется к конкретным операциям записи, в которых вы его используете, а не к определению члена, поэтому его использование не подразумевает ничего для чтения.

Похоже, вы пытаетесь реализовать что-то вроде seqlock - это означает, что вам нужно избегать повторного заказа между чтениями счетчика версий A и чтениями / записью самих данных. Обычное int не сократит его - поскольку JIT может делать всевозможные непослушные вещи. Я бы порекомендовал использовать volatile int для вашего счетчика, но затем записать его с помощью putOrdered. Таким образом, вы не платите цену за изменчивую запись (обычно дюжину циклов или больше), в то время как получаете барьер компилятора, подразумеваемый изменчивым чтением (а аппаратный барьер для этих чтений не работает, что делает их быстрыми. ).

С учетом всего сказанного, я думаю, что вы находитесь здесь в серой зоне, потому что lazySet не является частью формальной модели памяти и не полностью вписывается в рассуждения о том, что происходит до, поэтому вам нужно более глубокое понимание фактического JIT. и аппаратная реализация, чтобы увидеть, можно ли объединить вещи таким образом.

Наконец, даже с изменчивым чтением и записью (игнорируя lazySet), я не думаю, что ваш seqlock является правильным с точки зрения модели памяти Java, потому что изменчивые записи устанавливают только то, что происходит раньше между этой записью и последующим чтением на другом поток и более ранние действия в потоке записи, но не между чтением и действиями после записи в потоке записи. Иными словами, это однонаправленный забор, а не двунаправленный. Я считаю, что записи в версии N + 1 в ваш общий регион могут быть видны потоку чтения, даже если он дважды читает A == N.

Пояснение из комментария:

Volatile только создает односторонний барьер. Это очень похоже на семантику получения / выпуска, используемую WinTel в некоторых API. Например, предположим, что A, Bv и C изначально равны нулю:

Thread 1:
A = 1;   // 1
Bv = 1;  // 2
C = 1;   // 3

Thread 2:

int c = C;  // 4
int b = Bv; // 5
int a = A;  // 6

Здесь летучим только Bv. Два потока делают что-то похожее по концепции на ваши писатели и читатели seqlock - поток 1 записывает некоторые данные в одном порядке, а поток 2 читает те же данные в обратном порядке и пытается рассуждать о порядке их выполнения.

Если поток два имеет b == 1, тогда a == 1 всегда, потому что 1 происходит до 2 (порядок программы), а 5 происходит до 6 (порядок программы), и наиболее критично 2 происходит до 5, поскольку 5 читает записанное значение at 2. Таким образом, запись и чтение Bv действует как забор. Вещи выше (2) не могут «двигаться ниже» (2), а элементы ниже (5) не могут «двигаться вверх» 5. Обратите внимание, что я ограничил движение только в одном направлении непосредственно для каждого потока, но не в обоих, что подводит нас к следующему пример:

Эквивалентно вышесказанному, вы можете предположить, что если a == 0, то также c == 0, поскольку C пишется после a и читается до него. Однако летучие вещества не гарантируют этого. В частности, рассуждения, приведенные выше, не препятствуют перемещению (3) выше (2), как это наблюдается в потоке 2, и не препятствуют перемещению (4) ниже (5).

Обновление:

Посмотрим конкретно на ваш пример.

Я считаю, что это может произойти, если развернуть цикл записи, который происходит в p1.

p1:

i = 0
A = 0
// (p1-1) write data1 to B
A = ++i;  // (p1-2) 1 assigned to A

A=0  // (p1-3)
// (p1-4) write data2 to B
A = ++i;  // (p1-5) 2 assigned to A

p2:

a1 = A // (p2-1)
//Read from B // (p2-2)
a2 = A // (p2-3)

if a1 == a2 and a1 != 0:

Скажем, p2 видит 1 для a1 и a2. Это означает, что между p2-1 и p1-2 (и расширением p1-1), а также между p2-3 и p1-2 произошла ошибка. Однако между чем-либо в p2 и p1-4 происходит «раньше». Фактически, я считаю, что при чтении B на p2-2 можно наблюдать второе (возможно, частично завершенное) чтение на p1-4, которое может «перемещаться выше» изменчивой записи на p1-2 и p1-3.

Достаточно интересно, что я думаю, вы могли бы задать новый вопрос только по этому поводу - забудьте о более быстрых барьерах - работает ли это вообще даже с volatile?

person BeeOnRope    schedule 02.02.2013
comment
Согласовано, что JVM не перемещает инструкции, но ЦП может в противном случае. - person Peter Lawrey; 02.02.2013
comment
Верно, но он по-прежнему имеет особую семантику - в частности, он не переупорядочивается с другими хранилищами, что на x86 так же просто, как обычное хранилище, именно так на этой платформе реализованы putOrdered и lazySet. Таким образом, метод должен делать то, что хочет OP. Конечно, точная семантика тонко документирована, так что двойная проверка источника JDK может быть уместна. - person BeeOnRope; 02.02.2013
comment
Спасибо, похоже, это подойдет очень хорошо. А как насчет читаемой стороны? - person jmetcalfe; 03.02.2013
comment
Я добавил более подробную информацию выше, чтобы решить эту проблему. Меня беспокоит, что даже при полностью изменчивом чтении / записи ваша seqlock не будет работать (теоретически, я думаю, что она, вероятно, будет работать на практике), потому что изменчивые чтения означают, что вы увидите все, что произошло до соответствующей записи , но не препятствуйте тому, чтобы вы видели еще больше вещей, которые произошли после этой записи. - person BeeOnRope; 04.02.2013
comment
Спасибо за обновления. Чтобы подтвердить (я плохо разбираюсь в Java, извините за мое незнание), вы думаете, что изменчивое чтение будет гарантировать только упорядочение относительно более ранних записей, а не более ранних чтений, что означает, что чтение из B или начальное чтение из A может быть переупорядочен со вторым чтением из A? Итак, единственным решением будет исходная запись в volatile? - person jmetcalfe; 05.02.2013
comment
Я имею в виду, что форматирование в комментариях - отстой, поэтому позвольте мне добавить его в ответ. - person BeeOnRope; 05.02.2013
comment
@BeeOnRope Очень полезно, спасибо. Чтобы еще раз прояснить, из приведенного выше кажется, что ваше предложение использовать putOrdered для двух счетчиков записывает , если B также не записано с putOrdered / volatile, потому что, используя ваш пример, назначение Bv в ( 2) может превзойти A в точке (1). Поэтому мы должны использовать putOrdered для всех трех операций записи. - person jmetcalfe; 05.02.2013
comment
На самом деле я думаю, что говорю, что даже volatile недостаточно для записи вашего счетчика (A в вашем коде). Однако я не понял вашего вопроса. Когда вы сказали счетчик, вы имели в виду свой код или мой? Когда вы сказали B, вы имели в виду Bv из моего кода? - person BeeOnRope; 05.02.2013
comment
@BeeOnRope Возьмем ваш пример. Если мое понимание Java происходит до того, как gaurentees верны, выполнение записи в A, Bv и C, все изменчивые, должно обеспечивать согласованность? Мы остановили (3) движение выше (2), и я считаю, что поток 2 видит c = 1 и a = 1, он может гарантировать Bv = 1. Счетчик, о котором я говорил, в моем исходном примере - A. - person jmetcalfe; 06.02.2013
comment
Что ж, в моем примере thead 2 смотрит на Bv и пытается рассуждать о действительности a и c, а не наоборот. В основном a и c представляют ваши данные, а Bv - это ваша переменная A - счетчик версии seqlock (запутанно, конечно). Давайте откажемся от примера и посмотрим прямо на ваш. Форматирование - отстой, поэтому я помещу свой анализ в этот пост. - person BeeOnRope; 07.02.2013
comment
Пользователь @irreputable, кажется, знает, что происходит с подобными вещами. См., Например, его ответ на этот вопрос, который указывает на исчерпывающий подход, который вы могли бы используйте, чтобы проанализировать это. - person BeeOnRope; 07.02.2013
comment
Я спросил об общем случае Seqlock с использованием летучих здесь. - person BeeOnRope; 07.02.2013

Вы можете использовать lazySet, он может быть до 10 раз быстрее, чем установка изменчивого поля, поскольку он не останавливает конвейер ЦП. например AtomicLong lazySet, или вы можете использовать эквивалент Unsafe, если вам нужно.

person Peter Lawrey    schedule 02.02.2013