Ruby / MRI és Rubinius sebesség

 ( log69 | 2017. augusztus 19., szombat - 14:00 )

Teszteltem a kommentek és üres sorok nélküli 17 ezer soros eléggé komplex kódomat az alábbi környezetben, mely egyébként 60-80 SQL hívást eszközöl klikkenként. Nyilván sokszori futtatással, warm-up után. Egyetlen hívás 40 és 80 ms között fut le MRI-vel (átlag 60 ms).

OS: Ubuntu 16.04 LTS, x64 / Apache + FCGI

MRI, v2.4.1, forrásból fordítva
Rubinius, v 3.84 (MRI 2.3.1), forrásból fordítva

Eredmény: Rubinius 4x lassabb átlagban

(MRI az ún. eredeti C-ben írt interpreter, Rubinius pedig egy C++-ban és Ruby-ban fejlesztett alternatíva)


Megnéztem még a dev ágat (2.5.0) a mai nightly snapshottal, ugyanúgy forrásból fordítva, eredmény: milliszekundumra egyezik az átlag a 2.4-es verzióval.


Csináltam még egy apró szintetikus tesztet fibonacchi sorozattal, aminek nincs sok értelme, de érdekes lassúságot mutat Rubinius itt is - úgy látszik az elmúlt pár évben jól ráhúzott MRI (Rubinius 2.77x lassabb):

def f(n); return n if n <= 1; return f(n-1) + f(n-2); end

require "benchmark"

puts Benchmark.realtime { f 40 }

ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-linux]
=> 14.174080050001066

rubinius 3.84 (2.3.1 3970f17d 2017-08-02 3.8.0) [x86_64-linux-gnu]
=> 39.267210773


További pár értelmetlen teszt:

Python:

def f(n):
	if n <= 1:
		return n
	return f(n-1) + f(n-2)


import time

t = time.time()
f(40)
print (time.time() - t)

2.7.12 (default, Nov 19 2016, 06:48:10) [GCC 5.4.0 20160609]
=> 35.6807920933
3.5.2 (default, Nov 17 2016, 17:05:23) [GCC 5.4.0 20160609]
=> 41.74373817443848
2.7.10 (5.1.2+dfsg-1~16.04, Jun 16 2016, 17:37:42) [PyPy 5.1.2 with GCC 5.3.1 20160413]
=> 4.68786501884


PHP:

<?php

function f($n) {
  if ($n <= 1) {
     return 1;
  }
  else {
     return f($n-1) + f($n-2);
  }
}


$t = microtime(true);
f(40);
echo (microtime(true) - $t);

?>

PHP 7.0.22-0ubuntu0.16.04.1 (cli) ( NTS )
=> 11.148350954056


Perl:

sub f { return @_[0] < 2 ? @_[0] : f(@_[0]-1) + f(@_[0]-2); }

use Time::HiRes qw( time );
my $t = Time::HiRes::gettimeofday();
f(40);
print (Time::HiRes::gettimeofday() - $t);

This is perl 5, version 22, subversion 1 (v5.22.1) built for x86_64-linux-gnu-thread-multi
=> 107.15203499794


Összesítés, Fibonacchi(40):

  4.7s pypy 5.1.2
 11.1s php 7.0.22
 14.2s ruby 2.4.1
 35.7s python 2.7.12
 39.3s rubinius 3.84
 41.7s python 3.5.2
107.1s perl 5.22.1

Hozzászólás megjelenítési lehetőségek

A választott hozzászólás megjelenítési mód a „Beállítás” gombbal rögzíthető.

Aki nem tud arabusul, az ne beszéljen arabusul.

perl -MTime::HiRes=time -e 'sub f { @_[0] < 2 ? @_[0] : f(@_[0] - 1) + f(@_[0] - 2) } my $t = time; f(40); print time - $t'
66.8140659332275

perl -MTime::HiRes=time -e 'sub f { $_[0] < 2 ? $_[0] : f($_[0] - 1) + f($_[0] - 2) } my $t = time; f(40); print time - $t'
44.8093011379242

This is perl 5, version 24, subversion 2 (v5.24.2) built for x86_64-linux-thread-multi

(A ruby-változat 10.0 s ugyanitt)

(Az mondjuk szomorú, hogy még így is a perl a leglassabb - régen arról volt híres, hogy a szkriptnyelvek között ő a leggyorsabb. Igaz, azt is tudjuk, hogy a függvényhívás aránylag drága művelet Perlben, ez a teszt meg pont arra van kihegyezve.)

