Волатильность-это дорого?


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

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

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

4 98

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    x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 33)
0xb396ce8e: mov    xffffffff,%ecx
0xb396ce93: mov    xffffffff,%ebx
0xb396ce98: mov    x6fa2b2f0,%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    xfffffffe,%ecx
0xb396ceb6: mov    xffffffff,%ebx
0xb396cebb: cmp    %ecx,%ebp
0xb396cebd: jne    0xb396cec1
0xb396cebf: cmp    %ebx,%edi
0xb396cec1: je     0xb396ceeb         ;*return
                                    ; - Test2::run@28 (line 40)
0xb396cec3: add    x8,%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    x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 32)
0xb3ab934e: mov    xffffffff,%ecx
0xb3ab9353: mov    xffffffff,%ebx
0xb3ab9358: mov    x150,%ebp
0xb3ab935d: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab9365: movd   %xmm0,%eax
0xb3ab9369: psrlq  x20,%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    xfffffffe,%ecx
0xb3ab937f: mov    xffffffff,%ebx
0xb3ab9384: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab938c: movd   %xmm0,%ebp
0xb3ab9390: psrlq  x20,%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 создание операции чтения одной инструкции (из предыдущего примера мы видели, что 64bit значение обычно требует двух 32-битных считываний в 32-битной системе).

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

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

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

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

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

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

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

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

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

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

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

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

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

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