Неустойчиво дорого?

После прочтения Поваренной книги JSR-133 для авторов компиляторов о реализации volatile, особенно в разделе "Взаимодействие с атомарными инструкциями", я предполагаю, что для чтения изменяемой переменной без ее обновления необходим барьер LoadLoad или LoadStore. Далее по странице я вижу, что LoadLoad и LoadStore фактически не работают на процессорах X86. Означает ли это, что операции чтения volatile могут выполняться без явного аннулирования кэша на x86, и это так же быстро, как чтение обычной переменной (не учитывая ограничения на изменение порядка volatile)?

Я считаю, что я не понимаю это правильно. Может кто-то хочет просветить меня?

РЕДАКТИРОВАТЬ: Интересно, есть ли различия в многопроцессорных средах. В однопроцессорных системах ЦП может смотреть на свои собственные кэши потоков, как утверждает Джон В., но в многопроцессорных системах должна быть некоторая опция конфигурации для ЦПУ, что этого недостаточно, и необходимо задействовать основную память, делая энергозависимую медленнее в системах с несколькими процессорами, верно?

PS: На своем пути, чтобы узнать больше об этом, я наткнулся на следующие замечательные статьи, и, поскольку этот вопрос может быть интересен для других, я поделюсь своими ссылками здесь:

Ответ 1

На Intel неспокойное нестабильное чтение довольно дешево. Если мы рассмотрим следующий простой случай:

public static long l;

public static void run() {        
    if (l == -1)
        System.exit(-1);

    if (l == -2)
        System.exit(-1);
}

С помощью возможности Java 7 для печати ассемблера метод запуска выглядит примерно так:

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb396ce80: mov    %eax,-0x3000(%esp)
0xb396ce87: push   %ebp
0xb396ce88: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::[email protected] (line 33)
0xb396ce8e: mov    $0xffffffff,%ecx
0xb396ce93: mov    $0xffffffff,%ebx
0xb396ce98: mov    $0x6fa2b2f0,%esi   ;   {oop('Test2')}
0xb396ce9d: mov    0x150(%esi),%ebp
0xb396cea3: mov    0x154(%esi),%edi   ;*getstatic l
                                    ; - Test2::[email protected] (line 33)
0xb396cea9: cmp    %ecx,%ebp
0xb396ceab: jne    0xb396ceaf
0xb396cead: cmp    %ebx,%edi
0xb396ceaf: je     0xb396cece         ;*getstatic l
                                    ; - Test2::[email protected] (line 37)
0xb396ceb1: mov    $0xfffffffe,%ecx
0xb396ceb6: mov    $0xffffffff,%ebx
0xb396cebb: cmp    %ecx,%ebp
0xb396cebd: jne    0xb396cec1
0xb396cebf: cmp    %ebx,%edi
0xb396cec1: je     0xb396ceeb         ;*return
                                    ; - Test2::[email protected] (line 40)
0xb396cec3: add    $0x8,%esp
0xb396cec6: pop    %ebp
0xb396cec7: test   %eax,0xb7732000    ;   {poll_return}
;... lines removed

Если вы посмотрите на 2 ссылки на getstatic, первая включает в себя загрузку из памяти, вторая пропускает нагрузку, поскольку значение повторно используется из регистра (ов), в который он уже загружен (длинный бит 64 бит и мой 32-битный ноутбук использует 2 регистра).

Если переменная l изменена, результирующая сборка отличается.

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb3ab9340: mov    %eax,-0x3000(%esp)
0xb3ab9347: push   %ebp
0xb3ab9348: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::[email protected] (line 32)
0xb3ab934e: mov    $0xffffffff,%ecx
0xb3ab9353: mov    $0xffffffff,%ebx
0xb3ab9358: mov    $0x150,%ebp
0xb3ab935d: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab9365: movd   %xmm0,%eax
0xb3ab9369: psrlq  $0x20,%xmm0
0xb3ab936e: movd   %xmm0,%edx         ;*getstatic l
                                    ; - Test2::[email protected] (line 32)
0xb3ab9372: cmp    %ecx,%eax
0xb3ab9374: jne    0xb3ab9378
0xb3ab9376: cmp    %ebx,%edx
0xb3ab9378: je     0xb3ab93ac
0xb3ab937a: mov    $0xfffffffe,%ecx
0xb3ab937f: mov    $0xffffffff,%ebx
0xb3ab9384: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab938c: movd   %xmm0,%ebp
0xb3ab9390: psrlq  $0x20,%xmm0
0xb3ab9395: movd   %xmm0,%edi         ;*getstatic l
                                    ; - Test2::[email protected] (line 36)
