Friday, February 20, 2009

Java Broke Computer Science

So... yeah, Java broke computer science. But first some background, I'm doing agent based modeling these days, and a common thing you have 2+ nested loops. For example I'm working on a project right now where I'll be looping an outer loop somewhere in the range of 100,000+ times and an inner loop maybe 10,000 - 20,000. Suffice to say, I need to make it as fast as I can. I wrote some little tests so I could get a sense of how long it would take, just to run through the loop and do something small.

A class to do stuff with:

public class Foo {
 private int count;
 private boolean inc;

 public Foo(boolean inc) {
  this.inc = inc;
 }

 public void tick() {
  if (inc) {
   count++;
  }
 }
}

My Test:

private int outerLoop = 100000;
private int innerLoop = 10000;

@Test
public void arrays() {
 Foo[] foos = new Foo[innerLoop];
 for (int i = 0; i < innerLoop; i++) {
  boolean inc = i % 2 == 0 ? true : false;
  foos[i] = new Foo(inc);
 }

 long start = System.currentTimeMillis();
 for (int i = 0; i < outerLoop; i++) {
  for (int j = 0; j < innerLoop; j++) {
   foos[j].tick();
  }
 }
 long fin = System.currentTimeMillis();
 System.out.println("arrays took: " + (fin - start));
}

Takes on my machine in java 1.6 ~2,000 ms I'd like to point out here, that Foo[] foos will not change, so I don't care about speed of modifying a data structure, just the fastest way to iterate.

Right, iterate, what's an arrayList looklike?

@Test
public void arrayList() {
 ArrayList arrayList = new ArrayList();
 for (int i = 0; i < innerLoop; i++) {
  boolean inc = i % 2 == 0 ? true : false;
  arrayList.add(new Foo(inc));
 }
 long start = System.currentTimeMillis();
 for (int i = 0; i < outerLoop; i++) {
  for (Foo node: arrayList) {
   node.tick();
  }
 }
 long fin = System.currentTimeMillis();
 System.out.println("ArrayList took: " + (fin - start));
}

This runs in ~3000 ms

Okay the Java Iterators aren't cutting it, but what about a real linked list? not java.util.LinkedList but, Linked_list the data structure. This as far as I can figure should be ideal. All there is to iteration is pointer de-referencing. There are no lookups to find the a given index in an array, just following the pointers.

Here's my implementation:

public class LinkedListNode {
 public T data;
 public LinkedListNode next;

 public LinkedListNode(T data) {
  this.data = data;
 }
}

public class LinkedList {
 private LinkedListNode first;
 private LinkedListNode last;

 public LinkedList() {}

 public LinkedListNode first() {
  return first;
 }

 public void addToEnd(LinkedListNode linkedListNode) {
  if (first == null) {
   first = linkedListNode;
   last = linkedListNode;
  } else {
   last.next = linkedListNode;
   last = linkedListNode;
  }
 }
}

And the test:

@Test
public void linkedList() {
 LinkedList linkedList = new LinkedList();
 for (int i = 0; i < innerLoop; i++) {
  boolean inc = i % 2 == 0 ? true : false;
  linkedList.addToEnd(new LinkedListNode(new Foo(inc)));
 }

 long start = System.currentTimeMillis();
 LinkedListNode startNode = linkedList.first();
 for (int i = 0; i < outerLoop; i++) {
  LinkedListNode node = startNode;
  while (node.next != null) {
   node.data.tick();
   node = node.next;
  }
 }
 long fin = System.currentTimeMillis();
 System.out.println("linkedList took: " + (fin - start));
}

Any guesses to how this performed? try ~5500 ms Like I said Java broke computer science, they must be doing some amazing compiler optimizations, how is it that node.next is slower than foo[499]. It boggles the mind. What's the take away here? When you can use a primitive array do so?

oh, just a note, yes I know ArrayList is backed by a array, and yes I tested a java.util.LinkedList as well, and it was the worst of the bunch.

2 comments:

Brad said...

Could Java Object assignment actually be most costly than either address arithmetic (e.g., foos[j]) or Iterators? Cause that's the only major difference I can see:

node = node.next

But everything in Java is pass by value (with the object reference being a value).

Perhaps the garbage collector is getting in the way when the Object goes out of scope, but never does in the array, because the array is always in scope?

Maybe check the reference classes (e.g., WeakReference), perhaps those on the optimizations going on under the hood in ArrayList.

Piotr said...

First thing I'd consider is cache-friendliness. Here, arrays win because of two reasons.

First, because their elements are smaller (no need to store the "next" pointer), so more fits into the cache.

Second, because they occupy contiguous memory (so accessing one element automatically loads a few next elements into cache). List elements, in the worst case, might be distributed randomly across available memory (but probably aren't).

To test whether the cache affects anything, try measuring the time for different numbers of iterations in the inner loop. If you see that for some number of iterations the time suddenly jumps, this is probably the point where the cache gets too small for the data structure (array or list).

Apart from cache issues, note that computing the value of node.next requires memory (cache) access, whereas computing the value of an array index is register access.

Finally, I'm not sure about javac but I think for(...; i++) optimization into pointer arithmetic is common in compilers.

 
Web Statistics