Want to show your appreciation?
Please a cup of tea.

Monday, May 25, 2009

AtomicInteger vs. Synchronized Monitor

Update (2/7/2010): I noticed a lot of Google traffic landing this page looking for .Net version of AtomicInteger. This is now available as part of the Java’s concurrent API port to .Net, you can download a Bate version here.

When porting the Java's AtomicInteger to .Net, I did a performance test to compare between the implementation using Interlocked and Monitor. The surprised finding inspired me to do a similar test in Java to see how does Java performs.

Let's start with below interface.

public interface AtomicTest {
	public int get();
	public void set(int value);
	public boolean compareAndSet(int expected, int newValue);
	public int incrementAndGet();
	public int decrementAndGet();
}

And we are going to have two implementations, one users AtomicInterger:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest extends AtomicInteger implements AtomicTest {}

Another one uses synchronized access to a volatile field. Except that the read access is not synchronized.

public class MonitorAtomicTest implements AtomicTest {
	private volatile int _value;
	public synchronized boolean compareAndSet(int expected, int newValue) {
		if (expected==_value) {
			_value = newValue;
			return true;
		}
		return false;
	}

	public synchronized int decrementAndGet() {
		return --_value;
	}

	public int get() {
		return _value;
	}

	public synchronized int incrementAndGet() {
		return ++_value;
	}

	public synchronized void set(int value) {
		_value = value;
	}
}

Similar to the .Net test, we run below methods for each implementation with loop set to one million.

    private void runCompareAndSet() {
        int result1, result2, result3;
        for (int i = loop - 1; i >= 0; i--) {
            atomic.compareAndSet(100, 50);
            result1 = atomic.get();
            atomic.compareAndSet(50, 100);
            result2 = atomic.get();
            atomic.compareAndSet(100, 50);
            result3 = atomic.get();
        }
    }

    private void runIncrement() {
        int result1, result2, result3;
        for (int i = loop - 1; i >= 0; i--) {
            atomic.incrementAndGet();
            result1 = atomic.get();
            atomic.incrementAndGet();
            result2 = atomic.get();
            atomic.incrementAndGet();
            result3 = atomic.get();
        }
    }

Finally, we run each method above in multiple threads in parallel. Amount of thread can be passed as command line parameter. Below is the main method, for the detail of how things get done, the full source code is available here.

    public static void main(String[] args)
        throws InterruptedException 
    {
        if (args.length > 0) threadCount = Integer.parseInt(args[0]);
        verbose = "true".equalsIgnoreCase(System.getProperty("verbose"));

        TestRunner a = new TestRunner(new AtomicIntegerTest());
        TestRunner b = new TestRunner(new MonitorAtomicTest());

        a.runCompareAndSetInParallel();
        b.runCompareAndSetInParallel();
        a.runIncrementInParallel();
        b.runIncrementInParallel();
    }

Ran the test on three Windows boxes with lated JRE which is 1.6.0_13. Detail of test hardware and OS can be found in my previous post about similar test for .Net.

Below I listed the test result:

Test result on a two(2) CPU box with client VM

D:\>java -jar AtomicIntegerVsMonitor.jar 1 
  AtomicIntegerTest.runCompareAndSet (ns):   109 Average,   109 Minimal,   109 Maxmial,  1 Threads
  MonitorAtomicTest.runCompareAndSet (ns):   219 Average,   219 Minimal,   219 Maxmial,  1 Threads
  AtomicIntegerTest.runIncrement     (ns):   125 Average,   125 Minimal,   125 Maxmial,  1 Threads
  MonitorAtomicTest.runIncrement     (ns):   250 Average,   250 Minimal,   250 Maxmial,  1 Threads
	
