Содержание


Производительность языков программирования. Часть 2

Comments

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

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 ... 2C, GCC C++, Go, Ocaml
2 ... 5Cobj C++, PureBasic
5 ... 10Ocaml
10 ... 20Java
20 ... 50Lua, Scheme, Haskell
50 ... 100Python 2, Ruby, JavaScript, Scala
100 ... 200Perl, Python 3, PHP
200 ... 500bash

Заключение

Рассмотрены реализации одной и той же задачи для сравнения скорости её выполнения в совершенно различных языках программирования.

Из всех охваченных в сравнении языков только очень немногие (C, C++, Go, PureBasic) используют технику «нативной» компиляции в машинный код используемой платформы. Все же остальные, в той или иной мере и технике, используют виртуальную исполняющую машину (среду выполнения). Это, очевидно, становится тенденцией последнего десятилетия. (Все ранние проекты языков программирования – Algol, FORTRAN, COBOL, Pascal — предполагали компиляцию именно в исполнимый машинный код).

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

Но есть ещё одна объяснимая причина пристрастия к интерпретирующей реализации: она позволяет локализовать ошибки выполняемого программного кода (ошибки программиста) внутри исполняющей системы (виртуальной машины), не позволить им распространиться на операционную систему. Это можно принять как актуальный способ для ... «настольных» приложений: учебных, математических вычислений, развлекательных, бытовых... К сожалению, такой способ неприемлем для «промышленных» приложений: управляющих систем, электронных платежей, систем связи и телекоммуникаций — падение приложения там эквивалентно аварийной ситуации даже если это приложение будет повторно поднято.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=974653
ArticleTitle=Производительность языков программирования. Часть 2
publish-date=06172014