Летучий дорогой? - PullRequest
       46

Летучий дорогой?

104 голосов
/ 08 января 2011

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

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

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

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

Ответы [ 4 ]

115 голосов
/ 08 января 2011

На 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

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

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

20 голосов
/ 08 января 2011

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

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

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

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

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

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

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

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

12 голосов
/ 08 января 2011

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

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

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

4 голосов
/ 08 января 2011

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

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

...