0xb3ab9399: cmp    %ecx,%ebp
0xb3ab939b: jne    0xb3ab939f
0xb3ab939d: cmp    %ebx,%edi
0xb3ab939f: je     0xb3ab93ba         ;*return
;... lines removed

В этом случае обе ссылки getstatic на переменную l включают в себя нагрузку из памяти, то есть значение не может храниться в регистре через несколько изменчивых чтений. Чтобы обеспечить чтение атома, значение считывается из основной памяти в регистр MMX movsd 0x6fb7b2f0(%ebp),%xmm0, делая операцию чтения одной инструкцией (из предыдущего примера мы увидели, что для 64-битного значения обычно требуется два 32-битных чтения в 32-битной системе).

Таким образом, общая стоимость волатильного чтения будет примерно эквивалентна нагрузке на память и может быть столь же дешевой, как доступ к кеш-памяти L1. Однако, если другое ядро ​​записывает переменную volatile, кэш-строка будет недействительной, если требуется основная память или, возможно, доступ к кэшу L3. Фактическая стоимость будет в значительной степени зависеть от архитектуры процессора. Даже между Intel и AMD протоколы когерентности кэша отличаются.

Ответ 2

Вообще говоря, на большинстве современных процессоров энергозависимая нагрузка сравнима с нормальной нагрузкой. Нестабильное хранилище составляет около 1/3 времени входа/выхода монитора. Это видно по системам, которые являются когерентными.

Чтобы ответить на вопрос OP, изменчивые записи являются дорогостоящими, в то время как чтение обычно не являются.

Означает ли это, что операции чтения volatile могут выполняться без явного аннулирования кэша на x86, и это так же быстро, как и обычное чтение переменных (не учитывая ограничения на изменение порядка volatile)?

Да, иногда при проверке поля процессор может даже не попадать в основную память, а шпионить за другими кешами потоков и получать оттуда значение (очень общее объяснение).

Тем не менее, я высказал предположение Нейла, что если у вас есть поле, к которому обращаются несколько потоков, вы должны обернуть его в AtomicReference. Будучи AtomicReference, он выполняет примерно одинаковую пропускную способность для чтения/записи, но также более очевидно, что поле будет доступно и изменено несколькими потоками.

Изменить, чтобы ответить OP edit:

Когерентность кэша представляет собой сложный протокол, но вкратце: CPU будет использовать общую строку кэша, которая подключена к основной памяти. Если процессор загружает память, и у него нет другого процессора, он будет считать, что это наиболее актуальное значение. Если другой ЦП попытается загрузить ту же ячейку памяти, то уже загруженный ЦП будет знать об этом и фактически разделит кешированную ссылку на запрашивающий ЦП - теперь ЦП запроса имеет копию этой памяти в своем кеше ЦП. (Никогда не приходилось искать в основной памяти ссылки)

Здесь задействовано немного больше протокола, но это дает представление о том, что происходит. Также, чтобы ответить на ваш другой вопрос, при отсутствии нескольких процессоров, энергозависимые операции чтения/записи могут фактически быть быстрее, чем с несколькими процессорами. Есть некоторые приложения, которые фактически работают быстрее одновременно с одним процессором, а не с несколькими.

Ответ 3

В словах модели памяти Java (как определено для Java 5+ в JSR 133) любая операция - чтение или запись - в переменной volatile создает произойдет-до отношения по отношению к любой другой операции по одной и той же переменной. Это означает, что компилятор и JIT вынуждены избегать определенных оптимизаций, таких как инструкции по переупорядочению в потоке или выполнение операций только в локальном кеше.

Поскольку некоторые оптимизации недоступны, полученный код обязательно медленнее, чем это было бы, хотя, вероятно, не очень.

Тем не менее вы не должны создавать переменную volatile, если не знаете, что к ней будут доступны несколько потоков вне блоков synchronized. Даже тогда вам следует подумать, является ли volatile лучшим выбором по сравнению с synchronized, AtomicReference и его друзьями, явными классами Lock и т.д.

Ответ 4

Доступ к изменчивой переменной во многом аналогичен обходу доступа к обычной переменной в синхронизированном блоке. Например, доступ к переменной volatile запрещает CPU переупорядочивать инструкции до и после доступа, и это обычно замедляет выполнение (хотя я не могу сказать, сколько).

В общем, в многопроцессорной системе я не вижу, как можно сделать доступ к изменчивой переменной без штрафа - должен быть какой-то способ гарантировать, что запись на процессоре A будет синхронизирована с процессором чтения на процессоре Б.