(Másfelől
perl -MTime::HiRes=time -MMemoize -e 'memoize("f"); sub f {$_[0]<2?$_[0]:f($_[0]-1)+f($_[0]-2)} my $t = time; f(40); print time - $t'
0.000284910202026367

)

Az első perl résznél nem látom mit változtattál érdemben.

$_[0] vs @_[0]

Az előbbi visszaadja a @_ tömb (amelyben az argumentumok adódnak át a függvénynek) 0. elemét skalárként. Az utóbbi pedig egy ún. array slice, azaz egy egy elemű tömb, az eredeti @_ tömb 0. elemével.
Véletlenül működik és ugyanazt az eredményt adja ebben az esetben, de nem ugyanaz a kettő.

Köszi.

Ez a perl tényleg annyira undorító, mintha úgy született volna, hogy egy csapat autista programozót begombázva bezárnak egy hétre a pincébe, hogy programnyelv nélkül nem jöhetnek ki a szabad levegőre.

--
arch,debian,retropie,osmc,android,windows

Mellékelem a fenti C-re átírt változatát (fibo.c):

#include <time.h>
#include <stdio.h>

int f(int n) {
    if (n <= 1) return n;
    return f(n-1) + f(n-2);
}

int main() {
    struct timespec gstart, gend;
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &gstart);
    int fib = f(40);
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &gend);

    double eltime = (gend.tv_sec - gstart.tv_sec) + (gend.tv_nsec - gstart.tv_nsec)/1000./1000./1000.;
    printf("eredmeny: %d, ido: %f mp\n", fib, eltime);

    return 0;
}

Fordítása: gcc -O2 fibo.c -o fibo

Futásidő normalizálva log69 számítógépének tempójára:

  0,7s C (gcc-7)
  4.7s pypy 5.1.2
 11.1s php 7.0.22
 14.2s ruby 2.4.1
 35.7s python 2.7.12
 39.3s rubinius 3.84
 41.7s python 3.5.2
107.1s perl 5.22.1

@hg2ecz: kösz, ez érdekes.

És még egy érdekes: lua szkript de luajit-tel futtatva.

#!/usr/bin/luajit

function f(n)
    if n <= 1 then
        return n
    end
    return f(n-1) + f(n-2)
end

local t = os.clock()
f(40)
print ( os.clock() - t )

A futásidő log69 számítógép tempójára normalizálva:

  0.7s C (gcc-7)
  1.7s luajit 2.0.4
  4.7s pypy 5.1.2
 11.1s php 7.0.22
 14.2s ruby 2.4.1
 30,5s lua5.3
 35.7s python 2.7.12
 39.3s rubinius 3.84
 41.7s python 3.5.2
107.1s perl 5.22.1

@hg2ecz: LUAJit érdekes. Én most futtattam még JRuby-t (1.7.22 (1.9.3p551)), ugye ez Java JVM-en fut bytecode-ra fordulás után, 5.5 sec alatt fut.

Közben buildeltem friss JRuby-t (9.1.12.0 (2.3.3)), ezzel 7.4 sec

És egy népszerű szkriptnyelv: javascript & nodejs

#!/usr/bin/nodejs

function f(n) {
    if (n <= 1) return n
    return f(n-1) + f(n-2)
}

var t = new Date()
f(40)
console.log((new Date() - t)/1000.)

És a normalizált tempó:

  0.7s C (gcc-7)
  1.7s luajit 2.0.4
  2.2s nodejs v4.8.4
  4.7s pypy 5.1.2
 11.1s php 7.0.22
 14.2s ruby 2.4.1
 30,5s lua5.3
 35.7s python 2.7.12
 39.3s rubinius 3.84
 41.7s python 3.5.2
107.1s perl 5.22.1

-

Tovább a fun kedvéért, sima JS-t futtatva FF 55-ön 1.4 sec illetve Chromium-on 1.7 sec.

