Может ли поток сначала получить объект через безопасную публикацию, а затем опубликовать его небезопасно?

Этот вопрос возник у меня после прочтения этого ответа.

Пример кода:

class Obj1 {
  int f1 = 0;
}

volatile Obj1 v1;
Obj1 v2;

Thread 1            | Thread 2 | Thread 3
-------------------------------------------------
var o = new Obj1(); |          |
o.f1 = 1;           |          |
v1 = o;             |          |
                    | v2 = v1; |
                    |          | var r1 = v2.f1;

Is (r1 == 0) possible?

Здесь объект o:

  • впервые опубликовано безопасно: с Thread 1 по Thread 2 через поле volatile v1
  • затем опубликовано небезопасно: с Thread 2 по Thread 3 через v2

Вопрос в следующем: может ли Thread 3 видеть o частично построенным (то есть o.f1 == 0)?

Tom Hawtin - tackline говорит, что может: Thread 3 может видеть o частично построенным, потому что нет происходит до отношения между o.f1 = 1 в Thread 1 и r1 = v2.f1 в Thread 3 из-за небезопасной публикации.

Честно говоря, меня это удивило: до этого момента я думал, что 1-й безопасной публикации достаточно.
Насколько я понимаю, эффективно иммутабельные объекты (описанные в таких популярных книгах, как Effective Java и Java Concurrency на практике) также затронуты этой проблемой.

Объяснение Тома кажется мне совершенно правильным в соответствии с происходит до согласованности в JMM.
Но есть также часть причинно-следственной связи в JMM, которая добавляет ограничения поверх того, что произойдет раньше. Так что, возможно, причинно-следственная часть как-то гарантирует, что 1-й безопасной публикации будет достаточно.
(Не могу сказать, что полностью понимаю причинно-следственную часть, но думаю, что понял бы пример с наборами коммитов и исполнениями).

Итак, у меня есть 2 связанных вопроса:

  1. Включает ли причинно-следственную связь JMM разрешить или запретить Thread 3 видеть o частично построенным?
  2. Есть ли другие причины, по которым Thread 3 разрешено или запрещено видеть o частично построенным?

person Community    schedule 15.02.2021    source источник
comment
Интересный вопрос! Я бы сказал, что, поскольку между t1 и t2 существует отношение hb, между t1 и t3 не может быть неявного отношения, отличного от hb, поскольку они общаются через t2. Это означало бы, что другой поток может разрушить отношение hb между двумя другими потоками.   -  person Erik    schedule 16.02.2021
comment
Если я не ошибаюсь, вы делаете это как минимум второй раз: открываете фантастический вопрос, отвечаете себе фантастическим ответом, затем исчезаете. Жалко, очень жаль.   -  person Eugene    schedule 01.03.2021
comment
@Евгений я тоже так думал   -  person dreamcrash    schedule 01.03.2021


Ответы (2)


Частичный ответ: как сегодня работает небезопасная републикация в OpenJDK.
(Это не окончательный общий ответ, который я хотел бы получить, но, по крайней мере, он показывает, чего ожидать от самой популярной реализации Java. )

Короче говоря, это зависит от того, как объект был опубликован изначально:

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

Скорее всего потому, что я основываю свой ответ на сборке, сгенерированной JIT для моей тестовой программы, и, поскольку я не являюсь экспертом в JIT, меня не удивит, если JIT сгенерирует совершенно другой машинный код на чужой компьютер.


Для тестов я использовал OpenJDK 64-Bit Server VM (сборка 11.0.9+11-alpine-r1, смешанный режим) на ARMv8.
ARMv8 был выбран, потому что он имеет очень упрощенная модель памяти, которая требует инструкций барьера памяти как в потоках издателя, так и в потоках чтения (в отличие от x86). .

<сильный>1. Первоначальная публикация через volatile переменную: скорее всего безопасно

