Java сравнение и замена семантики и производительности
, что семантика сравнения и замены в Java? А именно, гарантирует ли метод сравнения и подкачки AtomicInteger
просто упорядоченный доступ между различными потоками к определенному местоположению памяти атомарного целого экземпляра, или он гарантирует упорядоченный доступ ко всем местоположениям в памяти, т. е. он действует так, как если бы он был летучим (забор памяти).
Из документов :
-
weakCompareAndSet
атомарно считывает и условно записывает переменную, но не делает этого. создание любого происходит-до упорядочения, поэтому не дает никаких гарантий относительно предыдущих или последующих операций чтения и записи любых переменных, кроме целиweakCompareAndSet
. -
compareAndSet
и все другие операции чтения и обновления, такие какgetAndIncrement
, имеют эффекты памяти как чтения, так и записи изменчивых переменных.
Из документации API очевидно, что compareAndSet
действует так, как если бы это была изменчивая переменная. Однако предполагается, что weakCompareAndSet
просто изменяет свою конкретную память местоположение. Таким образом, если эта область памяти является исключительной для кэша одного процессора, weakCompareAndSet
должна быть намного быстрее, чем обычная compareAndSet
.
Я задаю этот вопрос, потому что я провел сравнительный анализ следующих методов, запустив threadnum
различные потоки, варьируя threadnum
от 1 до 8 и имея totalwork=1e9
(код написан на Scala, статически скомпилированном языке JVM, но и его значение, и перевод байт-кода изоморфны Java в данном случае - это короткие фрагменты, которые должны быть ясно):
val atomic_cnt = new AtomicInteger(0)
val atomic_tlocal_cnt = new java.lang.ThreadLocal[AtomicInteger] {
override def initialValue = new AtomicInteger(0)
}
def loop_atomic_tlocal_cas = {
var i = 0
val until = totalwork / threadnum
val acnt = atomic_tlocal_cnt.get
while (i < until) {
i += 1
acnt.compareAndSet(i - 1, i)
}
acnt.get + i
}
def loop_atomic_weakcas = {
var i = 0
val until = totalwork / threadnum
val acnt = atomic_cnt
while (i < until) {
i += 1
acnt.weakCompareAndSet(i - 1, i)
}
acnt.get + i
}
def loop_atomic_tlocal_weakcas = {
var i = 0
val until = totalwork / threadnum
val acnt = atomic_tlocal_cnt.get
while (i < until) {
i += 1
acnt.weakCompareAndSet(i - 1, i)
}
acnt.get + i
}
На AMD с 4 двойными ядрами 2,8 ГГц и 4-ядерным процессором i7 2,67 ГГц. JVM - это Sun Server Hotspot JVM 1.6. Результаты не показывают никакой разницы в производительности.
Технические характеристики: AMD 8220 4X dual-core @ 2.8 GHzИмя теста: loop_atomic_tlocal_cas
- номер нити.: 1
Время выполнения: (показывает последние 3) 7504.562 7502.817 7504.626 (среднее значение = 7415.637 мин = 7147.628 Макс = 7504.886)
- номер нити.: 2
Время выполнения: (показывает последние 3) 3751.553 3752.589 3751.519 (среднее значение = 3713.5513 мин = 3574.708 Макс = 3752.949)
- номер нити.: 4
Время выполнения: (показывает последние 3) 1890.055 1889.813 1890.047 (среднее значение = 2065.7207 мин = 1804.652 Макс = 3755.852)
- номер нити.: 8
Время выполнения: (показывает последние 3) 960.12 989.453 970.842 (среднее значение = 1058.8776 мин = 940.492 Макс = 1893.127 )
Имя теста: loop_atomic_weakcas
- номер нити.: 1
Время выполнения: (показывает последние 3) 7325.425 7057.03 7325.407 (среднее значение = 7231.8682 мин = 7057.03 Макс = 7325.45)
- номер нити.: 2
Время выполнения: (показывает последние 3) 3663.21 3665.838 3533.406 (среднее значение = 3607.2149 мин = 3529.177 Макс = 3665.838)
- номер нити.: 4
Время выполнения: (показывает последнее Три) 3664.163 1831.979 1835.07 (среднее значение = 2014.2086 мин = 1797.997 Макс = 3664.163)
- номер нити.: 8
Время выполнения: (показывает последние 3) 940.504 928.467 921.376 (среднее значение = 943.665 мин = 919.985 Макс = 997.681)
Имя теста: loop_atomic_tlocal_weakcas
- номер нити.: 1
Время выполнения: (показывает последние 3) 7502.876 7502.857 7502.933 (среднее значение = 7414.8132 мин = 7145.869 max = 7502,933)
- номер нити.: 2
Время выполнения: (показывает последние 3) 3752.623 3751.53 3752.434 (среднее значение = 3710.1782 мин = 3574.398 Макс = 3752.623)
- номер нити.: 4
Время выполнения: (показывает последние 3) 1876.723 1881.069 1876.538 (среднее значение = 4110.4221 мин = 1804.62 Макс = 12467.351)
- номер нити.: 8
Время выполнения: (показывает последние 3) 959.329 1010.53 969.767 (среднее значение = 1072.8444 мин = 959.329 Макс = 1880.049)
Технические характеристики: Intel i7 quad-core @ 2.67 GHzИмя теста: loop_atomic_tlocal_cas
- номер нити.: 1
Время выполнения: (показывает последние 3) 8138.3175 8130.0044 8130.1535 (среднее значение = 8119.2888 мин = 8049.6497 Макс = 8150.1950)
- номер нити.: 2
Время выполнения: (показывает последние 3) 4067.7399 4067.5403 4068.3747 (среднее значение = 4059.6344 мин = 4026.2739 max = 4068.5455)
- номер нити.: 4
Время выполнения: (показывает последние 3) 2033.4389 2033.2695 2033.2918 (среднее значение = 2030.5825 мин = 2017.6880 Макс = 2035.0352)
Имя теста: loop_atomic_weakcas
- номер нити.: 1
Время выполнения: (показывает последние 3) 8130.5620 8129.9963 8132.3382 (среднее значение = 8114.0052 мин = 8042.0742 Макс = 8132.8542)
- номер нити.: 2
Беги время: (показывает последние 3) 4066.9559 4067.0414 4067.2080 (среднее значение = 4086.0608 мин = 4023.6822 Макс = 4335.1791)
- номер нити.: 4
Время выполнения: (показывает последние 3) 2034.6084 2169.8127 2034.5625 (среднее значение = 2047.7025 мин = 2032.8131 Макс = 2169.8127)
Имя теста: loop_atomic_tlocal_weakcas
- номер нити.: 1
Время выполнения: (показывает последние 3) 8132.5267 8132.0299 8132.2415 (авг = 8114.9328 мин = 8043.3674 Макс = 8134.0418)
- номер нити.: 2
Время выполнения: (показывает последние 3) 4066.5924 4066.5797 4066.6519 (среднее значение = 4059.1911 мин = 4025.0703 Макс = 4066.8547)
- номер нити.: 4
Время выполнения: (показывает последние 3) 2033.2614 2035.5754 2036.9110 (среднее значение = 2033.2958 мин = 2023.5082 Макс = 2038.8750)
Хотя вполне возможно, что локальные потоки в приведенном выше примере заканчиваются в тех же строках кэша мне кажется, что нет заметной разницы в производительности между обычным CAS и его слабой версией.
Это может означать, что на самом деле слабое сравнение и подкачка действуют как полноценный забор памяти, то есть действуют, как если бы это была изменчивая переменная.Вопрос: Является ли это наблюдение правильно? Кроме того, существует ли известная архитектура или дистрибутив Java, для которых слабое сравнение и набор на самом деле быстрее? Если нет, то в чем преимущество использования слабого CAS в первое место?
3 ответа:
Слабое сравнение и свопможет действовать как полная переменная волатильности, в зависимости от реализации JVM, конечно. На самом деле, я не удивлюсь, если на некоторых архитектурах невозможно реализовать слабый CAS заметно более производительным способом, чем обычный CAS. На этих архитектурах вполне может быть так, что слабые случаи реализуются точно так же, как и полный CAS. Или это может быть просто то, что ваш JVM не имел большой оптимизации, вложенной в создание слабых случаев особенно быстро, поэтому текущая реализация просто вызывает полный CAS, потому что это быстро реализовать, и будущая версия будет уточнять это.
JLS просто говорит, что слабый CAS не устанавливает отношенияhappens-before , поэтому просто нетгарантии , что вызванная им модификация видна в других потоках. Все, что вы получаете в этом случае, - это гарантия того, что операция сравнения и задания атомарна, но без гарантий относительно видимость (потенциально) нового значения. Это не то же самое, что гарантировать, что он не будет замечен, поэтому ваши тесты согласуются с этим.
В общем, старайтесь избегать каких-либо выводов о поведении, связанном с параллелизмом, путем экспериментов. Существует так много переменных, которые нужно учитывать, что если вы не следуете тому, что гарантирует JLS, чтобы быть правильным, то ваша программа может сломаться в любое время (возможно, на другой архитектуре, возможно, под более агрессивная оптимизация, которая вызвана небольшим изменением макета вашего кода, возможно, при будущих сборках JVM, которые еще не существуют, и т. д.). Есть Никогда основания предполагать, что вам может сойти с рук то, что заявлено не гарантированным, потому что эксперименты показывают, что "это работает".
Инструкция x86 для "атомарного сравнения и подкачки" имеет вид
LOCK CMPXCHG
. Эта инструкция создает полный забор памяти.Нет инструкции, которая выполняет эту работу без создания ограждения памяти, поэтому очень вероятно, что и
Но это для x86, другие архитектуры (включая будущие варианты x86) могут делать вещи по-другому.compareAndSet
, иweakCompareAndSet
сопоставляются сLOCK CMPXCHG
и выполняют полное ограждение памяти.
weakCompareAndSwap
негарантируется быть быстрее; это просторазрешено быть быстрее. Вы можете посмотреть на открытый исходный код OpenJDK, чтобы увидеть, что некоторые умные люди решили сделать с этим разрешением:А именно: они оба реализованы как однослойные
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
Они имеют точно такую же производительность, потому что они имейте точно такую же реализацию! (по крайней мере, в OpenJDK). Другие люди отметили тот факт, что вы все равно не можете сделать лучше на x86, потому что оборудование уже дает вам кучу гарантий "бесплатно". Это только на более простых архитектурах, таких как ARM, что вы должны беспокоиться об этом.