Ich untersuche die beste Leistung von Multithreading-Inkrement. Ich überprüfte Implementierung auf der Grundlage von Synchronisierung, AtomicInteger und benutzerdefinierte Implementierung wie in AtomicInteger, aber mit ParkNanos (1) auf fehlgeschlagenem CAS.Java Increment Benchmark
private int customAtomic() {
int ret;
for (;;) {
ret = intValue;
if (unsafe.compareAndSwapInt(this, offsetIntValue, ret, ++ret)) {
break;
}
LockSupport.parkNanos(1);
}
return ret;
}
I Benchmark gemacht basierend auf JMH: klare Ausführung jedes Verfahren, jede Methode mit verbrauchen CPU (1,2,4,8,16 mal) und nur CPU verbrauchen. Jede Benchmark-Methode auf Intel (R) Xeon (R) CPU E5-1680 v2 @ 3.00 GHz, 8 Core + 8 HT 64 GB RAM, auf 1-17 Threads durchgeführt. Die Ergebnisse haben mich überrascht:
- CAS am effektivsten in 1 Thread. 2-Faden - ähnliches Ergebnis mit Monitor. 3 und mehr - schlechter als Monitor, ~ 2 mal.
- Die benutzerdefinierte Implementierung ist in den meisten Fällen in 2-3 mal besser, als Monitor.
- Aber in der benutzerdefinierten Implementierung geschieht gelegentlich zufällig schlechte Ausführung. Guter Fall - 50 OP/Mikrosekunden, schlechter Fall - 0,5 OP/Mikrosekunden.
Fragen:
- Warum Atomicinteger nicht auf Synchronize basiert, ist es produktiver, dann aktuelle impl?
- Warum AtomicInteger LockSupport.parkNanos (1) nicht verwendet, auf CAS fehlschlagen?
- Warum passiert diese Spikes in benutzerdefinierten Implementierung?
Ich habe versucht, diesen Test einige Male durchzuführen, und Spike geschieht immer in unterschiedlicher Anzahl Threads. Auch ich habe diesen Test in einer anderen Maschine versucht, das Ergebnis ist das gleiche. Vielleicht ist es ein Testproblem. In "bad case" von benutzerdefinierten impl in StackProfiler, ich sehe:
....[Thread state distributions]....................................................................
50.0% RUNNABLE
49.9% TIMED_WAITING
....[Thread state: RUNNABLE]........................................................................
43.3% 86.6% sun.misc.Unsafe.park
5.8% 11.6% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest.incrementCustomAtomicWithWork_thrpt_jmhStub
0.8% 1.7% org.openjdk.jmh.infra.Blackhole.consumeCPU
0.1% 0.1% com.jad.IncrementBench$Worker.work
0.0% 0.0% java.lang.Thread.currentThread
0.0% 0.0% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest._jmh_tryInit_f_benchmarkparams1_0
0.0% 0.0% org.openjdk.jmh.infra.generated.BenchmarkParams_jmhType_B1.<init>
....[Thread state: TIMED_WAITING]...................................................................
49.9% 100.0% sun.misc.Unsafe.park
In "guten Fall":
....[Thread state distributions]....................................................................
88.2% TIMED_WAITING
11.8% RUNNABLE
....[Thread state: TIMED_WAITING]...................................................................
88.2% 100.0% sun.misc.Unsafe.park
....[Thread state: RUNNABLE]........................................................................
5.6% 47.9% sun.misc.Unsafe.park
3.1% 26.3% org.openjdk.jmh.infra.Blackhole.consumeCPU
2.4% 20.3% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest.incrementCustomAtomicWithWork_thrpt_jmhStub
0.6% 5.5% com.jad.IncrementBench$Worker.work
0.0% 0.0% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest.incrementCustomAtomicWithWork_Throughput
0.0% 0.0% java.lang.Thread.currentThread
0.0% 0.0% org.openjdk.jmh.infra.generated.BenchmarkParams_jmhType_B1.<init>
0.0% 0.0% sun.misc.Unsafe.putObject
0.0% 0.0% org.openjdk.jmh.runner.InfraControlL2.announceWarmdownReady
0.0% 0.0% sun.misc.Unsafe.compareAndSwapInt
Link to result graphs. X - threads count, Y - thpt, op/microsec
UPD
Okay, ich weiß, ich verstehe das, wenn ich ParkNanos benutze, kann ein Thread auch das Schloss (CAS) für längere Zeit halten. Threads mit CAS-Fehler werden in den Ruhezustand versetzt und nur ein Thread arbeitet und erhöht den Wert. Ich sehe, dass für große Nebenläufigkeit, wenn die Arbeit so klein ist - AtomicInteger ist nicht besser Ansatz. Aber wenn wir workSize erhöhen, zum Beispiel auf Stufe = CASThrpt/threadNum, sollte es funktioniert: Für die lokale Maschine I workSize = 300, Ergebnis meiner Prüfung eingestellt haben:
Benchmark (workSize) Mode Cnt Score Error Units
IncrementBench.incrementAtomicWithWork 300 thrpt 3 4.133 ± 0.516 ops/us
IncrementBench.incrementCustomAtomicWithWork 300 thrpt 3 1.883 ± 0.234 ops/us
IncrementBench.lockIntWithWork 300 thrpt 3 3.831 ± 0.501 ops/us
IncrementBench.onlyWithWork 300 thrpt 3 4.339 ± 0.243 ops/us
Atomicinteger - win, lock - zweite Ort, Gewohnheit - Dritter. Aber Problem mit Spikes, immer noch nicht klar. Und ich habe vergessen, Java-Version: Java (TM) SE Laufzeitumgebung (Build 1.7.0_79-b15) Java HotSpot (TM) 64-Bit-Server-VM (Build 24.79-b02, gemischter Modus)
den Anruf zu parkNanos entfernen. Sie möchten so schnell wie möglich erneut iterieren. Stellen Sie außerdem sicher, dass intValue flüchtig ist. Sonst kann ret = intValue nicht den gleichen Wert sehen, den das CAS macht –
'Aber Problem mit Spikes, immer noch nicht klar' Haben Sie Ihre Log-Datei überprüft? Es gibt eine Menge von '' Ereignisse –
Ivan
Wenn Sie in den Code anschauen, werden Sie System.exit (0) in @Setup Methode. Es ist für sinnlose Fälle zu entfernen, Beispiel: löschen Sie AtomicInteger mit params workSize (2,4,8 ..). Dieser Fall ist unabhängig vom Parameter. –