30 votes

Java compare et échange la sémantique et les performances

Qu'est-ce que la sémantique de comparer et d'échanger en Java? À savoir, la comparaison et la méthode d'échange de l' AtomicInteger seulement garantir commandé l'accès entre les différents threads particulier à l'emplacement de mémoire de la atomique en entier instance, ou qu'il ne garantit commandé l'accès à tous les emplacements dans la mémoire, c'est à dire qu'il agit comme si il s'agissait d'un volatile (mémoire de la clôture).

À partir de la docs:

  • weakCompareAndSet atomiquement lit, et à condition écrit une variable, mais ne crée pas de passe-avant de rangements, de sorte que ne fournit aucune garantie à l'égard de précédents ou suivants les lectures et les écritures de toutes les variables autres que la cible de l' weakCompareAndSet.
  • compareAndSet et tous les autres en lecture et de mise à jour des opérations telles que l' getAndIncrement ont la mémoire des effets de la lecture et de l'écriture des variables volatiles.

Il est évident à partir de la documentation de l'API compareAndSet des actes comme si c'était une variable volatile. Toutefois, weakCompareAndSet est censé juste changer son emplacement mémoire spécifique. Ainsi, si cet emplacement de la mémoire est exclusif à la cache d'un processeur unique, weakCompareAndSet est censé être beaucoup plus rapide que régulier compareAndSet.

Je vous pose cette question parce que j'ai comparé les méthodes suivantes en exécutant threadnum des threads différents, variant threadnum de 1 à 8, et ayant totalwork=1e9 (le code est écrit en Scala, un compilés statiquement JVM de la langue, mais à la fois son sens et le bytecode de traduction sont isomorphe à celle de Java dans ce cas - cette information doit être claire):

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
}

sur un AMD avec 4 double 2.8 GHz cœurs, et un 2,67 GHz 4-core i7. La JVM est un Serveur Sun JVM Hotspot 1.6. Les résultats ne montrent aucune différence de performances.

Spécifications: AMD 8220 4x dual-core @ 2.8 GHz

Nom du Test: loop_atomic_tlocal_cas

  • Thread num.: 1

Temps d'exécution: (montrant des 3 derniers) 7504.562 7502.817 7504.626 (moyenne = 7415.637 min = 7147.628 max = 7504.886 )

  • Thread num.: 2

Temps d'exécution: (montrant des 3 derniers) 3751.553 3752.589 3751.519 (moyenne = 3713.5513 min = 3574.708 max = 3752.949 )

  • Thread num.: 4

Temps d'exécution: (montrant des 3 derniers) 1890.055 1889.813 1890.047 (moyenne = 2065.7207 min = 1804.652 max = 3755.852 )

  • Thread num.: 8

Temps d'exécution: (montrant des 3 derniers) 960.12 989.453 970.842 (moyenne = 1058.8776 min = 940.492 max = 1893.127 )


Nom du Test: loop_atomic_weakcas

  • Thread num.: 1

Temps d'exécution: (montrant des 3 derniers) 7325.425 7057.03 7325.407 (moyenne = 7231.8682 min = 7057.03 max = 7325.45 )

  • Thread num.: 2

Temps d'exécution: (montrant des 3 derniers) 3663.21 3665.838 3533.406 (moyenne = 3607.2149 min = 3529.177 max = 3665.838 )

  • Thread num.: 4

Temps d'exécution: (montrant des 3 derniers) 3664.163 1831.979 1835.07 (moyenne = 2014.2086 min = 1797.997 max = 3664.163 )

  • Thread num.: 8

Temps d'exécution: (montrant des 3 derniers) 940.504 928.467 921.376 (moyenne = 943.665 min = 919.985 max = 997.681 )


Nom du Test: loop_atomic_tlocal_weakcas

  • Thread num.: 1

Temps d'exécution: (montrant des 3 derniers) 7502.876 7502.857 7502.933 (moyenne = 7414.8132 min = 7145.869 max = 7502.933 )

  • Thread num.: 2

Temps d'exécution: (montrant des 3 derniers) 3752.623 3751.53 3752.434 (moyenne = 3710.1782 min = 3574.398 max = 3752.623 )

  • Thread num.: 4

Temps d'exécution: (montrant des 3 derniers) 1876.723 1881.069 1876.538 (moyenne = 4110.4221 min = 1804.62 max = 12467.351 )

  • Thread num.: 8

Temps d'exécution: (montrant des 3 derniers) 959.329 1010.53 969.767 (moyenne = 1072.8444 min = 959.329 max = 1880.049 )

Spécifications: Intel core i7 quad-core @ 2.67 GHz

Nom du Test: loop_atomic_tlocal_cas

  • Thread num.: 1

Temps d'exécution: (montrant des 3 derniers) 8138.3175 8130.0044 8130.1535 (moyenne = 8119.2888 min = 8049.6497 max = 8150.1950 )

  • Thread num.: 2

Temps d'exécution: (montrant des 3 derniers) 4067.7399 4067.5403 4068.3747 (moyenne = 4059.6344 min = 4026.2739 max = 4068.5455 )

  • Thread num.: 4

Temps d'exécution: (montrant des 3 derniers) 2033.4389 2033.2695 2033.2918 (moyenne = 2030.5825 min = 2017.6880 max = 2035.0352 )


