Java сравнивает и меняет семантику и производительность - PullRequest
32 голосов
/ 15 ноября 2010

Какова семантика сравнения и обмена в 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 двухъядерный с частотой 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 (среднее = 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

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

  • Номер резьбы: 8

Выполнитьвремя: (показывает последние 3) 940,50 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 макс = 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 @ 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.3747 (ср = 4059.6344 мин = 4026.2739 макс = 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 ]

29 голосов
/ 15 ноября 2010

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

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

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

29 голосов
/ 15 ноября 2010

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

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

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

6 голосов
/ 14 марта 2014

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

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

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

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

...