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

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

Думаю, я неправильно это понимаю. Может ли кто-нибудь просветить меня?

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

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


person Daniel    schedule 08.01.2011    source источник
comment
Вы можете прочитать мою правку о конфигурации с несколькими процессорами, о которых вы говорите. Может случиться так, что в многопроцессорных системах для кратковременной ссылки будет выполняться не более одного чтения / записи в основную память.   -  person John Vint    schedule 08.01.2011
comment
Само по себе изменчивое чтение не является дорогостоящим. основная цена - это то, как это предотвращает оптимизацию. на практике эта стоимость в среднем тоже не очень высока, если только volatile не используется в замкнутом цикле.   -  person irreputable    schedule 08.01.2011
comment
Эта статья об infoq (infoq.com/articles/memory_barriers_jvm_concurrency) также может вас заинтересовать, она показывает влияние изменчивости и синхронизации на сгенерированный код для разных архитектур. Это также тот случай, когда jvm может работать лучше, чем опережающий компилятор, поскольку он знает, работает ли он в однопроцессорной системе, и может опустить некоторые барьеры памяти.   -  person Jörn Horstmann    schedule 27.01.2012


Ответы (4)


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

public static long l;

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

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

Используя способность Java 7 печатать ассемблерный код, метод run выглядит примерно так:

