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

111

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

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

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

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

Даниэль
источник
1
Вы можете прочитать мою правку о конфигурации с несколькими процессорами, о которых вы говорите. Может случиться так, что в многопроцессорных системах для кратковременной ссылки будет выполняться не более одного чтения / записи в основную память.
Джон Винт
2
Сам по себе изменчивое чтение не дорого. основная цена - это то, как это предотвращает оптимизацию. на практике эта стоимость в среднем также не очень высока, если только volatile не используется в жестком цикле.
непререкаемый
2
Эта статья на infoq ( infoq.com/articles/memory_barriers_jvm_concurrency ) также может вас заинтересовать, она показывает влияние изменчивости и синхронизации на сгенерированный код для разных архитектур. Это также тот случай, когда jvm может работать лучше, чем компилятор с опережением времени, поскольку он знает, работает ли он в однопроцессорной системе, и может опустить некоторые барьеры памяти.
Йорн Хорстманн

Ответы:

123

На 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 протоколы согласованности кеша различаются.

Майкл Баркер
источник
примечание: java 6 имеет такую ​​же способность показывать сборку (это делает точка доступа)
bestsss
+1 В JDK5 volatile не может быть переупорядочен относительно любого чтения / записи (что, например, исправляет блокировку с двойной проверкой). Означает ли это, что это также повлияет на то, как манипулируют энергонезависимыми полями? Было бы интересно смешать доступ к изменчивым и энергонезависимым полям.
ewernli
@evemli, будьте осторожны, я сам однажды сделал это заявление, но оказалось, что оно неверное. Есть крайний случай. Модель памяти Java допускает семантику тараканов мотелей, когда магазины могут быть переупорядочены перед изменчивыми магазинами. Если вы почерпнули это из статьи Брайана Гетца на сайте IBM, то стоит упомянуть, что эта статья чрезмерно упрощает спецификацию JMM.
Майкл Баркер,
20

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

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

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

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

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

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

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

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

Джон Винт
источник
5
AtomicReference - это просто оболочка для изменчивого поля с добавленными собственными функциями, обеспечивающими дополнительные функции, такие как getAndSet, compareAndSet и т. Д., Поэтому с точки зрения производительности его использование просто полезно, если вам нужны дополнительные функции. Но мне интересно, почему вы здесь говорите об ОС? Функциональность реализована непосредственно в кодах операций ЦП. И означает ли это, что в многопроцессорных системах, где один ЦП не знает о содержимом кэша других ЦП, которые изменяются медленнее, потому что ЦП всегда должны использовать основную память?
Daniel
Вы правы, я пропустил, говорил о том, что ОС должна была написать CPU, и сейчас это исправлю. И да, я знаю, что AtomicReference - это просто оболочка для изменчивых полей, но она также добавляет как своего рода документацию, что само поле будет доступно для нескольких потоков.
Джон Винт
@John, зачем вам добавлять еще одно косвенное обращение через AtomicReference? Если вам нужен CAS - хорошо, но AtomicUpdater может быть лучшим вариантом. Насколько я помню, в AtomicReference нет никаких особенностей.
bestsss 01
@bestsss Для всех основных целей, вы правы, нет никакой разницы между AtomicReference.set / get и изменчивой загрузкой и хранением. При этом у меня было такое же чувство (и до некоторой степени) о том, когда использовать что. Этот ответ может немного подробно описать его. Stackoverflow.com/questions/3964317/… . Использование любого из них является более предпочтительным, мой единственный аргумент в пользу использования AtomicReference вместо простого volatile - это четкая документация - это само по себе не является лучшим аргументом, как я понимаю,
Джон Винт
С другой стороны, некоторые утверждают, что использование изменчивого поля / AtomicReference (без необходимости в CAS) приводит к ошибочному коду old.nabble.com/…
Джон Винт
12

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

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

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

Нил Бартлетт
источник
4

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

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

krakover
источник
4
Чтение изменчивых переменных имеет те же недостатки, что и выполнение монитора-входа, в отношении возможностей переупорядочения инструкций, в то время как запись изменчивой переменной равняется выходу из монитора. Разница может заключаться в том, какие переменные (например, кеш-память процессора) сбрасываются или становятся недействительными. В то время как синхронизированный сброс или аннулирование всего, доступ к изменчивой переменной всегда должен игнорироваться кешем.
Дэниел
12
-1, доступ к изменчивой переменной немного отличается от использования синхронизированного блока. Для ввода синхронизированного блока требуется атомарная запись на основе compareAndSet для снятия блокировки и временная запись для ее снятия. Если блокировка удовлетворена, тогда управление должно перейти из пользовательского пространства в пространство ядра для арбитража блокировки (это дорогой бит). Доступ к изменчивому всегда будет оставаться в пространстве пользователя.
Майкл Баркер,
@MichaelBarker: Вы уверены, что все мониторы должны охраняться ядром, а не приложением?
Daniel
@Daniel: Если вы представляете монитор с помощью синхронизированного блока или Lock, тогда да, но только если монитор удовлетворен. Единственный способ сделать это без арбитража ядра - это использовать ту же логику, но с занятым вращением вместо парковки потока.
Майкл Баркер,
@MichaelBarker: Ладно, для довольных блокировок я это понимаю.
Daniel