D:\>java -jar AtomicIntegerVsMonitor.jar 1 
  AtomicIntegerTest.runCompareAndSet (ns):   109 Average,   109 Minimal,   109 Maxmial,  1 Threads
  MonitorAtomicTest.runCompareAndSet (ns):   219 Average,   219 Minimal,   219 Maxmial,  1 Threads
  AtomicIntegerTest.runIncrement     (ns):   125 Average,   125 Minimal,   125 Maxmial,  1 Threads
  MonitorAtomicTest.runIncrement     (ns):   265 Average,   265 Minimal,   265 Maxmial,  1 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 2 
  AtomicIntegerTest.runCompareAndSet (ns):   211 Average,   203 Minimal,   219 Maxmial,  2 Threads
  MonitorAtomicTest.runCompareAndSet (ns):  1359 Average,  1297 Minimal,  1422 Maxmial,  2 Threads
  AtomicIntegerTest.runIncrement     (ns):   312 Average,   312 Minimal,   312 Maxmial,  2 Threads
  MonitorAtomicTest.runIncrement     (ns):  1461 Average,  1453 Minimal,  1469 Maxmial,  2 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 2 
  AtomicIntegerTest.runCompareAndSet (ns):   203 Average,   203 Minimal,   203 Maxmial,  2 Threads
  MonitorAtomicTest.runCompareAndSet (ns):  1390 Average,  1375 Minimal,  1406 Maxmial,  2 Threads
  AtomicIntegerTest.runIncrement     (ns):   313 Average,   313 Minimal,   313 Maxmial,  2 Threads
  MonitorAtomicTest.runIncrement     (ns):  1359 Average,  1359 Minimal,  1359 Maxmial,  2 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 4 
  AtomicIntegerTest.runCompareAndSet (ns):   449 Average,   406 Minimal,   485 Maxmial,  4 Threads
  MonitorAtomicTest.runCompareAndSet (ns):  2590 Average,  2469 Minimal,  2641 Maxmial,  4 Threads
  AtomicIntegerTest.runIncrement     (ns):   582 Average,   562 Minimal,   625 Maxmial,  4 Threads
  MonitorAtomicTest.runIncrement     (ns):  2624 Average,  2406 Minimal,  2765 Maxmial,  4 Threads
	
D:\>java -jar AtomicIntegerVsMonitor.jar 4 
  AtomicIntegerTest.runCompareAndSet (ns):   300 Average,   235 Minimal,   406 Maxmial,  4 Threads
  MonitorAtomicTest.runCompareAndSet (ns):  2649 Average,  2532 Minimal,  2797 Maxmial,  4 Threads
  AtomicIntegerTest.runIncrement     (ns):   602 Average,   563 Minimal,   641 Maxmial,  4 Threads
  MonitorAtomicTest.runIncrement     (ns):  2871 Average,  2766 Minimal,  2953 Maxmial,  4 Threads
	
D:\>java -jar AtomicIntegerVsMonitor.jar 16 
  AtomicIntegerTest.runCompareAndSet (ns):  1305 Average,   906 Minimal,  1703 Maxmial, 16 Threads
  MonitorAtomicTest.runCompareAndSet (ns):  9507 Average,  5344 Minimal, 10610 Maxmial, 16 Threads
  AtomicIntegerTest.runIncrement     (ns):  2064 Average,  1516 Minimal,  2516 Maxmial, 16 Threads
  MonitorAtomicTest.runIncrement     (ns):  9944 Average,  8312 Minimal, 10703 Maxmial, 16 Threads
	
D:\>java -jar AtomicIntegerVsMonitor.jar 16 
  AtomicIntegerTest.runCompareAndSet (ns):  1309 Average,   984 Minimal,  1625 Maxmial, 16 Threads
  MonitorAtomicTest.runCompareAndSet (ns): 10958 Average,  8485 Minimal, 12188 Maxmial, 16 Threads
  AtomicIntegerTest.runIncrement     (ns):  2093 Average,  1188 Minimal,  2515 Maxmial, 16 Threads
  MonitorAtomicTest.runIncrement     (ns): 12046 Average, 11188 Minimal, 13110 Maxmial, 16 Threads

Test result on a four(4) CPU box with client VM