https://frontfoo.com/main?user_main=f&pass_main=f&menu=code&uuid=368963410976605672212657162222

  0.7s C (gcc-7)
  1.1s js (epiphany 3.18.11)
  1.4s js (firefox 55.0.2)
  1.7s js (chrome 60.0.3112.78)
  1.7s luajit 2.0.4
  1.9s js (midori 0.5.11)
  2.2s nodejs v4.8.4
  4.7s pypy 5.1.2
  5.5s jruby 1.7.22
  7.4s jruby 9.1.12.0
 11.1s php 7.0.22
 14.2s ruby 2.4.1
 30,5s lua5.3
 35.7s python 2.7.12
 39.3s rubinius 3.84
 41.7s python 3.5.2
107.1s perl 5.22.1

Még egy érdekes témáról ne feledkezzünk meg: eddig a programozási nyelvek futásteljesítményét vizsgáltuk, kihegyezve a rekurzióra.

Nézzük meg a programozó szerepét. Fibonacci, de nem rekurziv módon és legyen most Javascript.

#!/usr/bin/nodejs

function f(n) {
    var p1 = 1,
        p2 = 0,
        fibo = 0;

    for (var i = 1; i < n; i++) {
        fibo = p2 + p1;
        p2 = p1;
        p1 = fibo;
    }
    return fibo;
}

var t = new Date()
console.log( f(40) )
console.log((new Date() - t)/1000.)

Ugyanazt a végeredményt számolva, de máshogy megírt algoritmussal, jelen esetben a rekurzió kerülésével kb. 150-szeresére gyorsítottuk.

Igen, de nem 150x, hanem akár sok milliószoros, mert itt most 2 monoton növekvő görbénk van, csak a meredekségük különböző, és ahogy haladunk jobbra az x tengelyen egyre nagyobb fibo értéket kiszámolva, úgy nő exponenciálisan a két algo közti sebesség (idő) különbség.

Igazából a rekurzív algo nyilván csak egyetlen keresztmetszetet ad, de azért érdekes látni értékeket ebből a szempontból is. Ha lesz időm, lefuttatom a nem rekurzívat több nyelvre.

Kérdés még ennél az algonál, hogy nem fut-e az egész szám értéke a határokon túl, mert Ruby-nál nem tud ugye és ott lefut normálisan. Viszont JS alatt böngészőn belül túl gyorsan fut le, de ugye ott gyorsan túl is csordul (Infinity). Tehát nehéz érdemben tesztelni így egymáshoz képest a nyeveket ezzel az algo-val. C-ben ugye nem is tudod ábrázolni a fibo(500_000) értéket, hiába a 64 bit integer (2^64 körülbelül fibo(92)-vel egyenlő).

Szerk.:
PHP és Lua sem tesztelhető ezért ahogy látom. Túl gyorsan lefut a túlcsordulás miatt.

Szerk.:
fibo(500_000) körülbelül 2^347_120 és 10^104_494 értékkel egyenlő.

Igen, ezzel célszerű tisztában kell lenni. A Python integer osztálya figyel a túlcsordulásra és nagy számokat automatikusan szoftverből elvégzett aritmetikával kezel.

C estén 64 bit, illetve __int128 (64 bites OS-eken) típus áll rendelkezésedre például a 64 bites szorzások eredményének átvételéhez. Ellenben natív proci sebességen adja.
Utána jöhet a gmp vagy boost lib, amely szoftverből számol tovább.

