Functions don't perform like first-class citizens


#1

Syntactically functions are first-class citizens in Kotlin. Performancewise, however, they aren't. Is this a bug?

fun dummy(x: Long) : Long{   return x+1 }

fun call(fn: (Long)->Long, x: Long) : Long {
  return fn(x)
}

fun callDummy(x : Long) {
  dummy(x)
}

fun tic(message: String) : ()-> Unit {
  val time = System.currentTimeMillis()
  println(message)
  return {
  println("${(System.currentTimeMillis() - time)/1000f} seconds")
  }
}

fun main (args : Array<String>) {
  val n = 10000000000
  val toc1 = tic(“callDummy(x)   $n times”)
  for (x in 1…n)
  callDummy(x)
  toc1()
  val toc2 = tic(“call(::dummy, x) $n times”)
  for (x in 1…n)
  call(::dummy, x)
  toc2()
  val toc3 = tic("(::dummy)(x)   $n times")
  for (x in 1…n)
  (::dummy)(x)
  toc3()
  val toc4 = tic(“dummy(x)   $n times”)
  for (x in 1…n)
  dummy(x)
  toc4()

}

/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/bin/java -Didea.launcher.port=7534 “-Didea.launcher.bin.path=/Applications/IntelliJ IDEA 13 CE.app/bin” -Dfile.encoding=UTF-8 -classpath “/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/deploy.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/dt.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/javaws.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/jce.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/jconsole.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/management-agent.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/plugin.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/sa-jdi.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Classes/charsets.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Classes/classes.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Classes/jsse.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Classes/ui.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/ext/apple_provider.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/ext/dnsns.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/ext/localedata.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/ext/sunjce_provider.jar:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home/lib/ext/sunpkcs11.jar:/Users/kburjorjee/Nd/out/production/Nd:/Users/kburjorjee/Library/Application Support/IdeaIC13/Kotlin/kotlinc/lib/kotlin-runtime.jar:/Applications/IntelliJ IDEA 13 CE.app/lib/idea_rt.jar” com.intellij.rt.execution.application.AppMain _DefaultPackage
callDummy(x)   10000000000 times
2.844 seconds
call(::dummy, x) 10000000000 times
54.654 seconds
(::dummy)(x)   10000000000 times
55.41 seconds
dummy(x)   10000000000 times
2.862 seconds


#2

Well, first of all, microbenchmarks are very often wrong (see below).

To answer your question: no, this is not a bug. It’s reality: when used as objects, functions are slower, especially when you pass values of Java primitive types to them (Long is one), primarily because of boxing being necessary on the JVM. Look at it this way: direct function calls are very highly optimized by the compiler and/or runtime, so they work a lot faster than normal calls (when functions are ordinary obejcts) :slight_smile:

Here’s a slight modification of your benchmark that uses strings instead of longs (I had to reduce the number of iterations, because it’s too slow otherwise), it eliminates boxing, which makes all calls perform the same way:

 
fun dummy(x: String) : String{
    return x+1
}

fun call(fn: (String)->String, x: String) : String {
  return fn(x)
}

fun callDummy(x : String) {
  dummy(x)
}

fun tic(message: String) : ()-> Unit {
  val time = System.currentTimeMillis()
  println(message)
  return {
  println("${(System.currentTimeMillis() - time)/1000f} seconds")
  }
}

fun main (args : Array<String>) {
  val n = 10000000L
  val toc1 = tic(“callDummy(x)   </span>n<span> times"</span>) &nbsp;&nbsp;<span>for </span>(x <span>in </span><span>1</span>..n) &nbsp;&nbsp;<span>callDummy</span>(<span>"" </span>+ x) &nbsp;&nbsp;toc1() &nbsp;&nbsp;<span>val </span>toc2 = <span>tic</span>(<span>"call(::dummy, x) </span><span>n times”)
  for (x in 1…n)
  call(::dummy, “” + x)
  toc2()
  val toc3 = tic("(::dummy)(x)   </span>n<span> times"</span>) &nbsp;&nbsp;<span>for </span>(x <span>in </span><span>1</span>..n) &nbsp;&nbsp;(::<span>dummy</span>)(<span>"" </span>+ x) &nbsp;&nbsp;toc3() &nbsp;&nbsp;<span>val </span>toc4 = <span>tic</span>(<span>"dummy(x) &nbsp;&nbsp;</span><span>n times")
  for (x in 1…n)
  dummy("" + x)
  toc4()

}

Results:

callDummy(x)   10000000 times 0.795 seconds call(::dummy, x) 10000000 times 0.843 seconds (::dummy)(x)   10000000 times 0.832 seconds dummy(x)   10000000 times 0.824 seconds

And, for the "See below": Microbenchmarks are very-very often wrong: here's a benchmark that runs the same loop four times:

 
fun dummy(x: String) : String{
    return x
}

fun tic(message: String) : ()-> Unit {
  val time = System.currentTimeMillis()
  println(message)
  return {
  println("${(System.currentTimeMillis() - time)/1000f} seconds")
  }
}

fun main (args : Array<String>) {
  val n = 1000000000L
  val toc1 = tic(“callDummy(x)   </span>n<span> times"</span>) &nbsp;&nbsp;<span>for </span>(x <span>in </span><span>1</span>..n) &nbsp;&nbsp;<span>dummy</span>(<span>""</span>) &nbsp;&nbsp;toc1() &nbsp;&nbsp;<span>val </span>toc2 = <span>tic</span>(<span>"call(::dummy, x) </span><span>n times”)
  for (x in 1…n)
  dummy("")
  toc2()
  val toc3 = tic("(::dummy)(x)   </span>n<span> times"</span>) &nbsp;&nbsp;<span>for </span>(x <span>in </span><span>1</span>..n) &nbsp;&nbsp;<span>dummy</span>(<span>""</span>) &nbsp;&nbsp;toc3() &nbsp;&nbsp;<span>val </span>toc4 = <span>tic</span>(<span>"dummy(x) &nbsp;&nbsp;</span><span>n times")
  for (x in 1…n)
  dummy("")
  toc4()
}

Results:

callDummy(x)   1000000000 times 0.304 seconds call(::dummy, x) 1000000000 times 2.031 seconds (::dummy)(x)   1000000000 times 2.056 seconds dummy(x)   1000000000 times 2.076 seconds

Yes, running THE SAME LOOP gives drastically different results the first time you do it, and the consequent times. And this is not just Kotlin, Java works the same way. Beware of microbenchmarks!


#3

Ah, so the culprit is the auto-boxing/unboxing. Good to know. And thanks for the lesson on microbenchmarks :-)