Thursday, May 28, 2009

Does it matter if immutable data is slow?

Note: this post is specifically about Java.

One thing I noticed about the benchmarks in the blog post I linked to on my last post is that the Java tests weren't wrapped in synchronized blocks (which would be a given in any concurrent program).

So I decided to modify it a bit and see how things went. Here's the code:

public final class Bench {
 public static Object mutex = new Object();
 public static Object getMutex() {
  return mutex;
 }
 //immutable
 private static final class Person0 {
  public final String name;
  public final int age;
  public final double balance;
  
  public Person0(final String name, final int age, final double balance) {
   this.name = name;
   this.age = age;
   this.balance = balance;
  }
 }
 
 //idiomatic mutable
 private static final class Person1 {
  private String name;
  private int age;
  private double balance;
  
  public Person1(final String name, final int age, final double balance) {
   this.name = name;
   this.age = age;
   this.balance = balance;
  }
  
  public void setName(final String n) {
   this.name = n;
  }
  
  public void setAge(final int n) {
   this.age = n;
  }
  
  public int getAge() {
   return this.age;
  }
  
  public void deposit(final double n) {
   this.balance += n;
  }
 }
 
 //mutable w/ public fields
 private static final class Person2 {
  public String name;
  public int age;
  private double balance;
  
  public Person2(final String name, final int age, final double balance) {
   this.name = name;
   this.age = age;
   this.balance = balance;
  }
  
  public void deposit(final double n) {
   this.balance += n;
  } 
 }
 
 private interface Test {
  double test(int iters);
 }

 private static final Test TEST0 = new Test() {
  public double test(int iters) {
   Person0 person = new Person0("John Doe", 19, 100.0);
   for (int i = 0; i != iters; i++) {
    synchronized (mutex) {
     person = new Person0(person.name, person.age + i, person.balance + 1.0);
    }
   }
   return person.balance;
  }
 };
 
 private static final Test TEST1 = new Test() {
  public double test(int iters) {
   Person0 person = new Person0("John Doe", 19, 100.0);
   Person0 p;
   for (int i = 0; i != iters; i++) {
    p = new Person0(person.name, person.age + i, person.balance + 1.0);
    synchronized (mutex) {
     person = p;
    }
   }
   return person.balance;
  }
 };
 
 private static final Test TEST2 = new Test() {
  public double test(int iters) {
   Person1 person = new Person1("John Doe", 19, 100.0);
   for (int i = 0; i != iters; i++) {
    synchronized (getMutex()) {
     person.setName("John Doe");
     person.setAge(person.getAge() + i);
     person.deposit(1.0);
    }
   }
   return person.balance;
  }
 };
 
 private static final Test TEST3 = new Test() {
  public double test(int iters) {
   Person1 person = new Person1("John Doe", 19, 100.0);
   for (int i = 0; i != iters; i++) {
    synchronized (mutex) {
     person.name = "John Doe";
     person.age += i;
     person.deposit(1.0);
    }
   }
   return person.balance;
  }
 };
 
 private static final Test TEST4 = new Test() {
  public double test(int iters) {
   Person0 person = new Person0("John Doe", 19, 100.0);
   for (int i = 0; i != iters; i++) {
    person = new Person0(person.name, person.age + i, person.balance + 1.0);
   }
   return person.balance;
  }
 };
 
 private static final Test TEST5 = new Test() {
  public double test(int iters) {
   Person1 person = new Person1("John Doe", 19, 100.0);
   for (int i = 0; i != iters; i++) {
    person.name = "John Doe";
    person.age += i;
    person.deposit(1.0);
   }
   return person.balance;
  }
 };
 
 private static double test(int times, Test test, int iters) {
  long best = Long.MAX_VALUE;
  double balance;
  for (int i = 0; i != times; i++) {
   long now = System.currentTimeMillis();
   balance = test.test(iters);
   now = System.currentTimeMillis() - now;
   if (best > now) best = now;
  }
  return (double) iters / ((double) best / 1000.0);
 }

 public static void main(String[] args) {
  final int iters = 10000000;
  System.out.printf("Immutable sync all: %f updates/s\n", test(5, TEST0, iters));
  System.out.printf("Immutable sync =: %f updates/s\n", test(5, TEST1, iters));
  System.out.printf("Mutable idiomatic: %f updates/s\n", test(5, TEST2, iters));
  System.out.printf("Mutable fields:  %f updates/s\n", test(5, TEST3, iters));
  System.out.printf("Unsafe immutable: %f updates/s\n", test(5, TEST4, iters));
  System.out.printf("Unsafe mutable:  %f updates/s\n", test(5, TEST5, iters));
 }
}

And the results:

Immutable syncing constructor14,903,129 updates/s
Immutable syncing assignment14,880,952 updates/s
Mutable with idiomatic Java18,832,391 updates/s
Mutable accessing fields directly18,832,391 updates/s
Non-synced immutable70,921,985 updates/s
Non-synced mutable322,580,645 updates/s

Interesting points to note:

  • Immutables turn out to be slightly faster than mutables when synchronization is involved.
  • Using idiomatic set/get methods directly seems to be about as fast as accessing fields directly (albeit more verbose).
  • Non-thread-safe code runs about an order of magnitude faster than the synchronized counterparts. Does that mean I need 20+ CPUs to match the speed of a single-threaded program (due to synchronization overhead)?

So is concurrency really overhyped or did I just do something dumb with the code?

No comments:

Post a Comment