Learning Scala: Performance Impact of Implicit Conversions

Scala’s implicit conversions are a great tool for enhancing classes coming from libraries that you can’t just change yourself. I went into details on how it works a few weeks ago. With implicit conversions it can be made to appear that methods you have written exist as part of some objects that actually remain unchanged. You usually do this by creating (1) a wrapper class that takes the original object as it’s single parameter and (2) an implicit function that converts the original to the wrapped type by instantiating and returning the wrapper. If such an implicit is in a given scope, then in that scope the wrapper type’s methods can be called as if they were part of the wrapped class. For example, see the code below (note: the syntax highlighting is not perfect because it’s using Java rules — if anyone knows an easy way to add Scala highlighting support to a wordpress.com blog, please let me know in the comments):

case class ImmutableRichLong(x: Long) {
  // returns the predecessor of the wrapped object
  def pred = x - 1
}

implicit def long2immutableRichLong(x: Long) = ImmutableRichLong(x)

20L.pred // returns 19

Other languages have ways of doing similar things. JavaScript and Ruby actually let you add methods to existing objects, and these methods will then work everywhere else where those objects are used, which is not always good. A badly behaving piece of code could easily screw things up for everyone that way. Extension methods in C# are more similar to Scala’s implicit conversion, but they don’t require actual creation of new objects — they are just a mechanism for invoking static methods as if they were members of their first parameter. I’m not sure what to think about that. I guess it might be preferable to just calling the method normally, in some DSL scenario.

But what I really wanted to talk about is that there is a potential performance impact on using Scala’s method of extending classes that way. When using the implicit conversions, you must remember that a new object is created for every such “extension method” call. If you use them sparingly, it probably won’t matter, but it could have a serious performance impact in some situations, although I guess these situations are rare enough.Below is some code that goes through a loop where this impact becomes apparent, but we must keep in mind that production code rarely has that kind of loops and the performance comparison doesn’t tell us anything about how an actual application would be affected by using a lot of implicit conversions.

We’ll run four different loops using different kinds of “extension methods” and time them. First we need some helper code to measure execution time — I think the best way to do it is to create a singleton function that can time a piece of code we pass to it. I will go into more detail about the design of this object in a later blog entry, just take it as it is for now.

object Time {
  def apply[T](name: String)(block: => T) {
    val start = System.currentTimeMillis
    try {
        block
    } finally {
        val diff = System.currentTimeMillis - start
        println("# Block \"" + name +"\" completed, time taken: " + diff + " ms (" + diff / 1000.0 + " s)")
    }
  }
}

Now we create four slightly different classes and objects that all have the same purpose: define a utility method that can decrease a Long by one. Why they are defined as they are will be come more clear in the last piece of code.

case class ImmutableRichLong(x: Long) {
  def pred = x - 1
}

case class MutableRichLong(var x: Long) {
  def dec = x = x - 1
}

object LongExtension {
  def pred(x: Long) = x - 1
}

object LongImplicitExtension {
  def pred(implicit x: Long) = x - 1
}

And finally we run and time four loops that do the exact same thing: decrease a Long by 1, starting from 1 billion and going to 0, but they do it slightly differently, using the four different types defined above.

object ConversionPerfImpact {
  def main(args : Array[String]) : Unit = {
    val n = 1000000000L

    Time("implicit conversion") {
      // this is the usual implicit conversion in Scala
      // the ImmutableRichLong takes Long as a parameter
      // and adds a method 'pred' that returns Long as well
      implicit def convert(x: Long) = ImmutableRichLong(x)
      var x = n
      while (x > 0) {
        x = x.pred
      }
    }
    Time("wrapper object") {
      // this time we just wrap Long in a mutable object
      // and use that object everywhere instead of long
      var x = MutableRichLong(n)
      while (x.x > 0) {
        x.dec
      }
    }
    Time("singleton helper") {
      // Here we have defined a singleton helper class
      // which is analogous to defining static helper functions
      // and we use these methods from the singleton explicitly
      var x = n
      while (x > 0) {
        x = LongExtension.pred(x)
      }
    }
    Time("singleton helper and implicit variable") {
      // This is analogous to the previous, but we define
      // an implicit conversion from Long to the singleton
      // this will make the singleton's methods appear as Long's methods
      // but the singleton's methods also define the argument list
      // to be implicit, and by defining the var x to be implicit
      // it will be passed to these methods implicitly
      implicit def convert(x: Long) = LongImplicitExtension
      implicit var x = n
      while (x > 0) {
        x = x.pred
      }
    }
  }
}

The results on my computer (Dual Core AMD Opteron 185) are something like this:
# Block "implicit conversion" completed, time taken: 9406 ms (9.406 s)
# Block "wrapper object" completed, time taken: 3125 ms (3.125 s)
# Block "singleton helper" completed, time taken: 1969 ms (1.969 s)
# Block "singleton helper and implicit variable" completed, time taken: 1968 ms (1.968 s)

They vary a bit on each execution, but the relationships stay the same:
1) the loop that creates a billion objects by implicit conversion is obviously the slowest, here the loop takes around 9 seconds, 3 times as long as using a mutable wrapper object
2) I was surprised that the singleton helper methods (which compile into static methods) were that much faster than the mutable wrapper, taking only about 2/3 of the time. I’m not sure about the exact reasons for this, but at the moment I’m not curious enough to investigate myself.

Despite these differences, my recommendation for adding functionality to existing classes would still probably be to use the implicit conversions in situations where there aren’t an insane amount of objects being created. Creating objects that are immediately thrown away on the JVM is not that expensive. But if you really do need to improve performance where a lot of “extension methods” are being called, then I would use helper methods on a singleton object.

Note that the last method of defining an implicit conversion to a singleton is not something I would actually recommend using. I think it only adds confusion and doesn’t even work if you have multiple variables of type Long in scope and you want to “extend” more than one of them. It’s better to actually use the explicit syntax of LongExtension.pred(x) instead, or import the members of the singleton (import LongExtension._), which is analogous to static imports in Java. That way you can just use x = pred(x) and IMHO this is no worse that x = x.pred.

So I’d conclude that there is probably no notable performance impact for using implicit conversions sparingly in most applications. It is likely that an implicit conversion + a method call on the result may be somewhere around 5 times slower than simply calling a static method, but unless a lot of your method calls require implicit conversions, I’d say there’s not much need to worry.

Advertisements

2 thoughts on “Learning Scala: Performance Impact of Implicit Conversions

  1. Pingback: Scala: for a/vs while « Villane

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s