Saturday, June 28, 2014

Benchmarking Apples and Apples


I left off yesterday with some initial benchmarks for Design Patterns in Dart. The benchmark_harness makes that pretty darn easy. What I found was that one of my Factory Method pattern implementation was not the same as the other.

Speed is not the only concern when picking an implementation, but it is certainly good to know. For the Factory Method, the fastest that I found was the classic subclass approach:
abstract class Creator {
  Product productMaker();
}

abstract class Product {}

class ConcreteCreator extends Creator {
  productMaker()=> new ConcreteProduct();
}

class ConcreteProduct extends Product {}
The abstract creator describes that subclasses will be responsible for choosing the class to instantiate. In this case it is a new ConcreteProduct, mirroring the role in the pattern. When I use the pattern in Dart Comics, the collection chooses to create a concrete ComicBook. The point being that the subclass can choose whatever it likes. And it is fast.

The alternate approach uses a Map of factories, enabling the top-level creator to sport a default implementation:
typedef Product productFactory();
Map<Type, productFactory> Factory = {
  ConcreteCreator: (){ return new ConcreteProduct(); }
};

abstract class Creator {
  Product productMaker()=> Factory[this.runtimeType]();
}

abstract class Product {}

class ConcreteCreator extends Creator {}
class ConcreteProduct extends Product {}
Yesterday, I found that subclass approach was roughly 600% faster than the map-of-factories implementation. It is actually even better than that. After updating to Dart 1.5, the results are now:
$ dart tool/benchmark.dart
Factory Method — Subclass(RunTime): 0.08871921437716161 us.
Factory Method — Map of Factories(RunTime): 1.7642425092468361 us.
1800% faster. That's craziness. But...

It is not quite a fair comparison. The productMaker() from the subclass version immediately creates the concrete instance. Calling the productMaker() method in the map-of-factories version does not immediately instantiate the concrete instance. Instead, it calls a closure that creates and returns the object. Perhaps this indirection makes a difference?

To find out, I replace the productMaker() method in the map-of-factories approach with a getter that returns the factory method:
typedef Product productFactory();
Map<Type, productFactory> Factory = {
  ConcreteCreator: (){ return new ConcreteProduct(); }
};

abstract class Creator {
  productFactory get productMaker=> Factory[this.runtimeType];
}
// ...
The dynamic language dork in me loves this kind of thing, but it is probably a little hard to read. The productMaker getter returns the Map lookup in Factory. The value returned from the Factory lookup, and hence from the productMaker getter, is a closure—an anonymous function. This return value can be invoked with the call operator—open and closing parenthesis. So it looks just like a method call:
new MapOfFactories.ConcreteCreator().productMaker();
But it is not a method—I am immediately invoking the result of the Map lookup.

Cool, eh? Well, I like it. Anyway, the result of all that? No change whatsoever:
$ dart tool/benchmark.dart
Factory Method — Subclass(RunTime): 0.08664997297170718 us.
Factory Method — Map of Factories(RunTime): 1.7268800975341878 us.
Well, maybe a slight improvement, but it is unclear if the improvement is statistically significant.

So, unless I am missing some obvious way to improve things, my map-of-factories approach is 20 times slower than the subclass approach. There are times in which this will not matter (e.g. when I am not creating a large quantity of concrete products), but this certainly needs to factor into choosing an implementation.


Day #106

No comments:

Post a Comment