Подтвердить что ты не робот

Семантика сравнения и смены языка Java и производительность

Что такое семантика сравнения и свопинга в Java? А именно, метод сравнения и свопинга AtomicInteger просто гарантирует упорядоченный доступ между различными потоками к конкретному местоположению памяти экземпляра атомного целого или гарантирует упорядоченный доступ ко всем местоположениям в памяти, то есть он действует так, как если бы он были неустойчивыми (забор памяти).

Из docs:

  • 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 двухъядерные @2,8 ГГц

Имя теста: 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 (avg = 3713,5513 мин = 3574,708 max = 3752,949)

  • Номер темы: 4

Время выполнения: (показывая последние 3) 1890.055 1889.813 1890.047 (avg = 2065.7207 мин = 1804,652 макс. = 3755,852).

  • Номер темы: 8

Время выполнения: (показывая последние 3) 960,12 989,453 970,842 (avg = 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

Время выполнения: (показывая последние 3) 3664,163 1831,979 1835,07 (avg = 2014,2086 мин = 1797,997 max = 3664,163)

  • Номер темы: 8

Время выполнения: (показывая последние 3) 940,504 928,467 921,376 (среднее value = 943,665 мин = 919,985 max = 997,681)


Имя теста: loop_atomic_tlocal_weakcas

  • Номер темы: 1

Время выполнения: (показывая последние 3) 7502,876 7502,857 7502,933 (avg = 7414,8132 мин = 7145,869 макс = 7502,933)

  • Номер темы: 2

Время выполнения: (показывая последние 3) 3752,623 3751,53 3752,434 (avg = 3710,1778 мин = 3574,398 max = 3752,623)

  • Номер темы: 4

Время выполнения: (показывая последние 3) 1876,723 1881,069 1876,538 (avg = 4110,4221 мин = 1804,62 max = 12467,351)

  • Номер темы: 8

Время выполнения: (показывая последние 3) 959,329 1010,53 969,767 (avg = 1072,8444 мин = 959,329 макс = 1880,049)

Технические характеристики: Intel i7 с четырьмя ядрами с частотой 2,67 ГГц

Имя теста: 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,377 (среднее value = 4059,6344 мин = 4026,2739 макс = 4068,5455)

  • Номер темы: 4

Время выполнения: (показывая последние 3) 2033,4389 2033,2695 2033,2918 (avg = 2030,5825 мин = 2017,6880 max = 2035,0352)


Имя теста: loop_atomic_weakcas

  • Номер темы: 1

Время выполнения: (показывая последние 3) 8130,5620 8129.9963 8132.3382 (среднее value = 8114,0052 мин = 8042,0742 макс = 8132,8542)

  • Номер темы: 2

Время выполнения: (показывая последние 3) 4066,9559 4067,0440 4067,2080 (avg = 4086,0608 мин = 4023,6822 макс = 4335,1791)

  • Номер темы: 4

Время выполнения: (показывая последние 3) 2034.6084 2169,8127 2034,5625 (avg = 2047,7025 мин = 2032,8131 макс = 2169,8127)


Имя теста: loop_atomic_tlocal_weakcas

  • Номер темы: 1

Время выполнения: (показывая последние 3) 8132,5267 8132,0299 8132,2415 (avg = 8114,9328 мин = 8043,3674 макс = 8134,0418)

  • Номер темы: 2

Время выполнения: (показывая последние 3) 4066,5924 4066,5797 4066,6519 (среднее value = 4059,1911 мин = 4025,0703 макс = 4066,8547)

  • Номер темы: 4

Время выполнения: (показывая последние 3) 2033,2614 2035,5754 2036,9110 (среднее value = 2033,2958 мин = 2023,5082 макс = 2038,8750)


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

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

Вопрос: Правильно ли это наблюдение? Кроме того, существует ли известная архитектура или дистрибутив Java, для которых слабые сравнения и настройки на самом деле быстрее? Если нет, то в чем преимущество использования слабого CAS в первую очередь?

4b9b3361

Ответ 1

Слабые сравнения и свопы могут действовать как полная изменчивая переменная, в зависимости от реализации JVM. На самом деле, я не удивлюсь, если на некоторых архитектурах невозможно реализовать слабый CAS в значительно более производительном режиме, чем обычный CAS. На этих архитектурах вполне может быть, что слабые CASes реализованы точно так же, как и полный CAS. Или просто может быть, что ваша JVM не очень оптимизировалась в создании слабых CASes особенно быстро, поэтому текущая реализация просто вызывает полный CAS, потому что она быстро реализуется, и будущая версия улучшит ее.

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

В общем, постарайтесь не делать каких-либо выводов о concurrency -связанном поведении посредством экспериментов. Есть так много переменных, которые следует учитывать, если вы не следите за тем, что JLS гарантирует правильность, то ваша программа может разорваться в любое время (возможно, в другой архитектуре, возможно, при более агрессивной оптимизации, вызванной небольшой изменение макета вашего кода, возможно, в будущих сборках JVM, которые еще не существуют и т.д.). Там never причина предполагать, что вы можете уйти от чего-то, что не гарантировано, потому что эксперименты показывают, что "это работает".

Ответ 2

Инструкция x86 для "атомного сравнения и замены" LOCK CMPXCHG. Эта инструкция создает полный забор памяти.

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

Но для x86 другие архитектуры (включая будущие варианты x86) могут делать что-то по-другому.

Ответ 3

weakCompareAndSwap не гарантируется быстрее; это просто позволило быстрее. Вы можете посмотреть открытый код OpenJDK, чтобы узнать, что некоторые умные люди решили сделать с этим разрешением:

А именно: оба они реализованы как однострочный

return unsafe.compareAndSwapObject(this, valueOffset, expect, update);

Они имеют точно такую ​​же производительность, потому что они имеют точно такую ​​же реализацию! (по крайней мере, в OpenJDK). Другие люди отметили, что в любом случае вы не можете делать ничего лучше на x86, потому что оборудование уже дает вам кучу гарантий "бесплатно". Это только на более простых архитектурах, таких как ARM, что вам нужно беспокоиться об этом.