Wednesday, December 21, 2011

Groovy, Scala, Java, Xtend - an UPDATED stupid comparison

This is an update to yesterday's post, based on feedback I got so far:

Measurement

Henning suggested that the number of iterations is too small:

10000 iterations are probably not enough for a comparison: If I go from 10k to 100k iterations, the Scala version needs 5x time -- the Java version only 2x.

Indeed. Here's what I got when using 1M iterations:

  • Java (the pretty version):
    Took : 245 ms, number of elements : 3000000
  • Xtend :
    Took : 278 ms, number of elements : 3000000
  • Xtend (using switch - see below) :
    Took : 298 ms, number of elements : 3000000
  • Scala (using for comprehensions) :
    Took : 5140 ms, number of elements : 3000000
  • Scala (using while loops) :
    Took : 931 ms, number of elements : 3000000
  • Scala (using while loops and java.util.collections - see below) :
    Took : 315 ms, number of elements : 3000000
  • Groovy (using << operator - see below):
    Took : 1683 ms, number of elements : 3000000

Groovy

Thorsten and Jochen pointed out that I used the wrong operator for adding new elements to a list. I should use '<<' instead of '+=' since the latter would create a new list for the single elements before adding them. I've fixed this and the Groovy program is much faster now.

Jochen also pointed out that the comparison is unfair since, because of the dynamic dispatch Groovy does heavy caching assuming that a method is invoked for the same type of argument most of the time.

My initial idea was to do a comparison where Groovy has a strength (i.e. dynamic dispatch). After all the other examples also do dispatching at runtime. The Java and Scala version don't call another method, but since Xtend does so and is as fast as Java I'd say this extra call doesn't change much.

Jochen also states:

Imagine a class with those three foo methods written in Java. Then in Groovy you can still run your test. I dare to say that in Xtend you cannot make this work without a dispatcher method somewhere - and your dispatch keyword is implying that - thus I assume you cannot make this then work using a Java class.


I think this is actually a big problem with Groovy. Although it's syntax might look a bit like Java it behaves totally different.
In contrast Xtend binds statically just like Java and in fact a multi method in Xtend behaves always the same no matter if you call it from Java or Xtend.

He further says

But let us not stop here... let us assume that you write the base class in Java, where it knows foo(String) foo(Object) and foo(Boolean). Using this class the Integer variant would call the foo(Object) method in your code. Now extend that class in Java with a foo(Integer) method and Groovy would no longer call foo(Object) but foo(Integer).
I doubt you can easily simulate that case.


I can indeed not do that in Java. But I can do that with Xtend and it still behaves as expected no matter form what language you'd call that code.

Scala

Jan said:

I rewrote your little program to use while loops and the java utils collections and the scala version was as fast as java - but nearly as ugly to :-D

Here's the updated Scala version with while and java.util.collections, which is now almost as fast as the Java version:
object Scala extends Application {
  val absoluteResult = new ArrayList[Any]()
  val before = System.currentTimeMillis()
  var i =0
  while (i<1000000) {
    i = i+1
    val result = List("foo", 23, true).iterator
    while (result.hasNext) {
      absoluteResult.add(foo(result.next))
    }
  }
  println("Took : "+(System.currentTimeMillis() - before)
      +" ms, number of elements : "+absoluteResult.size)
  
  def foo(obj : Any) =
    obj match {
          case _:String => "String"
          case _:Boolean => "Boolean"
          case _:Integer => "Integer"
          case _ => throw new IllegalArgumentException()
    }
}

Clojure

Stefan did a nice little example in Clojure. It doesn't use any mutable collections but uses a pure functional style:

(defn foo [x]
(cond
(number? x) "Number"
(string? x) "String"
(= (class x) Boolean) "Boolean"))

(time (println (count 
(map foo 
(apply concat 
(repeat 10000 ["foo" 23 true]))))))
On his machine the Java example takes '31ms' and the Clojure program prints

Prints:
30000
"Elapsed time: 194.046477 msecs"

Xtend

An alternative way to do the dispatch in Xtend is using type guards in switch,
which doesn't make a huge difference performance-wise :

class XtendWithSwitch {
  def static void main(String[] args) {
    val absoluteResult = newArrayList()
    val before = System::currentTimeMillis()
    for (times : 0..1000000) {
      val result = newArrayList('foo', 23, true)
      for (y : result) {
        absoluteResult += foo(y)
      }
    }
    println("Took : "+(System::currentTimeMillis() as int - before)
      +" ms, number of elements : "+absoluteResult.size)
  }

  def static foo(Comparable comp) {
    switch(comp) {
      String : 'String'
      Boolean : 'Boolean'
      Integer : 'Integer'
      default : throw new IllegalArgumentException()
    }  
  }
}

Java


Finally based on Henning's suggestion I beautified the Java version a bit using Guava and static imports:

public static void main(String[] args) {
  List<Object> absoluteResult = newArrayList();
  long before = System.currentTimeMillis();
  for (int i=0; i < 1000000; i++) {
    for (Object y : newArrayList("foo", 23, true)) {
      absoluteResult.add(foo(y));
    }
  }
  System.out.println("Took : "+(System.currentTimeMillis() - before)
    +" ms, number of elements : "+absoluteResult.size());
}

static String foo(Object s) {
  if (s instanceof String) {
    return "String";
  } else if (s instanceof Boolean) {
    return "Boolean";
  } else if (s instanceof Integer) {
    return "Integer";
  } else {
    throw new IllegalArgumentException();
  }
}