2015-11-20 4 views
3

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

У меня есть строка с миллионом символов, и мне нужно суммировать каждую цифру. Типичный функциональный подход будет выглядеть так:

val sum = value.map(_.asDigit).sum.toString 

Однако, этот красивый, лаконичный, функциональный подход занимает 0.98s (почти второй)

var sum = 0; 

for(digit <- value) 
    sum += digit.asDigit 

Этот императив подход с другой стороны принимает только 0,022 s (2.24% из вышеприведенного времени) - это примерно в 50 раз быстрее ...

Я уверен, проблема возникает из-за того, что Scala генерирует новый список в первом подходе, а затем снова итерации по этому списку, чтобы создать сумма.

Это просто плохая идея полагаться на функциональные конструкции? Я имею в виду, они прекрасны - я их люблю, но они в 50 раз медленнее ...

P.S. Я тоже попробовал что-то еще.

val sum = value.foldLeft(0)((sum, value) => sum + value.asDigit) 

Этот функциональный подход, который является чуть менее кратким и, возможно, даже труднее, чем читать императивный подход, берет 0.085s. Труднее читать И еще в 4 раза медленнее ...

+1

Ваше использование 'map' создает промежуточную строку, которая вам не нужна. Вы можете избежать этого, используя 'value.view.map (_. AsDigit) .sum'. – Lee

ответ

14

Прежде всего: вы уверены, что вы правильно сравнили две версии? Простое измерение времени выполнения с использованием чего-то типа System.nanoTime будет не дает точные результаты. См. Это смешное и проницательное blog post от JVM гуру производительности Aleksey Shipilёv.

Вот тест, используя отличную Thyme SCALA библиотеку бенчмаркинга:

val value = "1234567890" * 100000 
def sumf = value.map(_.asDigit).sum 
def sumi = { var sum = 0; for(digit <- value) sum += digit.asDigit; sum } 

val th = ichi.bench.Thyme.warmed(verbose = println) 
scala> th.pbenchOffWarm("Functional vs. Imperative")(th.Warm(sumf))(th.Warm(sumi)) 
Benchmark comparison (in 6.654 s): Functional vs. Imperative 
Significantly different (p ~= 0) 
    Time ratio: 0.36877 95% CI 0.36625 - 0.37129 (n=20) 
    First  40.25 ms 95% CI 40.15 ms - 40.34 ms 
    Second 14.84 ms 95% CI 14.75 ms - 14.94 ms 
res3: Int = 4500000 

Так что да, императив версия является быстрее. Но не так сильно, как вы измеряли. Во многих, многих ситуациях разница в производительности будет совершенно неактуальной. И для тех немногих ситуаций, в которых разница в производительности имеет значение, scala дает вам возможность написать императивный код. В общем, я думаю, что scala работает очень хорошо.

Кстати: ваш второй подход почти так же быстро, как императивное версии при правильном протестированный:

def sumf2 = value.foldLeft(0)(_ + _.asDigit) 

scala> th.pbenchOffWarm("Functional2 vs. Imperative")(th.Warm(sumf2))(th.Warm(sumi)) 
Benchmark comparison (in 3.886 s): Functional2 vs. Imperative 
Significantly different (p ~= 0) 
    Time ratio: 0.89560 95% CI 0.88823 - 0.90297 (n=20) 
    First  16.95 ms 95% CI 16.85 ms - 17.04 ms 
    Second 15.18 ms 95% CI 15.08 ms - 15.27 ms 
res17: Int = 4500000 

Обновления в связи с предложением от @Odomontois: Заметьте, что если вы действительно хотите оптимизировать это, вы должны убедиться, что символы строки не помещаются в коробку. Вот императивная версия, на которую не очень приятно смотреть, но также почти как можно быстрее. Это использует макрос cfor от spire, но цикл while будет работать так же хорошо.

def sumi3 = { 
    var sum = 0 
    cfor(0)(_ < value.length, _ + 1) { i => 
    sum += value(i).asDigit 
    } 
    sum 
} 

scala> th.pbenchOffWarm("Imperative vs. optimized Imperative")(th.Warm(sumi))(th.Warm(sumi3)) 
Benchmark comparison (in 4.401 s): Imperative vs. optimized Imperative 
Significantly different (p ~= 0) 
    Time ratio: 0.08925 95% CI 0.08880 - 0.08970 (n=20) 
    First  15.10 ms 95% CI 15.04 ms - 15.16 ms 
    Second 1.348 ms 95% CI 1.344 ms - 1.351 ms 
res9: Int = 4500000 

Преждевременная оптимизация отречение:

Если вы не абсолютно уверены в том, что а) кусок кода является узким местом производительности и б) императив версия гораздо быстрее, я всегда предпочитаю наиболее читаемый версию поверх быстрейший. Scala 2.12 будет поставляться с new optimizer, что значительно уменьшит накладные расходы функционального стиля, поскольку во многих случаях он может выполнять расширенные оптимизации, такие как закрытие вставки.

+0

Ваша «императивная» версия не очень важна. Сталь использует итераторы scala. Для альтернативы исполнителя см. [Spire 'cfor'] (https://github.com/non/spire#syntax) см. Также [это сообщение в блоге] (https://www.chrisstucchio.com/blog/2014/learning_spire_cfor.html) – Odomontois

+1

Это не * моя * императивная версия, но одна из OP. Вот тот, который действительно кричит, но уродлив, как ночь: 'def sumi2 = {var i = 0; var sum = 0; while (i <значение.length) {sum + = значение (i) .asDigit; i + = 1}; sum} ' –

+0

Да, извините за ошибочное обвинение. Но то, что я сказал: «Было бы здорово, если бы вы также включили версию' cfor' в свой тест ». Потому что расширение макросов 'cfor' - это в основном то, что вы только что написали в комментарии, но менее уродливое. – Odomontois

Смежные вопросы