Производительность языков программирования. Часть 2
Продолжаем сравнительные оценки производительности разноязыковых реализаций одной задачи на примере рекурсивного вычисления чисел Фибоначчи.
Go
Листинг 12. Реализация теста на языке Go (файл fido_go.go):
package main import ( "fmt"; "os"; "strconv" ) func fib ( n int ) int { if n < 2 { return 1 } else { return fib( n - 1 ) + fib( n - 2 ) } } func main(){ n, _ := strconv.Atoi( os.Args[ 1 ] ) fmt.Println( fib( n ) ) }
Сборка и выполнения этой программы (с демонстрацией используемой версии):
$ gccgo fibo_go.go -o -g -O3 fibo_go # time nice -19 ./fibo_go 30 1346269 real 0m0.031s user 0m0.026s sys 0m0.005s
И вот здесь маленький и не очень пока отточенный из-за своей новизны Go, показывает замечательный результат: всего в 2 раза медленнее GCC, и быстрее, чем Clang!
Ocaml
Листинг 13. Реализация теста на языке Ocaml (файл fido_ml.ml):
let rec fib n = if n < 2 then 1 else fib( n - 1 ) + fib( n - 2 );; let main () = let arg = int_of_string Sys.argv.( 1 ) in print_int( fib arg ); print_newline(); exit 0;; main ();;
Но этот код можно Ocaml можно выполнять двояким способом … посредством его интерпретации:
$ ocaml -version The Objective Caml toplevel, version 3.12.1 # time nice -19 ocaml fibo_ml.ml 30 1346269 real 0m0.126s user 0m0.118s sys 0m0.005s
Или откомпилировав его в машинный код:
$ ocamlc -o fibo_ml fibo_ml.ml # time nice -19 ./fibo_ml 30 1346269 real 0m0.107s user 0m0.106s sys 0m0.001s
Вообще то, по времени выполнения различия не столь существенные. Это наталкивает на мысль, что интерпретатор Ocaml работает с предкомпиляцией (JIT), а компилированная форма — это тот же байт-код с прикомпонованной к нему исполняющей системой.
Инструментарий Ocaml включает в себя интерпретатор (ocaml), компилятор в байт-код (ocamlc), исполняющую систему (ocamlrun) и ряд других компонент, в том числе оптимизирующий компилятор в машинный код (о котором авторами утверждается, что он превосходит по своим параметрам аналогичные компиляторы C/C++ для многих задач, особенно связанных с синтаксическим анализом и т. п.). Проверяем эти утверждения:
$ ocamlc -o fibo_ml fibo_ml.ml $ ocamlopt -o fiboo_ml fibo_ml.ml $ ls -l *_ml -rwxrwxr-x. 1 Olej Olej 13381 фев 23 21:09 fibo_ml -rwxrwxr-x. 1 Olej Olej 170691 фев 23 21:10 fiboo_ml $ time ./fibo_ml 30 1346269 real 0m0.106s user 0m0.105s sys 0m0.000s $ time ./fiboo_ml 30 1346269 real 0m0.023s user 0m0.021s sys 0m0.001s
Видно очень существенное различие результирующих (исполнимых) файлов по размеру, но они принципиально отличаются по формату содержимого:
$ file fibo_ml fibo_ml: data $ file fiboo_ml fiboo_ml: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=8c2cd3f1ccc9d49bd445a46ede4ebc0ac9bf48a5, not stripped
В итоге, мало того, что оптимизированная версия выполняется в 4.5 раза быстрее байт-кода, но её время выполнения всего на 80-90% медленнее, чем выполнение кода C, компилированного GCC с указанием максимальной степени оптимизации!
PureBasic
Ещё один вариант для любителей простых и лёгких решений — PureBasic (простые и лёгкие решения позже оказываются очень сложными при дальнейшем развитии проекта). PureBasic, вообще-то говоря, коммерческий продукт, но его демонстрационная версия, полностью работоспособная, позволяет создавать приложения не более 800 строк. Для небольших утилит и системных скриптов этого вполне достаточно, а для крупных проектов использовать что-либо из клонов Basic я бы просто не рискнул.
В контексте нашего рассмотрения PureBasic интересен только как ещё один сравнительный вариант реализаций с компиляцией в исполнимый машинный код, которых не так много.
Листинг 13. Реализация в системе PureBasic (файл fido.pb):
rocedure fib(n) If n<2 ProcedureReturn 1 Else ProcedureReturn fib(n-1) + fib(n-2) EndIf EndProcedure OpenConsole() PrintN(Str(fib(Val(ProgramParameter(0))))) CloseConsole()
И то, что из этого получилось в итоге:
# time nice -9 ./fibo_pb 30 1346269 real 0m0.036s user 0m0.033s sys 0m0.002s
Это в 3 раза медленнее кода, произведенного GCC, на уровне Go и кода, компилируемого Clang.
Scheme
Реализация той же функции на чисто функциональном языке Scheme (Scheme — один из двух наиболее популярных в наши дни диалектов языка Lisp).
Листинг 14. Реализация в системе Scheme (файл fibo.scm):
;; функция Фибоначчи — требует параллельной рекурсии (define (fib n) (cond ((= n 0) 1) ((= n 1) 1) (else (+ (fib (- n 1)) (fib (- n 2)))))) (begin (write ( fib (string->number (car (cdr (command-line)))))) (newline))
Выполнение:
# time nice -9 guile -q fibo.scm 30 1346269 real 0m0.520s user 0m0.505s sys 0m0.008s
Haskell
Ещё один стандартизованный чистый функциональный язык программирования общего назначения Haskell.
Листинг 15. Реализация той же функции на Haskell (файл fibo.hs):
module Main where import System.Environment main :: IO () -- определение функции fibo :: Integer -> Integer fibo n | n == 0 = 1 | n == 1 = 1 {- вложенный комментарий -} | otherwise = fibo( n - 1 ) + fibo( n - 2 ) -- сама программа main = do args <- getArgs print( fibo( read( args !! 0 ) :: Integer ) )
Компиляция:
$ ghc -o fibo_hs fibo.hs [1 of 1] Compiling Main ( fibo.hs, fibo.o ) Linking fibo_hs ...
Выполнение:
# time nice -9 ./fibo_hs 30 1346269 real 0m0.391s user 0m0.385s sys 0m0.004s
Scala
Scala — относительно новый (2003г.) язык, сочетающий в себе возможности функционального и объектно-ориентированного программирования. Многие считают Scala дальнейшим расширением языковой линии Java и даже называют его как Java++.
Листинг 16. Реализация программы вычисления чисел Фибоначчи на языке Scala (файл fibo.scala):
object fibo_scala { def fib( n: Int ): Long = if( n < 2 ) 1 else fib( n - 1 ) + fib( n - 2 ) def main( args: Array[ String ] ): Unit = { System.out.println( fib( args( 0 ).toInt ) ) } }
Компиляция такой программы в файл (компиляция происходит весьма продолжительное время):
$ scalac fibo.scala $ ls -l *.class -rw-rw-r--. 1 Olej Olej 682 фев 21 15:49 fibo_scala.class -rw-rw-r--. 1 Olej Olej 981 фев 21 15:49 fibo_scala$.class
Хронометраж выполнения этого варианта программы, в сравнении с эталонной реализацией GCC C, и родственной Scala реализацией на Java:
# time nice -19 ./fibo_c 30 1346269 real 0m0.011s user 0m0.009s sys 0m0.000s # time nice -19 java fibo 30 1346269 real 0m0.195s user 0m0.147s sys 0m0.042s # time nice -19 scala fibo_scala 30 1346269 real 0m0.740s user 0m0.634s sys 0m0.115s
Обсуждение
Прежде всего, отмечаем, что эффективность исполняемого кода зависит не только от языка, на котором код написан, и от технологии используемой в языке (компиляция, интерпретация, компиляция в промежуточный байт-код), но и от конкретного компилятора и заказанных уровней его оптимизации. Разница в зависимости от этих факторов может составлять до уровня 3-х раз.
Разница же в скорости выполнения эквивалентных приложений, реализованных на разных языках, может легко достигать 3-х порядков, нескольких сот раз (между компилирующими и интерпретирующими реализациями).
С другой стороны, понятно, что интерпретируемые языки более гибкие в смысле динамической типизации и особенно в операциях с функциями высших порядков (элементы функционального программирования).
Хронометрировать скорость выполнения — неблагодарное занятие, а полученные результаты будут весьма и весьма относительными. Чтобы не сравнивать численные значения, разложим полученные значения для разных языков по логарифмическим кластерам, по фактору скорости относительно самого быстрого GCC C варианта:
Фактор скорости | Язык |
---|---|
1 ... 2 | C, GCC C++, Go, Ocaml |
2 ... 5 | Cobj C++, PureBasic |
5 ... 10 | Ocaml |
10 ... 20 | Java |
20 ... 50 | Lua, Scheme, Haskell |
50 ... 100 | Python 2, Ruby, JavaScript, Scala |
100 ... 200 | Perl, Python 3, PHP |
200 ... 500 | bash |
Заключение
Рассмотрены реализации одной и той же задачи для сравнения скорости её выполнения в совершенно различных языках программирования.
Из всех охваченных в сравнении языков только очень немногие (C, C++, Go, PureBasic) используют технику «нативной» компиляции в машинный код используемой платформы. Все же остальные, в той или иной мере и технике, используют виртуальную исполняющую машину (среду выполнения). Это, очевидно, становится тенденцией последнего десятилетия. (Все ранние проекты языков программирования – Algol, FORTRAN, COBOL, Pascal — предполагали компиляцию именно в исполнимый машинный код).
Такая тенденция связана, в первую очередь, с лавинным ростом скорости современных процессоров: скорости, которую предлагают производители процессоров, но которую не могут потребить и которая не нужна львиной доле потребителей компьютерного оборудования. Интерпретирующая реализация приложений канализирует избыточную производительность.
Но есть ещё одна объяснимая причина пристрастия к интерпретирующей реализации: она позволяет локализовать ошибки выполняемого программного кода (ошибки программиста) внутри исполняющей системы (виртуальной машины), не позволить им распространиться на операционную систему. Это можно принять как актуальный способ для ... «настольных» приложений: учебных, математических вычислений, развлекательных, бытовых... К сожалению, такой способ неприемлем для «промышленных» приложений: управляющих систем, электронных платежей, систем связи и телекоммуникаций — падение приложения там эквивалентно аварийной ситуации даже если это приложение будет повторно поднято.
Ресурсы для скачивания
- этот контент в PDF
- Архив программных кодов (speed.tgz | 6.83KB)