Тестовая java-программа похожа на вопрос (я только добавил еще один поток, чтобы увидеть, какой код сборки генерируется для изменчивой записи):

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(value = 1,
    jvmArgsAppend = {"-Xmx512m", "-server", "-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintAssembly",
        "-XX:+PrintInterpreter", "-XX:+PrintNMethods", "-XX:+PrintNativeNMethods",
        "-XX:+PrintSignatureHandlers", "-XX:+PrintAdapterHandlers", "-XX:+PrintStubCode",
        "-XX:+PrintCompilation", "-XX:+PrintInlining", "-XX:+TraceClassLoading",})
@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(4)
public class VolTest {

  static class Obj1 {
    int f1 = 0;
  }

  @State(Scope.Group)
  public static class State1 {
    volatile Obj1 v1 = new Obj1();
    Obj1 v2 = new Obj1();
  }

  @Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
  public void runVolT1(State1 s) {
    Obj1 o = new Obj1();  /* 43 */
    o.f1 = 1;             /* 44 */
    s.v1 = o;             /* 45 */
  }

  @Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
  public void runVolT2(State1 s) {
    s.v2 = s.v1;          /* 52 */
  }

  @Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
  public int runVolT3(State1 s) {
    return s.v1.f1;       /* 59 */
  }

  @Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
  public int runVolT4(State1 s) {
    return s.v2.f1;       /* 66 */
  }
}

Вот сборка, сгенерированная JIT для runVolT3 и runVolT4:

Compiled method (c1)   26806  529       2       org.sample.VolTest::runVolT3 (8 bytes)
  ...
[Constants]
  # {method} {0x0000fff77cbc4f10} 'runVolT3' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
  # this:     c_rarg1:c_rarg1
                        = 'org/sample/VolTest'
  # parm0:    c_rarg2:c_rarg2
                        = 'org/sample/VolTest$State1'
  ...