D:\>java -jar AtomicIntegerVsMonitor.jar 1
  AtomicIntegerTest.runCompareAndSet (ns):   203 Average,   203 Minimal,   203 Maxmial,  1 Threads
  MonitorAtomicTest.runCompareAndSet (ns):   516 Average,   516 Minimal,   516 Maxmial,  1 Threads
  AtomicIntegerTest.runIncrement     (ns):   218 Average,   218 Minimal,   218 Maxmial,  1 Threads
  MonitorAtomicTest.runIncrement     (ns):   563 Average,   563 Minimal,   563 Maxmial,  1 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 1
  AtomicIntegerTest.runCompareAndSet (ns):   219 Average,   219 Minimal,   219 Maxmial,  1 Threads
  MonitorAtomicTest.runCompareAndSet (ns):   531 Average,   531 Minimal,   531 Maxmial,  1 Threads
  AtomicIntegerTest.runIncrement     (ns):   219 Average,   219 Minimal,   219 Maxmial,  1 Threads
  MonitorAtomicTest.runIncrement     (ns):   578 Average,   578 Minimal,   578 Maxmial,  1 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 2
  AtomicIntegerTest.runCompareAndSet (ns):  1047 Average,  1047 Minimal,  1047 Maxmial,  2 Threads
  MonitorAtomicTest.runCompareAndSet (ns):  4930 Average,  4922 Minimal,  4938 Maxmial,  2 Threads
  AtomicIntegerTest.runIncrement     (ns):   929 Average,   922 Minimal,   937 Maxmial,  2 Threads
  MonitorAtomicTest.runIncrement     (ns):  4891 Average,  4891 Minimal,  4891 Maxmial,  2 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 2
  AtomicIntegerTest.runCompareAndSet (ns):   890 Average,   890 Minimal,   890 Maxmial,  2 Threads
  MonitorAtomicTest.runCompareAndSet (ns):  3922 Average,  3922 Minimal,  3922 Maxmial,  2 Threads
  AtomicIntegerTest.runIncrement     (ns):   961 Average,   953 Minimal,   969 Maxmial,  2 Threads
  MonitorAtomicTest.runIncrement     (ns):  4570 Average,  4562 Minimal,  4578 Maxmial,  2 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 4
  AtomicIntegerTest.runCompareAndSet (ns):  2558 Average,  2484 Minimal,  2594 Maxmial,  4 Threads
  MonitorAtomicTest.runCompareAndSet (ns): 15445 Average, 15375 Minimal, 15547 Maxmial,  4 Threads
  AtomicIntegerTest.runIncrement     (ns):  5359 Average,  5250 Minimal,  5406 Maxmial,  4 Threads
  MonitorAtomicTest.runIncrement     (ns): 15269 Average, 15141 Minimal, 15344 Maxmial,  4 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 4
  AtomicIntegerTest.runCompareAndSet (ns):  2573 Average,  2515 Minimal,  2593 Maxmial,  4 Threads
  MonitorAtomicTest.runCompareAndSet (ns): 15363 Average, 14969 Minimal, 15516 Maxmial,  4 Threads
  AtomicIntegerTest.runIncrement     (ns):  5296 Average,  5250 Minimal,  5328 Maxmial,  4 Threads
  MonitorAtomicTest.runIncrement     (ns): 15823 Average, 15734 Minimal, 15890 Maxmial,  4 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 16
  AtomicIntegerTest.runCompareAndSet (ns):  8722 Average,  6859 Minimal, 10047 Maxmial, 16 Threads
  MonitorAtomicTest.runCompareAndSet (ns): 60182 Average, 57750 Minimal, 63984 Maxmial, 16 Threads
  AtomicIntegerTest.runIncrement     (ns): 18828 Average, 12032 Minimal, 20672 Maxmial, 16 Threads
  MonitorAtomicTest.runIncrement     (ns): 59987 Average, 58516 Minimal, 60594 Maxmial, 16 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 16
  AtomicIntegerTest.runCompareAndSet (ns):  8216 Average,  4828 Minimal, 10219 Maxmial, 16 Threads
  MonitorAtomicTest.runCompareAndSet (ns): 68320 Average, 67188 Minimal, 68719 Maxmial, 16 Threads
  AtomicIntegerTest.runIncrement     (ns): 18796 Average, 14719 Minimal, 21469 Maxmial, 16 Threads
  MonitorAtomicTest.runIncrement     (ns): 67786 Average, 66844 Minimal, 68281 Maxmial, 16 Threads

Test result on a sixteen(16) CPU box with server VM

