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

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

Из docs :

  • weakCompareAndSet атомарно считывает и условно записывает переменную, но не создает каких-либо порядков, предшествующих случаю, поэтому не предоставляет никаких гарантий в отношении предыдущих или последующих операций чтения и записи любых переменных, кроме цели weakCompareAndSet .
  • compareAndSet и всех других операций чтения и обновления, таких как ] getAndIncrement имеют эффекты памяти как для чтения, так и для записи энергозависимых переменных.

Из документации API очевидно, что compareAndSet действует так, как если бы это была энергозависимая переменная. Однако предполагается, что weakCompareAndSet просто изменяет свое конкретное место в памяти. Таким образом, если эта область памяти является эксклюзивной для кеша одного процессора, weakCompareAndSet должен быть намного быстрее, чем обычный compareAndSet .

Я спрашиваю об этом, потому что я ' Мы протестировали следующие методы, запустив threadnum разных потоков, изменив threadnum от 1 до 8 и имея totalwork = 1e9 (код написан на Scala, a статически скомпилированный язык 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-ядерным 2,67 ГГц i7 процессор. JVM - это Sun Server Hotspot JVM 1.6. Результаты не показывают разницы в производительности.

Спецификации: AMD 8220 4x, двухъядерный @ 2,8 ГГц

Название теста: loop_atomic_tlocal_cas

  • Номер потока: 1

Время выполнения: (показаны последние 3) и имеющий 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) и имеющий 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,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 макс. = 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 (avg = 8119.2888 min = 8049.6497 max = 8150.1950)

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

Время выполнения: (показаны последние 3) 4067,7399 4067,5403 4068,3747 (среднее = 4059,6344 мин. = 4026,2739 макс. = 4068,5455)

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

Время выполнения: (показаны последние 3) 2033.4389 2033.2695 2033.2918 (avg = 2030.5825 min = 2017.6880 max = 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 (avg = 4086.0608 min = 4023.6822 max = 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 (avg = 2033.2958 min = 2023.5082 max = 2038.8750)


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

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

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

32
задан mtsz 20 May 2012 в 21:48
поделиться