Подтвердить что ты не робот

Каково влияние Scala неявных типов преобразований?

В Scala существует ли значительное влияние процессора или памяти на использование неявных преобразований типов для расширения функциональности класса по сравнению с другими возможными вариантами реализации?

Например, рассмотрим глупую функцию манипуляции строками. Эта реализация использует конкатенацию строк:

object Funky {
  def main(args: Array[String]) {
    args foreach(arg => println("Funky " + arg))
  }
}

Эта реализация скрывает конкатенацию за метод-член, используя неявное преобразование типа:

class FunkyString(str: String) {
  def funkify() = "Funky " + str
}

object ImplicitFunky {
  implicit def asFunkyString(str: String) = new FunkyString(str)

  def main(args: Array[String]) {
    args foreach(arg => println(arg.funkify()))
  }
}

Оба делают то же самое:

scala> Funky.main(Array("Cold Medina", "Town", "Drummer"))        
Funky Cold Medina
Funky Town
Funky Drummer

scala> ImplicitFunky.main(Array("Cold Medina", "Town", "Drummer"))
Funky Cold Medina
Funky Town
Funky Drummer

Есть ли разница в производительности? Несколько конкретных соображений:

Включает ли Scala неявные вызовы метода asFunkyString?

Создает ли Scala новый объект FunkyString для каждого аргумента или может оптимизировать выделение дополнительных объектов?

Предположим, что у FunkyString было 3 разных метода (funkify1, funkify2 и funkify3), а тело foreach вызывало каждый из них подряд:

println(arg.funkify1())
println(arg.funkify2())
println(arg.funkify3())

Будет ли Scala повторять преобразование 3 раза или будет оптимизировать избыточные преобразования и просто сделать это один раз для каждой итерации цикла?

Предположим, что я явно фиксирую преобразование в другой переменной, например:

val fs = asFunkyString(arg)
println(fs.funkify1())
println(fs.funkify2())
println(fs.funkify3())

Изменит ли это ситуацию?

В практическом плане широкое использование неявных преобразований является потенциальной проблемой производительности или обычно безвредно?

4b9b3361

Ответ 1

Я попытался настроить микрозапуск, используя отличный Scala -Benchmark-Template.

Очень сложно написать значимый (не оптимизированный JIT) тест, который проверяет только неявные преобразования, поэтому мне пришлось добавить немного накладных расходов.

Вот код:

class FunkyBench extends SimpleScalaBenchmark {
  val N = 10000
  def timeDirect( reps: Int ) = repeat(reps) {
    var strs = List[String]()
    var s = "a"
    for( i <- 0 until N ) {
      s += "a"
      strs ::= "Funky " + s 
    }
    strs
  }
  def timeImplicit( reps: Int ) = repeat(reps) {
    import Funky._
    var strs = List[String]()
    var s = "a"
    for( i <- 0 until N ) {
      s += "a"
      strs ::= s.funkify
    }
    strs
  }
}

И вот результаты:

[info] benchmark  ms linear runtime
[info]    Direct 308 =============================
[info]  Implicit 309 ==============================

Мой вывод: в любом нетривиальном фрагменте кода, влияние неявных преобразований (создание объекта) не поддается измерению.

EDIT: Я использовал scala 2.9.0 и java 1.6.0_24 (в режиме сервера)

Ответ 2

JVM может оптимизировать дополнительные распределения объектов, если обнаруживает, что это было бы достойно.

Это важно, потому что, если вы просто встраиваете то, что в итоге вы получаете более крупные методы, которые могут вызвать проблемы с кешированием или даже уменьшить вероятность использования JVM для других оптимизаций.