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