Benchmark práce s objekty v jazycích C++, Vala a Python

Již delší dobu se chystám programovat v jazyku Vala. Jde o objektově orientovaný jazyk postavený nad GObject. Mima jiného mě zajímala i rychlost výsledného programu. Připravil sem tedy test, který porovnává rychlost práce s třídami a instancemi s ostatními jazyky, ve kterých programuji.

20.9.2011 10:00 | Ondřej Tůma | přečteno 6620×

Kompilátor jazyka Vala sám nevytváří binární kód, ale vygeneruje kód v čistém C, který je kompilován běžným kompilátorem, typicky gcc. A protože je inspirován vysokoúrovňovými jazyky a orientován právě na GObject, je interní práce s objekty od ostatních jazyků odlišná. Objekty jsou udržovány v paměti obdobně jako v jazyku Python pomocí referencí. Počítají se tedy reference na objekt, a pokud jsou vynulovány, objekt je z paměti uvolněn.

Test spočívá v definici jednoduché třídy Foo, která má jeden parametr typu integer. Tato třída je následně milionkrát vytvořena, je vyčten její parametr (zavolána metoda instance) a následně zničena. Ještě než přijde řada na zdrojové kódy testů, rád bych uvedl, že jsem si vědom, že kód není nijak optimalizován na konkrétní jazyky a v řadě případů by šel jistě napsat daleko efektivněji. Účelem testu však bylo zjistit, jaké jsou nároky na CPU při běžném použití.

Následují jednotlivé zdrojové kódy, případně jejich variace a výsledný čas měřený nástrojem time. Čas byl měřen třikrát a v grafech jsou použity průměry naměřených hodnot.

C++

V případě C++ jsem do testu začlenil použití třídy (class) bez předka. Rozdíl v použití struktury (struct) je velmi malý a pro srovnání s jazykem Vala vlastně ne zcela vypovídající.
int count = 1000*1000;

class Foo {
private:
int i;

public:
Foo(int i){
this->i = i;
}
int geti(){
return this->i;
}
};

int main(int argv, char ** argc){
for (int i = 0; i < (count); i++){
Foo foo(i);
foo.geti();
}

return 0;
}
C++ class:                                  avg
real 0m0.038s 0m0.039s 0m0.027s 0m0.035s
user 0m0.040s 0m0.032s 0m0.028s 0m0.033s
sys 0m0.000s 0m0.000s 0m0.000s 0m0.000

A také dědění třídy Glib.Object z glibmm, což je c++ wrapper nad glib knihovnou. Rozdíl mezi glibmm a Valou je zejména v tom, že výsledný program nevyžaduje žádnou další knihovnu ke svému provozu.
#include <glibmm/object.h>
#include <glibmm/init.h>

int count = 1000*1000;

class Foo : public Glib::Object {
private:
int i;

public:
Foo(int i):Glib::Object(){
this->i = i;
}
int geti(){
return this->i;
}
};

int main(int argv, char ** argc){
Glib::init();

for (int i = 0; i < (count); i++){
Foo foo(i);
foo.geti();
}

return 0;
}
C++ GObject:                                avg
real 0m5.222s 0m5.308s 0m5.225s 0m5.252s
user 0m5.140s 0m5.176s 0m5.124s 0m5.174s
sys 0m0.004s 0m0.016s 0m0.012s 0m0.011s

Vala

Jazyk Vala může přistupovat k třídám různými způsoby. Nejpodobnější třídě tak jak ji známe z C++ je Struct, který je značně omezen v případě použití jako třídy, nicméně režie okolo vytváření a práce se strukturou se blíží režii v C++, což je dáno zejména tím, že se nepracuje s referencemi.
int count = 1000*1000;

struct Foo {
public int i;
public Foo.new(int i){
this.i = i;
}

public int geti(){
return this.i;
}
}

public int main(string[] args){
for (int i = 0; i < (count); i++){
Foo foo = Foo(){
i = i
};
foo.geti();
}

return 0;
}
Vala struct:                                avg
real 0m0.102s 0m0.102s 0m0.105s 0m0.103s
user 0m0.088s 0m0.096s 0m0.096s 0m0.093s
sys 0m0.008s 0m0.000s 0m0.004s 0m0.004s

Další možnost jak vytvořit odlehčenou třídu je použití direktivy [Compact].
int count = 1000*1000;

[Compact]
class Foo {
public int i;

public Foo(int i){
this.i = i;
}

public int geti(){
return this.i;
}
}

public int main(string[] args){
for (int i = 0; i < (count); i++){
Foo foo = new Foo(i);
foo.geti();
}

return 0;
}
Vala compat class:                          avg
real 0m0.393s 0m0.395s 0m0.399s 0m0.396s
user 0m0.388s 0m0.396s 0m0.392s 0m0.392s
sys 0m0.000s 0m0.004s 0m0.004s 0m0.003s

V jazyce Vala lze dále dělit třídy na ty které nejsou děděny z GLib.Object a ty které jsou. Tyto dvě skupiny budou nejčastěji používané a dva předchozí testy jsou malým porušením testu, neboť se snaží optimalizovat výsledný kód. Ty třídy které nejsou potomky GLib.Object mají o něco menší režii.
int count = 1000*1000;