# {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::run2@-1 (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::run@0 (line 33)
0xb396cea9: cmp    %ecx,%ebp
0xb396ceab: jne    0xb396ceaf
0xb396cead: cmp    %ebx,%edi
0xb396ceaf: je     0xb396cece         ;*getstatic l
                                    ; - Test2::run@14 (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::run@28 (line 40)
0xb396cec3: add    $0x8,%esp
0xb396cec6: pop    %ebp
0xb396cec7: test   %eax,0xb7732000    ;   {poll_return}
;... lines removed

Если вы посмотрите на 2 ссылки на getstatic, первая включает в себя загрузку из памяти, вторая пропускает загрузку, поскольку значение повторно используется из регистров, в которые оно уже загружено (long - 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::run2@-1 (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::run@0 (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::run@14 (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. Однако, если другое ядро ​​выполняет запись в изменчивую переменную, строка кэша будет недействительной, что потребует доступа к основной памяти или, возможно, к кэш-памяти L3. Фактическая стоимость будет сильно зависеть от архитектуры процессора. Даже у Intel и AMD протоколы согласованности кеша различаются.

person Michael Barker    schedule 08.01.2011
comment
примечание стороны, java 6 имеет такую ​​же способность показывать сборку (это делает точка доступа) - person bestsss; 19.10.2011
comment
+1 В JDK5 переменные не могут быть переупорядочены относительно любого чтения / записи (что, например, исправляет блокировку с двойной проверкой). Означает ли это, что это также повлияет на то, как манипулируют энергонезависимыми полями? Было бы интересно смешать доступ к изменчивым и энергонезависимым полям. - person ewernli; 25.01.2012
comment
@evemli, будьте осторожны, я сам однажды сделал это заявление, но оказалось, что оно неверное. Есть крайний случай. Модель памяти Java допускает семантику тараканов-мотелей, когда магазины можно переупорядочивать перед нестабильными магазинами. Если вы почерпнули это из статьи Брайана Гетца на сайте IBM, то стоит упомянуть, что эта статья чрезмерно упрощает спецификацию JMM. - person Michael Barker; 08.03.2012

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

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

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

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

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

Изменить, чтобы ответить на редактирование OP:

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

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

person John Vint    schedule 08.01.2011
comment
AtomicReference - это просто оболочка для изменчивого поля с добавленными собственными функциями, обеспечивающими дополнительные функции, такие как getAndSet, compareAndSet и т. Д., Поэтому с точки зрения производительности его использование просто полезно, если вам нужны дополнительные функции. Но мне интересно, почему вы здесь говорите об ОС? Функциональность реализована непосредственно в кодах операций ЦП. И означает ли это, что в многопроцессорных системах, где один ЦП не знает о содержимом кеш-памяти других ЦП, которые изменяются медленнее, потому что ЦП всегда должны использовать основную память? - person Daniel; 08.01.2011
comment
Вы правы, я пропустил, говорил о том, что ОС должна была написать CPU, и сейчас это исправлю. И да, я знаю, что AtomicReference - это просто оболочка для изменчивых полей, но она также добавляет как своего рода документацию, что само поле будет доступно для нескольких потоков. - person John Vint; 08.01.2011
comment
@John, зачем вам добавлять еще одно косвенное обращение через AtomicReference? Если вам нужен CAS - хорошо, но AtomicUpdater может быть лучшим вариантом. Насколько я помню, в AtomicReference нет никаких особенностей. - person bestsss; 01.03.2011
comment
@bestsss Для всех общих целей, вы правы, нет никакой разницы между AtomicReference.set / get и изменчивой загрузкой и хранением. При этом у меня было такое же чувство (и до некоторой степени) о том, когда использовать что. Этот ответ может немного подробно описать его stackoverflow.com/questions/3964317/. Использование любого из них является более предпочтительным, мой единственный аргумент в пользу использования AtomicReference вместо простого volatile - это четкая документация - это само по себе не является лучшим аргументом, как я понимаю - person John Vint; 01.03.2011
comment
С другой стороны, некоторые утверждают, что использование изменчивого поля / AtomicReference (без необходимости в CAS) приводит к ошибочному коду old.nabble.com/ - person John Vint; 01.03.2011
comment
@John, если я объявлю что-нибудь через AtomicReference, я абсолютно уверен, что будет задействован какой-то CAS. Я редко объявляю что-либо изменчивое без необходимости в CAS, в большинстве случаев переменные обновляются одним потоком, но сохраняют возможность их контролировать. Другой вариант - stop логическое значение, которое нужно изменить только один раз. - person bestsss; 01.03.2011
comment
@bestsss Вы можете привести множество аргументов в пользу использования volatile boolean вместо AtomicBoolean и так далее. Я могу представить, что кто-то столкнулся с необходимостью поделиться AtomicXXX внутри объектов (а также нескольких потоков), в которых ему потребуется его изменчивость. - person John Vint; 01.03.2011
comment
Это наблюдается в системах с когерентным кешем. Какие системы нет? - person curiousguy; 20.11.2019

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

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

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

person Neil Bartlett    schedule 08.01.2011

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

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

person krakover    schedule 08.01.2011
comment
Чтение изменчивых переменных имеет те же недостатки, что и выполнение монитора-входа, в отношении возможностей переупорядочения инструкций, в то время как запись изменчивой переменной равняется выходу из монитора. Разница может заключаться в том, какие переменные (например, кеш-память процессора) сбрасываются или становятся недействительными. В то время как синхронизированный сброс или аннулирование всего, доступ к изменчивой переменной всегда должен игнорироваться кешем. - person Daniel; 08.01.2011
comment
-1, доступ к изменчивой переменной немного отличается от использования синхронизированного блока. Ввод синхронизированного блока требует атомарной записи на основе compareAndSet для снятия блокировки и временной записи для ее снятия. Если блокировка удовлетворена, тогда управление должно перейти из пользовательского пространства в пространство ядра для арбитража блокировки (это дорогостоящий бит). Доступ к изменчивому всегда будет оставаться в пользовательском пространстве. - person Michael Barker; 09.01.2011
comment
@MichaelBarker: Вы уверены, что все мониторы должны охраняться ядром, а не приложением? - person Daniel; 09.01.2011
comment
@Daniel: Если вы представляете монитор с помощью синхронизированного блока или Lock, тогда да, но только если монитор удовлетворен. Единственный способ сделать это без арбитража ядра - это использовать ту же логику, но с занятым вращением вместо парковки потока. - person Michael Barker; 09.01.2011
comment
@MichaelBarker: Ладно, для довольных замков я это понимаю. - person Daniel; 09.01.2011
comment
@Daniel Что вы имеете в виду под игнорированием кеша? - person curiousguy; 11.08.2015