Java comparer et de swaps, de la sémantique et de la performance
Qu'est-ce que la sémantique de comparer et d'échanger en Java? À savoir, la comparaison et la méthode d'échange d'un 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).
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 laweakCompareAndSet
.compareAndSet
et tous les autres lire-et-opérations de mise à jour commegetAndIncrement
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
actes comme si c'était une variable volatile. Cependant, 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 le 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?
- x86 ne prend pas en charge un non-fortement de la garantie des TAS comme il a VERROUILLAGE de préfixe. Si Faible et Standard AC sont de la même opération.
Vous devez vous connecter pour publier un commentaire.
Un faible comparer et 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 actuel mise en œuvre juste invoque un plein CAS parce que c'est rapide à mettre en œuvre, et une future version d'affiner ce.
La JLS dit simplement que la faiblesse de CAS ne permet pas d'établir un se passe-avant relation, il est donc tout simplement qu'il n'est pas 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 la même que de garantir qu'il ne ê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 pause à 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 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".
future version will refine this
il déjà fait cela dans le jdk-9, oùweak
est bien...weak
L'instruction x86 pour "atomiquement de comparer et d'échange" est
LOCK CMPXCHG
. Cette instruction crée une mémoire pleine de clôture.Il n'y a pas d'instruction qui fait ce travail sans la création d'une mémoire de la clôture, de sorte qu'il est très probable que les deux
compareAndSet
etweakCompareAndSet
carte deLOCK CMPXCHG
et d'effectuer une mémoire pleine de clôture.Mais c'est pour les architectures x86, d'autres architectures (y compris les variantes de x86) peut faire les choses différemment.
weakCompareAndSwap
n'est pas garanti pour être plus rapide, c'est juste permis pour ê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:le code source de compareAndSet
le code source de weakCompareAndSet
À savoir: Ils sont à la fois mises en œuvre, comme le one-liner
Ils ont exactement le même niveau de performance, parce que 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.