Nom du Test: loop_atomic_weakcas

  • Thread num.: 1

Temps d'exécution: (montrant des 3 derniers) 8130.5620 8129.9963 8132.3382 (moyenne = 8114.0052 min = 8042.0742 max = 8132.8542 )

  • Thread num.: 2

Temps d'exécution: (montrant des 3 derniers) 4066.9559 4067.0414 4067.2080 (moyenne = 4086.0608 min = 4023.6822 max = 4335.1791 )

  • Thread num.: 4

Temps d'exécution: (montrant des 3 derniers) 2034.6084 2169.8127 2034.5625 (moyenne = 2047.7025 min = 2032.8131 max = 2169.8127 )


Nom du Test: loop_atomic_tlocal_weakcas

  • Thread num.: 1

Temps d'exécution: (montrant des 3 derniers) 8132.5267 8132.0299 8132.2415 (moyenne = 8114.9328 min = 8043.3674 max = 8134.0418 )

  • Thread num.: 2

Temps d'exécution: (montrant des 3 derniers) 4066.5924 4066.5797 4066.6519 (moyenne = 4059.1911 min = 4025.0703 max = 4066.8547 )

  • Thread num.: 4

Temps d'exécution: (montrant des 3 derniers) 2033.2614 2035.5754 2036.9110 (moyenne = 2033.2958 min = 2023.5082 max = 2038.8750 )


Alors qu'il est possible que, au fil les habitants dans l'exemple ci-dessus se retrouvent dans la même lignes de cache, il me semble qu'il est n'observe pas de différence de performances entre les SAE et sa version faible.

Cela pourrait signifier que, en fait, un faible comparer et swap agit comme part entière de la mémoire de la clôture, c'est à dire des actes comme si c'était une variable volatile.

Question: Est-ce que cette observation correcte? Aussi, est-il connu de l'architecture de Java ou de la distribution pour laquelle une faible comparer et l'ensemble est vraiment le plus rapide? Si non, quel est l'avantage de l'utilisation d'une faiblesse de CAS en premier lieu?

28voto

Daniel Points 7960

L'instruction x86 pour "comparer et échanger atomiquement" est LOCK CMPXCHG . Cette instruction crée une clôture de mémoire complète.

Aucune instruction ne fait ce travail sans créer de barrière de mémoire, il est donc très probable que compareAndSet et weakCompareAndSet soient mappés à LOCK CMPXCHG et effectuent une mémoire complète clôture.

Mais c'est pour x86, d'autres architectures (y compris les futures variantes de x86) peuvent faire les choses différemment.

26voto

Andrzej Doyle Points 52541

Une faiblesse de comparer et de swap pourrait agir en tant que volatile variable, en fonction de l'implémentation de la JVM, c'est sûr. En fait, je ne serais pas surpris si sur certaines architectures, il n'est pas possible de mettre en œuvre une faiblesse des autorités de certification dans une nettement plus performant que le CAS normal. Sur ces architectures, il pourrait bien être le cas de la faiblesse des Cas sont mis en œuvre exactement la même chose qu'un plein TAS. Ou il pourrait simplement être que votre JVM n'a pas eu beaucoup d'optimisation mis dans la faiblesse de Cas particulièrement rapide, de sorte que le courant de la mise en œuvre juste invoque un plein CAS parce que c'est rapide à mettre en œuvre, et une future version d'affiner ce.

JL dit simplement que la faiblesse de CAS ne permet pas d'établir un passe-avant la relation, de sorte qu'il est tout simplement qu'il n'existe aucune garantie que la modification qu'il provoque est visible dans d'autres threads. Tout ce que vous obtenez dans ce cas est la garantie que le de compare-and-set opération est atomique, mais avec aucune garantie quant à la visibilité de l' (potentiellement) la nouvelle valeur. Ce n'est pas le même que garantissant qu'il ne sera pas être vu, si vos tests sont en accord avec cela.

En général, essayez d'éviter de faire des conclusions sur la simultanéité de comportement lié par l'expérimentation. Il y a tellement de variables à prendre en compte, que si vous ne suivez pas ce que l'JLS garanties être correctes, alors votre programme pourrait casser à tout moment (peut-être sur une architecture différente, peut-être plus agressives d'optimisation qui est invité par une légère modification de la disposition de votre code, peut-être dans le cadre des futures versions de la JVM qui n'existent pas encore, etc.). Il n'y a jamais une raison de supposer que vous pouvez obtenir loin avec quelque chose qui est indiqué ne pas être garanti, car les expériences montrent que "ça fonctionne".

5voto

Quuxplusone Points 4320

weakCompareAndSwap n'est pas garanti d'être plus rapide; il est seulement permis d'être plus rapide. Vous pouvez regarder le code open source de l'OpenJDK pour voir ce que certaines personnes ont décidé de faire de cette autorisation:

À savoir: Ils sont à la fois mises en œuvre, comme le one-liner

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

Ils ont exactement le même rendement, car ils ont exactement la même mise en œuvre! (dans OpenJDK au moins). D'autres personnes ont insisté sur le fait que vous ne pouvez pas vraiment faire mieux sur x86 de toute façon, parce que le matériel vous donne déjà un tas de garanties "gratuitement". C'est seulement sur des architectures plus simples comme le BRAS que vous ayez à vous en préoccuper.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X