D:\>java -jar AtomicIntegerVsMonitor.jar 1
  AtomicIntegerTest.runCompareAndSet (ns):    94 Average,    94 Minimal,    94 Maxmial,  1 Threads
  MonitorAtomicTest.runCompareAndSet (ns):   250 Average,   250 Minimal,   250 Maxmial,  1 Threads
  AtomicIntegerTest.runIncrement     (ns):   109 Average,   109 Minimal,   109 Maxmial,  1 Threads
  MonitorAtomicTest.runIncrement     (ns):   250 Average,   250 Minimal,   250 Maxmial,  1 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 1
  AtomicIntegerTest.runCompareAndSet (ns):    94 Average,    94 Minimal,    94 Maxmial,  1 Threads
  MonitorAtomicTest.runCompareAndSet (ns):   250 Average,   250 Minimal,   250 Maxmial,  1 Threads
  AtomicIntegerTest.runIncrement     (ns):   109 Average,   109 Minimal,   109 Maxmial,  1 Threads
  MonitorAtomicTest.runIncrement     (ns):   234 Average,   234 Minimal,   234 Maxmial,  1 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 2
  AtomicIntegerTest.runCompareAndSet (ns):   875 Average,   875 Minimal,   875 Maxmial,  2 Threads
  MonitorAtomicTest.runCompareAndSet (ns):  2461 Average,  2453 Minimal,  2469 Maxmial,  2 Threads
  AtomicIntegerTest.runIncrement     (ns):  1063 Average,  1063 Minimal,  1063 Maxmial,  2 Threads
  MonitorAtomicTest.runIncrement     (ns):  2382 Average,  2375 Minimal,  2390 Maxmial,  2 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 2
  AtomicIntegerTest.runCompareAndSet (ns):  1829 Average,  1829 Minimal,  1829 Maxmial,  2 Threads
  MonitorAtomicTest.runCompareAndSet (ns):  2828 Average,  2828 Minimal,  2828 Maxmial,  2 Threads
  AtomicIntegerTest.runIncrement     (ns):   422 Average,   391 Minimal,   453 Maxmial,  2 Threads
  MonitorAtomicTest.runIncrement     (ns):  3258 Average,  3250 Minimal,  3266 Maxmial,  2 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 4
  AtomicIntegerTest.runCompareAndSet (ns):  3250 Average,  3250 Minimal,  3250 Maxmial,  4 Threads
  MonitorAtomicTest.runCompareAndSet (ns):  6097 Average,  5953 Minimal,  6156 Maxmial,  4 Threads
  AtomicIntegerTest.runIncrement     (ns):  2531 Average,  2531 Minimal,  2531 Maxmial,  4 Threads
  MonitorAtomicTest.runIncrement     (ns):  5808 Average,  5766 Minimal,  5844 Maxmial,  4 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 4
  AtomicIntegerTest.runCompareAndSet (ns):  3000 Average,  3000 Minimal,  3000 Maxmial,  4 Threads
  MonitorAtomicTest.runCompareAndSet (ns):  6097 Average,  5922 Minimal,  6187 Maxmial,  4 Threads
  AtomicIntegerTest.runIncrement     (ns):  2547 Average,  2547 Minimal,  2547 Maxmial,  4 Threads
  MonitorAtomicTest.runIncrement     (ns):  6039 Average,  5969 Minimal,  6094 Maxmial,  4 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 16
  AtomicIntegerTest.runCompareAndSet (ns):  9749 Average,  9749 Minimal,  9749 Maxmial,  16 Threads
  MonitorAtomicTest.runCompareAndSet (ns): 21486 Average, 21078 Minimal, 21641 Maxmial,  16 Threads
  AtomicIntegerTest.runIncrement     (ns): 46216 Average, 45562 Minimal, 46827 Maxmial,  16 Threads
  MonitorAtomicTest.runIncrement     (ns): 24795 Average, 23422 Minimal, 25281 Maxmial,  16 Threads

D:\>java -jar AtomicIntegerVsMonitor.jar 16
  AtomicIntegerTest.runCompareAndSet (ns):  9787 Average,  9781 Minimal,  9797 Maxmial,  16 Threads
  MonitorAtomicTest.runCompareAndSet (ns): 23269 Average, 23109 Minimal, 23390 Maxmial,  16 Threads
  AtomicIntegerTest.runIncrement     (ns): 45608 Average, 45047 Minimal, 46141 Maxmial,  16 Threads
  MonitorAtomicTest.runIncrement     (ns): 19401 Average, 19156 Minimal, 19578 Maxmial,  16 Threads

This test is different from the .Net test. It is quite conclusive:

  • AtomicInteger performs consistently better then Monitor except one test case when running Increment with 16 threads on 16 CUP box.
  • Java's AtomicInteger.compareAndSet performs in peer with .Net's Interlocked.CompareExchange.
  • Java's Monitor is consistently slower then .Net's and getting worse when there are more threads and more collisions.
  • AtomicInteger also suffers from heavy collision as Interlocked does. One extreme case is the Increment test with 16 threads on 16 CPU box. The implementation of AtomicInteger.incrementAndGet uses a looped try of AtomicInteger.compareAndSet, that is how the collision increased. The result of Interlocked.Increment is much better so I believe a different strategy is in use.

No comments:

Post a Comment