Példakód C-ben (szedd ki a megfelelő // -t a típusválasztáshoz):

#include <stdio.h>

// ---> __fp16 IEEE754:2008 ... ARM-okon megy
//#define TYPE __fp16
//#define TYPE float
//#define TYPE double
//#define TYPE long double
//#define TYPE __float128

// ---> unsigned +1 bit
//#define TYPE byte
//#define TYPE short
//#define TYPE int
//#define TYPE long long
//#define TYPE __int128


int main() {
    TYPE ro, r2, r = 1;
    for (int i = 0; i < 9999; i++) {
        ro = r;
        r *= 2;
        r2 = r+1;
        if (r <= ro || r2 == r) {
            printf("Felbontas: %d bit\n", i+1);
            break;
        }
    }
    return 0;
}

A szkriptnyelvek numerikus értéknek gyakran double-t használnak. Lásd alábbi teszteket:

#!/usr/bin/nodejs

var ro, r2, r = 1;
for (var i = 0; i < 9999; i++) {
    ro = r;
    r *= 2;
    r2 = r+1;
    if (r <= ro || r2 == r) {
        console.log("Felbontas: %d bit\n", i+1);
        break;
    }
}

#!/usr/bin/luajit

local ro = 0
local r2 = 0
local r = 1
for i=0, 9999 do
    ro = r
    r = r*2
    r2 = r+1
    if r <= ro or r2 == r then
        print("Felbontas: ", i+1, "bit");
        break;
    end
end

#!/usr/bin/php
<?php
    $r = 1;
    for ($i = 0; $i < 9999; $i++) {
        $ro = $r;
        $r *= 2;
        $r2 = $r+1;
        if ($r <= $ro || $r2 == $r) {
            printf("Felbontas: %d bit\n", $i+1);
            break;
        }
    }
?>

C: típusfüggő, unsigned __int128 (128 bit) és __float128 (112 bit felbontás) a natív CPU aritmetika felső határa.
NodeJS, LuaJIT: 53 bit (double)
PHP7: 63 bit, de $r2 = $r+1.5; esetén 54 bit.
Python, Ruby: nincs a számábrázolásnak felső határa.

C-ben nem tudnál összeütni egy olyan algo-t, amely "arbitrary long integer"-t tud használni?

GMP a barátunk. A C-beli (processzorbeli) aritmetika helyett szoftveresen összerakott aritmetikával dolgozik.
Ellenben ezzel a teszttel eljutunk oda, hogy nem az adott nyelven megírt algoritmus futását teszteljük, hanem azt nézzük, mely programnyelv miként használja fel ugyanazt a C-ben megírt libgmp-t.

#include <gmp.h>
#include <time.h>
#include <stdio.h>

mpz_t *f(int n) {
    static mpz_t fibo;
    mpz_t p1, p2;
    mpz_init_set_ui(p1, 1);
    mpz_init(p2);

    for (int i = 1; i < n; i++) {
        mpz_add(fibo, p2, p1);
        mpz_add_ui(p2, p1, 0);
        mpz_add_ui(p1, fibo, 0);
    }
    mpz_clear(p1);
    mpz_clear(p2);
    return &fibo;
}

int main() {
    struct timespec gstart, gend;
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &gstart);

    mpz_t *res = f(40);

    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &gend);
    mpz_out_str(stdout, 10, res); puts("");
    double eltime = (gend.tv_sec - gstart.tv_sec) + (gend.tv_nsec - gstart.tv_nsec)/1000./1000./1000.;
    printf("ido: %f mp\n", eltime);
    return 0;
}

"ezzel a teszttel eljutunk oda.."

Egyetértek. 1.9s lett amúgy.

De ez is kis trükkel felezhető. Hagyjuk el a GMP-beli másolást.

#include <gmp.h>
#include <time.h>
#include <stdio.h>

mpz_t *f(int n) {
    static mpz_t prev[4];
    mpz_init_set_ui(prev[0], 1);
    mpz_init(prev[1]);
    mpz_init(prev[2]);
    mpz_init(prev[3]);

    int i;
    for (i = 1; i < n; i++) {
        mpz_add(prev[i&3], prev[(i-2)&3], prev[(i-1)&3]);
    }
    return &prev[(i-1)&3];
}

int main() {
    struct timespec gstart, gend;
    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &gstart);

    mpz_t *res = f(500000);

    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &gend);
    mpz_out_str(stdout, 10, res); puts("");
    double eltime = (gend.tv_sec - gstart.tv_sec) + (gend.tv_nsec - gstart.tv_nsec)/1000./1000./1000.;
    printf("ido: %f mp\n", eltime);
    return 0;
}

0.9 sec

Ruby:

def f2(n)
	p1 = 1
	p2 = 0
	fibo = 0

	while (n > 1) do
		fibo = p2 + p1
		p2 = p1
		p1 = fibo
		n -= 1
	end

	return fibo
end

require "benchmark"

puts Benchmark.realtime { x = f2(500_000) }

Python:

def f2(n):
	p1 = 1
	p2 = 0
	fibo = 0

	while (n > 1):
		fibo = p2 + p1
		p2 = p1
		p1 = fibo
		n -= 1

	return fibo


import time

t = time.time()
x = f2(500000)
print (time.time() - t)

fibo 500_000:

 2.0s pypy 2.7.10
 2.7s python 2.7.12
 2.7s python 3.5.2
 3.0s ruby 2.4.1
 5.2s jruby 1.7.22 (1.9.3p551)
 5.2s jruby 9.1.12.0 (2.3.3)
10.2s rubinius 3.84 (2.3.1)