class Foo {
private int i;

public Foo(int i){
this.i = i;
}

public int geti(){
return this.i;
}
}

public int main(string[] args){
for (int i = 0; i < (count); i++){
Foo foo = new Foo(i);
foo.geti();
}

return 0;
}
Vala class:                                 avg
real 0m2.712s 0m2.758s 0m2.404s 0m2.634s
user 0m2.620s 0m2.716s 0m2.372s 0m2.569s
sys 0m0.004s 0m0.012s 0m0.012s 0m0.009s

Na závěr tedy kód a čas v případě použití třídy, která je potomkem GLib.Object. Je asi vhodné dodat, že právě tyto třídy jsou v jazyce Vala preferované, což je vzhledem k nejčastějšímu použití jazyka pochopitelné.
int count = 1000*1000;

class Foo: GLib.Object {
private string name;
private int i;

public Foo(string name, int i){
this.name = name;
this.i = i;
}

public int geti(){
return this.i;
}
}

public int main(string[] args){
for (int i = 0; i < (count); i++){
Foo foo = new Foo("This is funcking foo", i);
foo.geti();
}

return 0;
}
Vala GObject:                               avg
real 0m6.042s 0m6.052s 0m6.142s 0m6.079s
user 0m5.956s 0m5.932s 0m6.040s 0m5.976s
sys 0m0.004s 0m0.012s 0m0.008s 0m0.008s

Python

Jak už bylo zmíněno, i Python používá ke správě objektů reference. A jelikož je Python jeden z mých hlavních programovacích jazyků, začlenil i jeho tři testy.

První test používá třídu bez předka. Jde o vlastně starý koncept tříd v Pythonu, který je udržován zejména kvůli zpětné kompatibilitě.
count = 1000*1000

class Foo:
def __init__(self, name, i):
self.name = name
self.i = i

def geti(self):
return self.i

for i in xrange(count):
foo = Foo("This is funcking foo", i)
foo.geti()
Python class:                               avg
real 0m6.052s 0m5.771s 0m6.066s 0m5.963s
user 0m5.888s 0m5.636s 0m5.912s 0m5.812s
sys 0m0.004s 0m0.020s 0m0.012s 0m0.012s

V druhém testu je použita třída object. Jde tedy o standardní způsob vytváření objektů. Takto vytvořené třídy a jejich instance mají některé další možnosti, a je to poznat i na režii CPU.
count = 1000*1000

class Foo(object):
def __init__(self, name, i):
self.name = name
self.i = i

def geti(self):
return self.i

for i in xrange(count):
foo = Foo("This is funcking foo", i)
foo.geti()
Python Object:                              avg
real 0m7.129s 0m7.611s 0m6.910s 0m6.883s
user 0m6.972s 0m6.484s 0m6.768s 0m6.741s
sys 0m0.020s 0m0.020s 0m0.028s 0m0.023s

Poslední test stejně jako v C++ nebo Vale používá třídu poděděnou z gobject.GObject. Zde už režie cpu dosahovala poněkud extrémních hodnot.
import gobject

count = 1000*1000

class Foo(gobject.GObject):
def __init__(self, name, i):
gobject.GObject.__init__(self)
self.name = name
self.i = i

def geti(self):
return self.i

for i in xrange(count):
#glib.init()

foo = Foo("This is funcking foo", i)
foo.geti()
Pthon GObject:                              avg
real 0m39.760s 0m39.194s 0m39.332s 0m39.760s
user 0m38.990s 0m38.518s 0m38.658s 0m38.772s
sys 0m0.032s 0m0.020s 0m0.040s 0m0.031s

Grafy


Obr1: Použití základních tříd jazyka.


Obr2: Použití tříd odvozených od GObject implementace v daném jazyku.

Pár slov na závěr

Z uvedených výsledků je zřejmé, že si jazyk Vala nestojí zase až tak špatně. Nicméně vzhledem k tomu, že jde o jazyk určený zejména k tvorbě grafických programů, je škoda, že chybí srovnání s jazyky C# (Mono) a Java. Stejně tak zde chybí test GObjectu v čistém C, který je značněji komplikovanější na použití, bylo by však vidět jak moc si člověk pohorší s výkonem výsledné aplikace.

Výsledný program nebyl nijak optimalizován, to znamená že kompilace probíhala bez dalších parametrů. Stejně tak sem si vědom, že jednotlivé jazyky používají různé vlastní optimalizace a ty, i když nejsou předmětem testu ovlivňují výsledky. A v konečné fázi si rozhodně nemyslím, že typická aplikace vytváří a následně ničí milion objektů. Na druhou stranu rozsáhlá aplikace může takových objektů mít opravdu mnoho, což je ale zcela jiná kapitola.

Jednoznačnou výhodu Valy proti C++ s glibmm vidím zejména v novém přístupu k programování. Jde o jiný jazyk, podobný spíše vysokoúrovňovým jazykům, který je ale šitý na míru GObjectu, tedy GTK+ knihovně. Výsledný kód je ale kompilovaný do binární podoby externím programátorem, a tak nejsou zapotřebí žádné další knihovny, což je vidět i na velikostech výsledných binárek.
Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1873