[Verified Entry Point]
  ...
                                                ;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::runVolT3@0 (line 59)

  0x0000fff781a60938: dmb       ish
  0x0000fff781a6093c: ldr       w0, [x2, #12]   ; implicit exception: dispatches to 0x0000fff781a60984
  0x0000fff781a60940: dmb       ishld           ;*getfield v1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::runVolT3@1 (line 59)

  0x0000fff781a60944: ldr       w0, [x0, #12]   ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::runVolT3@4 (line 59)
                                                ; implicit exception: dispatches to 0x0000fff781a60990
  0x0000fff781a60948: ldp       x29, x30, [sp, #48]
  0x0000fff781a6094c: add       sp, sp, #0x40
  0x0000fff781a60950: ldr       x8, [x28, #264]
  0x0000fff781a60954: ldr       wzr, [x8]       ;   {poll_return}
  0x0000fff781a60958: ret

...

Compiled method (c2)   27005  536       4       org.sample.VolTest::runVolT3 (8 bytes)
  ...
[Constants]
  # {method} {0x0000fff77cbc4f10} 'runVolT3' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
  # this:     c_rarg1:c_rarg1
                        = 'org/sample/VolTest'
  # parm0:    c_rarg2:c_rarg2
                        = 'org/sample/VolTest$State1'
  ...
[Verified Entry Point]
  ...
                                                ; - org.sample.VolTest::runVolT3@-1 (line 59)
  0x0000fff788f692f4: cbz       x2, 0x0000fff788f69318
  0x0000fff788f692f8: add       x10, x2, #0xc
  0x0000fff788f692fc: ldar      w11, [x10]      ;*getfield v1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::runVolT3@1 (line 59)

  0x0000fff788f69300: ldr       w0, [x11, #12]  ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::runVolT3@4 (line 59)
                                                ; implicit exception: dispatches to 0x0000fff788f69320
  0x0000fff788f69304: ldp       x29, x30, [sp, #16]
  0x0000fff788f69308: add       sp, sp, #0x20
  0x0000fff788f6930c: ldr       x8, [x28, #264]
  0x0000fff788f69310: ldr       wzr, [x8]       ;   {poll_return}
  0x0000fff788f69314: ret

...

Compiled method (c1)   26670  527       2       org.sample.VolTest::runVolT4 (8 bytes)
 ...
[Constants]
  # {method} {0x0000fff77cbc4ff0} 'runVolT4' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
  # this:     c_rarg1:c_rarg1 
                        = 'org/sample/VolTest'
  # parm0:    c_rarg2:c_rarg2 
                        = 'org/sample/VolTest$State1'
  ...
[Verified Entry Point]
  ...
                                                ;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::runVolT4@0 (line 66)

  0x0000fff781a604b8: ldr       w0, [x2, #16]   ;*getfield v2 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::runVolT4@1 (line 66)
                                                ; implicit exception: dispatches to 0x0000fff781a604fc
  0x0000fff781a604bc: ldr       w0, [x0, #12]   ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::runVolT4@4 (line 66)
                                                ; implicit exception: dispatches to 0x0000fff781a60508
  0x0000fff781a604c0: ldp       x29, x30, [sp, #48]
  0x0000fff781a604c4: add       sp, sp, #0x40
  0x0000fff781a604c8: ldr       x8, [x28, #264]
  0x0000fff781a604cc: ldr       wzr, [x8]       ;   {poll_return}
  0x0000fff781a604d0: ret

...

Compiled method (c2)   27497  535       4       org.sample.VolTest::runVolT4 (8 bytes)
  ...
[Constants]
  # {method} {0x0000fff77cbc4ff0} 'runVolT4' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
  # this:     c_rarg1:c_rarg1
                        = 'org/sample/VolTest'
  # parm0:    c_rarg2:c_rarg2
                        = 'org/sample/VolTest$State1'
  ...
[Verified Entry Point]
  ...
                                                ; - org.sample.VolTest::runVolT4@-1 (line 66)
  0x0000fff788f69674: ldr       w11, [x2, #16]  ;*getfield v2 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::runVolT4@1 (line 66)
                                                ; implicit exception: dispatches to 0x0000fff788f69690
  0x0000fff788f69678: ldr       w0, [x11, #12]  ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::runVolT4@4 (line 66)
                                                ; implicit exception: dispatches to 0x0000fff788f69698
  0x0000fff788f6967c: ldp       x29, x30, [sp, #16]
  0x0000fff788f69680: add       sp, sp, #0x20
  0x0000fff788f69684: ldr       x8, [x28, #264]
  0x0000fff788f69688: ldr       wzr, [x8]       ;   {poll_return}
  0x0000fff788f6968c: ret

Отметим, что барьерные инструкции сгенерированная сборка содержит:

  • runVolT1 (the assembly isn't shown above because it's too long):
    • c1 version contains 1x dmb ishst, 2x dmb ish
    • c2 версия содержит 1x dmb ishst, 1x dmb ish, 1x stlr
  • runVolT3 (which reads volatile v1):
    • c1 version 1x dmb ish, 1x dmb ishld
    • c2 версия 1x ldar
  • runVolT4 (что читается как энергонезависимая v2): нет барьеров памяти

Как видите, runVolT4 (который читает объект после небезопасной перезаписи) не содержит барьеров памяти.

Означает ли это, что поток может видеть состояние объекта как полуинициализированное?
Оказывается, нет, тем не менее на ARMv8 это безопасно.

Почему?
Посмотрите на return s.v2.f1; в коде. Здесь ЦП выполняет 2 чтения памяти:

  • сначала он читает s.v2, который содержит адрес памяти объекта o
  • затем он считывает значение o.f1 из (адрес памяти o) + (смещение поля f1 в пределах Obj1)

Адрес памяти для чтения o.f1 вычисляется из значения, возвращаемого чтением s.v2 — это так называемая адресная зависимость.

В ARMv8 такая зависимость от адреса предотвращает изменение порядка этих двух операций чтения (см. пример MP+dmb.sy+addr в Оперативное моделирование архитектуры ARMv8: параллелизм и ISA, вы можете сами попробовать в Инструмент моделирования памяти ARM), поэтому мы гарантированно увидим v2 полностью инициализированным.

Инструкции барьера памяти в runVolT3 служат другой цели: они предотвращают изменение порядка чтения volatile s.v1 с другими действиями в потоке (в Java чтение volatile является одним из действий синхронизации, которые должны быть полностью упорядочены).

Более того, сегодня оказывается на все поддерживаемые благодаря архитектуре OpenJDK адресная зависимость предотвращает переупорядочение операций чтения (см. Зависимые загрузки могут быть переупорядочены в этой таблица в вики или Заказы зависимостей данных загружаются? в таблице в JSR -133 Пособие для разработчиков компиляторов).

В итоге сегодня на OpenJDK если объект изначально публикуется через volatile поле, то он скорее всего будет виден как полностью инициализированный даже после небезопасной републикации.

<сильный>2. Первоначальная публикация через синхронизированный блок: скорее всего небезопасно

Иная ситуация, когда первоначальная публикация осуществляется через синхронизированный блок:

class Obj1 {
  int f1 = 0;
}

Obj1 v1;
Obj1 v2;

Thread 1              | Thread 2       | Thread 3
--------------------------------------------------------
synchronized {        |                |
  var o = new Obj1(); |                |
  o.f1 = 1;           |                |
  v1 = o;             |                |
}                     |                |
                      | synchronized { |
                      |   var r1 = v1; |
                      | }              |
                      | v2 = r1;       |
                      |                | var r2 = v2.f1;

Is (r2 == 0) possible?

Здесь сгенерированная сборка для Thread 3 такая же, как и для runVolT4 выше: она не содержит инструкций барьера памяти. В результате Thread 3 может легко увидеть неупорядоченные записи от Thread 1.

И вообще, небезопасная републикация в таких случаях скорее всего небезопасна сегодня на OpenJDK.

person Community    schedule 22.02.2021
comment
Впечатляющая работа! Конечно, мне это нравится, поскольку подтверждает мои первоначальные подозрения, но тем не менее отличная работа! - person Erik; 22.02.2021
comment
@ Эрик, этот вопрос беспокоил меня несколько дней. поскольку volatile предлагает последовательную согласованность + jls не допускает случайных значений; для меня при использовании volatile вы никогда не увидите r1 == 0. С synchronized эту блокировку можно было бы обойти, если JIT сможет доказать, что она не нужна, поэтому я думаю, поэтому в последнем случае нет барьеров. - person Eugene; 22.02.2021
comment
ваш анализ фантастический. Я пытался воспроизвести это с jcstress в течение нескольких дней и не смог (но я на x86). Я все еще пытаюсь построить правильную модель в своей голове в соответствии с JLS. - person Eugene; 22.02.2021
comment
Могу я спросить вас, на каком оборудовании вы запускали это? Где вы взяли ноутбук/компьютер ARM? - person Eugene; 24.02.2021
comment
@Eugene Я использовал машину x86: я использовал kvm+qemu для создания виртуальная машина ARMv8 для тестов. Я не уверен, что qemu эмулирует все послабления модели памяти ARM, но сборка, сгенерированная JIT, должна быть такой же, как и на реальном ARMv8. - person ; 24.02.2021
comment
@wkdtbqmw о! Это делает его еще более удивительным, количество времени, которое вы посвятили этому... - person Eugene; 24.02.2021

Ответ: Причинно-следственная связь часть JMM позволяет Thread 3 видеть o частично построенным.

Наконец-то мне удалось применить 17.4. 8. Исполнения и требования причинно-следственной связи (также называемые причинно-следственной связью JMM) к этому примеру.

Итак, это наша Java-программа:

class Obj1 {
  int f1;
}

volatile Obj1 v1;
Obj1 v2;

Thread 1            | Thread 2 | Thread 3
--------------------|----------|-----------------
var o = new Obj1(); |          |
o.f1 = 1;           |          |
v1 = o;             |          |
                    | v2 = v1; |
                    |          | var r1 = v2.f1;

И мы хотим узнать, разрешен ли результат (r1 == 0).

Оказывается, чтобы доказать, что (r1 == 0) разрешено, нам нужно найти правильное выполнение, которое дает этот результат и может быть проверено с помощью алгоритма, приведенного в 17.4.8. Казни и причинно-следственные связи.

Сначала давайте перепишем нашу Java-программу в терминах переменные и действия, как определено в алгоритме.
Давайте также покажем значения для наших действий чтения и записи, чтобы получить желаемое выполнение E для проверки:

Initially: W[v1]=null, W[v2]=null, W[o.f1]=0

Thread 1  | Thread 2 | Thread 3
----------|----------|-----------
W[o.f1]=1 |          |
Wv[v1]=o  |          |
          | Rv[v1]=o |
          | W[v2]=o  |
          |          | R[v2]=o
          |          | R[o.f1]=0

Примечания:

  • o представляет экземпляр, созданный new Obj1(); в коде Java.
  • W и R представляют обычные записи и чтения; Wv и Rv представляют энергозависимые записи и чтения
  • прочитанное/записанное значение для действия отображается после =
  • W[o.f1]=0 находится в начальных действиях, потому что согласно JLS:
    #P8#

Вот более компактная форма E:

W[v1]=null, W[v2]=null, W[o.f1]=0
---------------------------------
W[o.f1]=1 |          |
Wv[v1]=o  |          |
          | Rv[v1]=o |
          | W[v2]=o  |
          |          | R[v2]=o
          |          | R[o.f1]=0

Проверка E

Согласно 17.4.8. Казни и причинно-следственные связи:

Правильно построенное выполнение E = ‹ P, A, po, so, W, V, sw, hb › подтверждается совершением действий из A. Если все действия в A могут быть совершены, то выполнение удовлетворяет требованиям причинности модели памяти языка программирования Java.

Итак, нам нужно шаг за шагом строить набор совершенных действий (получаем последовательность C₀,C₁,... , где Cₖ — множество совершенных действий на k-й итерации, а Cₖ ⊆ Cₖ₊₁) до тех пор, пока не совершим все действия A нашего исполнения E .
Также раздел JLS содержит 9 правил, которые определяют, когда действие может быть совершено.

  • Шаг 0: алгоритм всегда начинается с пустого множества.

    C₀ = ∅
    
  • Шаг 1: фиксируем только записи.
    Причина в том, что согласно правилу 7, зафиксированное чтение в Сₖ должно возвращать запись из Сₖ₋₁, а у нас пустое C₀.

    E₁:
    
    W[v1]=null, W[v2]=null, W[o.f1]=0
    ----------------------------------
    W[o.f1]=1 |          |
    Wv[v1]=o  |          |
    
    C₁ = { W[v1]=null, W[v2]=null, W[o.f1]=0, W[o.f1]=1, Wv[v1]=o }
    
  • Шаг 2: теперь мы можем зафиксировать чтение и запись o в потоке 2.
    Поскольку v1 является непостоянным, Wv[v1]=o происходит раньше Rv[v1], и чтение возвращает o.

    E₂:
    
    W[v1]=null, W[v2]=null, W[o.f1]=0
    ---------------------------------
    W[o.f1]=1 |          |
    Wv[v1]=o  |          |
              | Rv[v1]=o |
              | W[v2]=o  |
    
    C₂ = C₁∪{ Rv[v1]=o, W[v2]=o }
    
  • Шаг 3: теперь, когда у нас зафиксировано W[v2]=o, мы можем зафиксировать чтение R[v2] в потоке 3.
    Согласно правилу 6, текущее зафиксированное чтение может возвращать только событие «происходит перед записью» (значение может быть изменено один раз на напишите на следующем шаге).
    R[v2] и W[v2]=o не упорядочены с R[v2] раньше, поэтому R[v2] читается как null.

    E₃:
    
    W[v1]=null, W[v2]=null, W[o.f1]=0
    ---------------------------------
    W[o.f1]=1 |          |
    Wv[v1]=o  |          |
              | Rv[v1]=o |
              | W[v2]=o  |
              |          | R[v2]=null
    
    C₃ = C₂∪{ R[v2]=null }
    
  • Шаг 4: теперь R[v2] может считывать W[v2]=o через гонку данных, и это делает возможным R[o.f1].
    R[o.f1] считывает значение по умолчанию 0, и алгоритм завершает работу, поскольку все действия нашего выполнения зафиксированы.

    E = E₄:
    
    W[v1]=null, W[v2]=null, W[o.f1]=0
    ---------------------------------
    W[o.f1]=1 |          |
    Wv[v1]=o  |          |
              | Rv[v1]=o |
              | W[v2]=o  |
              |          | R[v2]=o
              |          | R[o.f1]=0
    
    A = C₄ = C₂∪{ R[v2]=o, R[o.f1]=0 }
    

В результате мы проверили выполнение, которое выдает (r1 == 0), следовательно, этот результат действителен.


Кроме того, стоит отметить, что этот алгоритм проверки причинно-следственной связи почти не накладывает дополнительных ограничений на «происходит до».
Джереми Мэнсон (один из авторов JMM) объясняет, что алгоритм существует для предотвращения довольно странного поведения — так называемых петель причинно-следственной связи при наличии циклической цепочки действий, которые вызывают друг друга (т. е. когда действие вызывает само себя).
Во всех остальных случаях, кроме этих циклов причинности, мы используем «происходит-прежде», как в комментарий Тома.

person Community    schedule 23.02.2021
comment
что причинность в JLS должна объяснять (и запрещать) простую вещь: OoTA. К сожалению, каждый раз, когда я пытаюсь это прочитать и объяснить, у меня болит голова. Вы проделали большую работу здесь (у меня нет более сильного прилагательного), и я прочитал это 3 раза, медленно, кажется, что это имеет смысл. В общем, фантастическое посвящение. - person Eugene; 23.02.2021
comment
@Евгений Спасибо за добрые слова. И еще большее спасибо за то, что прочитали текст 3 раза и проверили его на наличие ошибок — я очень ценю это, потому что это первый раз, когда я применил алгоритм причинности к программе, поэтому ошибки очень вероятны. Относительно того, что каждый раз, когда я пытаюсь это прочитать и объяснить, у меня болит голова — я тоже многого в ней не понимаю (т.е. надеюсь, что понимаю, как применять алгоритм причинно-следственной правила в алгоритме такие, какие они есть). - person ; 23.02.2021
comment
@Eugene, что причинно-следственная связь в JLS должна объяснять (и запрещать) простую вещь: OoTA я не уверен, что это просто: в тезис Джереми Мэнсона (который является наиболее подробным объяснением JMM) он говорит: воздух читать сложно, а потом тратить огромное барахло диссертации объясняя это. - person ; 23.02.2021
comment
ага, мне как-то неправильно было сказать простой. OoTA не прост, но, на самом деле, может быть достигнут только при спекулятивных чтениях. Это абсолютное великое упрощение, которое я построил в своей голове вокруг этого, и оно выплеснулось в комментарий; Извини за это. Есть причина, по которой C/C++ даже не пытался указать это, они просто говорят: добро пожаловать в неопределенное поведение. - person Eugene; 23.02.2021
comment
относительно OoTA: вчера я нашел ответ на SO, в котором упоминается Объявление призраков вне закона: как избежать результатов из воздуха. Объяснения OoTA в этой статье очень понятны, и их чтение действительно помогло мне понять части OoTA тезис Джереми Мэнсона гораздо лучше (в нем тезис OoTA объясняется в основном примерами допустимого/недопустимого поведения). Так что я бы порекомендовал эту статью всем, кто интересуется OoTA. - person ; 25.02.2021
comment
действительно отличная бумага, какая жемчужина. Чтение на выходных будет потрясающим, большое спасибо за это - person Eugene; 26.02.2021
comment
Мне не понравилась статья после прочтения. Примеры со спекулятивным чтением слишком сложны (по крайней мере, недостаточно объяснены для среднего читателя). И их предложение, насколько я понимаю, LoadStore перед каждым магазином. Это было измерено? Ага, на х86 не так уж и плохо, а на слабее? lsync почти перед каждым магазином? Я не тот человек, чтобы судить об этом, но я должен почесать голову дважды. - person Eugene; 01.03.2021