HTML

C++ programozás

Főként C++ programozásról, de lehet szó még C#, D vagy más nyelvről is.

Friss topikok

  • tormanator: A CG-shaderben megírt raytracing 106x gyorsabb, mint egy SSE-utaításokkal futó raytracing. Mindeg... (2011.09.08. 07:00) Csak párhuzamosan!
  • koszperv: @Vorbis: Szia! Köszöntlek, mint a blogom első hozzászólóját. Az enum egyébként tényleg egész haszn... (2009.12.30. 11:44) const vs define

Linkblog

2010.01.21. 00:09 koszperv

Előre!

Eddig volt olyan feature, ami lerövidítette a kódot, volt, ami felgyorsította a kódot, volt, ami kijavított egy régi hibát, és most lesz egy, aminek, nos... használhatósága erősen korlátozott. Ez pedig a perfect forwarding. Ami nem más, minthogy úgy adjuk át a bekapott paramétereket egy belső függvényhívásnak, hogy azok lvalue-rvalue-sága ne sérüljön. Vágjunk hát bele a közepébe. A perfect forwarding így néz ki:

template<typename Left, typename Right>
    auto Add(Left&& left, Right&& right)
        -> decltype(forward<Left>(left) + forward<Right>(right))
{
    return forward<Left>(left) + forward<Right>(right);
}
A perfect forwardingnak gyakorlatilag csak egy általánoscélú library template-ei között van bármi értelme, ezért is egy template-en mutatom be (bár egy ilyen összeadó template-nek nincs semmi értelme, de a célnak megfelel - ráadásul máshol is általában ezzel mutatják be).

Amint látható, az újfajta autós függvényfejlécet alkalmazza. Ez azért szükséges, hogy a decltype-ban már használhassuk az input paramétereket lokális változóként. Egyébként nullpointerekkel kellene bűvészkedni, azt pedig senki nem akar.

A decltype pedig nem csinál mást, minthogy megmondja milyen típusú is a beleírt kifejezés eredménye. Így az Add template-függvény pont olyan típussal tér majd vissza, mint amivel az összeadás. Ez azért fontos, mert egy template-ben egy overloadolt operátor, vagy egy ismeretlen függvény visszatérési típusát nem ismerhetjük. Így viszont könnyedén le tudjuk kérdezni.

A következő trükk az input paramétereknél van. Ha a bejövő típusban van referencia, akkor az rvalue referencia beleolvad, ha nincs, akkor hozzáadódik. Ezáltal a paraméter típusa lvalue referencia lesz, ha az eredeti típus is lvalue referencia volt, és rvalue referencia lesz, ha eredetileg nem volt referencia, vagy rvalue referencia volt. Röviden lvalue referencia lesz, ha lvalue volt, és rvalue referencia lesz, ha rvalue volt.

Van itt még egy trükk: ez pedig a foward. (A forwardnak mindig van template-argumentuma, aminek mindig az eredeti típusnak kell lennie! Ez az argumentum valamilyen bonyolult template bűvészkedés miatt kell oda, de nem tudom racionálisan megindokolni, hogy miért.) A feladata, hogyha lvalue referenciát kap, akkor azt lvalue-ként adja ki, ha pedig értéket vagy rvalue referenciát kap, akkor azt rvalue-ként adja tovább. Ezáltal ha lvalue jön be a template-függvényünkbe, akkor a forward lvalue-t ad tovább, ha pedig rvalue jön be, akkor rvalue-t ad tovább. Hogy ez miért fontos? Két eset lehetséges: a belső függvény lvalue-t vár, és szeretné megváltoztatni, ekkor rvalue-t nem is kaphat; illetve, ha a belső függvény rvalue-t vagy konstans lvalue-t vár, és nem akarja megváltoztatni. Ha egyik feature-t sem szeretnénk elveszíteni, akkor eddig ezt a problémát overloaddal oldhattuk meg eddig. Ez paraméterenként 2 overload. Mármint nem összeadva, hanem összeszorozva, így 3 paraméter már 8 függvényt jelent. Ezeket az overloadokat most nem kell megcsinálni, mert elég egyetlen függvény is hozzá.

Igen jól látjátok, ez a perfect forwarding azért még nem teljesen perfect. Ugyanis csak egy adott paraméterszámú függvényt képes befogadni. Mégha a belső függvény nem is változó paraméterszámú, akkor is külön overloadot kell írni az egyparaméteres, a kétparaméteres, stb függvényekre. Ezt a "fontos" és "roppant sűrgős" problémát remélhetőleg a változó paraméterszámú template-ek megoldják majd.

A decltype-ról érdemes még néhány szót szólni, mert nem azt csinálja, mint amire az ember számítana. A szabvány szerint ez a feladata:

decltype(e)

  1. Ha 'e' egy nem zárójelezett változó, tagváltozó, függvény, metódus, statikus változó, stb, akkor a változó vagy függvény típusát jelenti.
  2. Egyébként, ha 'e' egy függvény- vagy operátorhívás, akkor annak a visszatérési típusát jelenti.
  3. Egyébként, ha 'e' lvalue, akkor a visszaadott típus egy lvalue referencia 'e' típusára.
  4. Egyékbént, 'e' típusa.

int fn() { return 5; }
int i;
struct A { double x; };
A a;

decltype(fn()) x1;            // int
decltype(i) x2;             // int
decltype(a.x) x3;             // type is double
decltype((a.x)) x4(a.x);    // type is const double&
decltype(fn) *x5 = &fn;
        // decltype: int(void) x5: int(void)*
A példák a szabvány alapján vannak. A legdurvább az, hogy ha a.x-et dupla zárójelbe rakjuk akkor referencia lesz belőle. Őszintén szólva nekem ez így roppant bizar lett. Érdekességként még ott az utolsó sor, amiben nem egy függvényhívást teszek a decltype-ba, hanem egy függvényt. Ebben az esetben a függvény típusát adja vissza, nem pedig a függvény visszatérési típusát. Ezt a típust azonban csak függvénypointerként tudjuk értelmesen felhasználni, ezért raktam oda azt a csillagot.

 

Szólj hozzá!

2010.01.19. 08:31 koszperv

Uniquum

Az auto_ptr valódi unikum volt az STL template-ek között, hiszen senki mással nem volt hajlandó együttműködni. Funkciója az volt, hogy bele lehessen rakni egy pointert, és ő meghívja a delete-et rá, ha meghívódik a saját destruktora. Ezért aztán copy-funkciót nem raktak bele, csak move-ot, mivel ha két auto_ptr-ben is szerepelne ugyanaz a pointer, akkor azt duplán törölnék. A heapen tárolt objektum lemásolása pedig elütött a szerepétől. Így úgy gondolták, a move-szemantikát, lévén, hogy akkor még nem létezett rvalue referencia, a copy construktorral és a copy assignment operátorral oldják meg. Ez azonban azt eredményezte, hogy ha berakták egy vectorba, akkor a vector újrafoglalása rendben működött, de egy másik vectorba már nem lehetett áttölteni. Emiatt letiltották a használatát a konténerekben. Viszont továbbra is remekül lehetett használni lokális változókban RAII-nak, és biztonságos pointereknek tagváltozókba.

Most viszont már meg lehet különböztetni a copy és a move szemantikát az rvalue referenciák segítségével. Ezért hozták létre a unique_ptr-t, ami átveszi a szerepét. Sajnos az auto_ptr-t nem lehetett kijavítani, mert az a copy konstruktorban és a copy assignment operátorban végezte a move-szemantikáját, és ha ezt átírják move konstruktorra és move assignment operátorra, akkor sok program nem futna többé. A unique_ptr nem tud másolódni, viszont move-olódni igen. Ezért ha berakjuk a vectorba, és azt megpróbáljuk lemásolni egy másik vectorba, kapunk egy hosszú és érthetetlen hibaüzenetet, melynek lényege, hogy pár függvény implementációja hiányzik a template-ből. Viszont ha nem akarjuk másolni, egy vectoron belül jól megvan. A helyes használata egy vectorral így néz ki:

    for (int i = 0; i < 10000; ++i)
    {
        unique_ptr<Obj> o(new Obj());
        v.push_back(move(o));
    }
Figyeljünk arra, hogy a move függvényt itt is használnunk kell, mert egyébként nem tudná, hogy move-olni akarjuk, és hibaüzenetet adna.

Így a unique_ptr lett az új auto_ptr, annyira, hogy az auto_ptr-t deprecateddé tették. Vagyis a jövőben célszerű lesz átírni régebbi kódjainkban is az auto_ptr-unique_ptr-re.

 

Szólj hozzá!

2010.01.07. 23:58 koszperv

Költözünk! A bútorokra meg csak rámutatunk majd az új lakásból.

Címkék: reference move constructor operator cpp0x lvalue rvalue assignment

Körülbelül ezzel lehet leírni a move konstruktor és a move assignment operátor funkcióját. Vagyis, ha egy objektumot értékszerint átmozgatunk egy másik helyre, miközben tudjuk, hogy a régi hely megszűnik, akkor a heapen tárolt dolgait felesleges újralefoglalni és átmásolni, elég ha a pointereket áttesszük az új objektumba, és lenullázuk a régiben. Ahhoz hogy ezt kényelmesen(???) megtehessük bevezették az rvalue referenciát. (Szerint egyszerűbb azt kiejteni, hogy balérték vagy jobbérték, minthogy lvalue vagy rvalue. De az egyértelműség miatt írásban továbbra is az angol kifejezéseket fogom használni.) Figyelem, ami most következik, az high magic! Mindazonáltal a jövőben felgyorsíthatja a programunkat, így érdemes megérteni valamennyire.

Először is tisztázzuk, mi is az rvalue és az lvalue. Általában azt szoktuk rvalue-nak hívni, ami ideiglenesen létrehozott objektum. De igazából vannak rvalue-k, amik tovább élnek, mint egyes lvalue-k. Valahol azt olvastam, hogy úgy tudjuk könnyen megkülönböztetni őket, hogy az lvalue-knak van nevük, míg az rvalue-k nincs. Legalábbis körülbelül igaz ez. Ez egy viszonylag jó magyarázat, de nem tökéletes. Talán a legpontosabb fogalmazás az, hogy rvalue az, aminek le tudjuk kérdezni a címét az & operátorral. Rvalue-t általában azok a függvények és operátorok adnak vissza, amiknek a visszatérési értékének a típusa nem lvalue referencia; illetve bizonyos kasztolásoknál is rvalue keletkezik, ahol ideiglenesen új objektumot kell létrehozni; illetve a legtöbb literál is rvalue (ez utóbbit a szabványból nem tudtam kiolvasni). Például rvalue az 5 és true. De mondjuk lvalue az x változó. Igazából minden változó lvalue. Függvényekre visszatérési értékeire pedig itt van három példa:

int fn1();
int& fn2();
int&& fn3();
A két zöld függvény (monokróm monitor előtt ülők kedvéért: fn1 és fn3) rvalue-t ad vissza, młg a kék fn2 lvalue-t. A kisérletezéseim alapján a string literál konstans lvalue-nak számít, de nem vagyok biztos benne. (A szabványból nem tudtam kitalálni.)

lvalue-ra hivatkozhatunk lvalue referenciával, ami azonos a jó öreg referenciával. Egy & jel jelöli. rvalue-ra viszont lvalue referenciák közül csak a konstans verzióval hivatkozhatunk. Ezért van az, hogy ha egy függvény const string& paraméterrel rendelkezik, annak át tudunk adni egy string literált, míg egy string& paraméterű függvénynek nem. Rvalue referenciával ezzel szemben tetszőleges rvalue-ra hivatkozhatunk, és meg is változtathatjuk őket. A jele: &&. Természetesen ebből is van konstans verzió.

Mivel most referenciákat fogunk nagy mennyiségben egymásra halmozni, még egy fontos szabály jön be: a referenciák összeolvadhatnak. Természetesen nem írhatunk két referencia jelet egymás mellé, de egy typedeffel létrehozott típus, vagy egy template paraméter hordozhat magában már egy referenciát, és a felhasználás helyén ehhez hozzáadhatunk egy továbbit is. A C++-ban nincs referenciára mutató referencia, így ilyen esetben a többszörös referenciák egyszeressé alakulnak át. Ráadásul az lvalue referencia a domináns:

typedef int& T;
typedef int&& U;

void alfa(T&& x) { }
void beta(U&& x) { }
void gamma(U& x) { }

alfa(5);
beta(5);
gamma(5);
Ezekből a kifejezésekből alfa és gamma meghívása hibás, mert a referencia összeolvadási szabály alapján lvalue referenciával probálnak rvalue-ra mutatni.

Ha az rvalue referenciát függvényparaméter típusaként alkalmazzuk, akkor tudnunk kell, hogy csak rvalue-kat kap el, abban viszont erősebb konstans lvalue referenciánál.

void multi(string& a) { cout << "one\n"; }
void multi(const string& a) { cout << "two\n"; }
void multi(string&& a) { cout << "three\n"; }
void multi(const string&& a) { cout << "four\n"; }

string s = "a";
const string s2 = "b";
multi(s);
multi(s2);
multi(string("d"));
A végeredménye:

one
two
three

Az első két hívás lvalue-t adott át a függvényeknek, ezért az lvalue referenciák kapták el. A harmadik esetben rvalue-t adunk át, amit az rvalue referencia kap el.

A két függvény, amit meg kell írnunk egy osztály mozgatásának felgyorsításához a move construktor, ami erősen hasonlít a copy construktorra, és a move értékadás (assignment) operátor, ami erősen hajaz a copy értékadás operátorra, vagyis a régi értékadás operátorra. A különbség a move és a copy függvények között, hogy copy esetén az értékek megduplázódnak, míg a move-nál a régi ideiglenes objektumból eltűnnek, és az új objektumba költöznek.

Lássuk ezt egy példán. Először írok egy hagyományos objektumot, ami egy karaktertömböt tárol a heapen. Nevezzük MovableStore-nak az osztályt:

#include <memory>
#include <iostream>

using namespace std;

class MovableStore
{
private:
    static const size_t defaultSize = 32;
    char *m_data;
    size_t m_size;

    void clear()
    {
        delete[] m_data;
        release();
    }

    void release()
    {
        m_data = NULL;
        m_size = 0;
    }

public:
    // Konstruktorok

    // Default konstruktor
    MovableStore():
        m_data(new char[defaultSize]), m_size(defaultSize)
    {
        cout << "Default konstruktor: size = 32, 0x" << hex << this << dec << endl;
        memset(m_data, 0, defaultSize);
    }

    // Egyparameteres konstruktor
    explicit MovableStore(size_t size, bool b = true):
        m_data(new char[size]), m_size(size)
    {
        cout << "Konstruktor: size = " << size << ", 0x" << hex << this << dec << endl;
        memset(m_data, 0, m_size);
    }

    // Copy konstruktor
    MovableStore(const MovableStore& other):
        m_data(new char[other.m_size]), m_size(other.m_size)
    {
        cout << "Copy konstruktor: size = " << m_size << ", 0x" << hex << &other << " -> 0x" << this << dec << endl;
        memcpy(m_data, other.m_data, m_size);
    }

    // Copy assignment operator
    MovableStore& operator =(MovableStore& other)
    {
        cout << "Copy ertekadas operator: size = " << other.m_size << ", 0x" << hex << &other << " -> 0x" << this << dec << endl;
        if (&other == this)
            return *this;
        delete[] m_data;
        m_data = new char[other.m_size];
        memcpy(m_data, other.m_data, other.m_size);
        m_size = other.m_size;
        return *this;
    }

    // Destruktor
    ~MovableStore()
    {
        cout << "Destruktor: size = " << m_size << ", 0x" << hex << this << dec << endl;
        clear();
    }

    // Lekerdezofuggvenyek
    char *getStorage() const { return m_data; }
    size_t getSize() const { return m_size; }
    bool isEmpty() const { return m_size == 0; }

};
Megjegyzés: az egyparaméteres konstruktor elé odaírtam az explicit kulcsszót. Ez fontos. Ugyanis azok a konstruktorok, amiknek egyetlen paramétere van, vagy amiknek a default értékeit behelyettesítve egyparaméteressé tehetők, azok implicit típuskonverziós konstruktorként is működnek. Vagyis úgy tudunk velük kasztolni, hogy ki sem írjuk a típust. Tehát, ha nem lenne ott az explicit, akkor ez is értelmes lenne: MovableStora a = 5; Ami azért meglepő. De még fontosabb, hogy egy bonyolult kifejezésben a tudtunk és beleegyezésünk nélkül kasztolgathatna kedvére. Mondjuk az explicit szó után is jó ez a sor: MovableStore a = (MovableStore)5; Dehát ehhez már explicit hülyének kell lenni.

Akkor most adjuk hozzá a move konstruktort és a move értékadás operátort:

    // Move konstruktor
    MovableStore(MovableStore&& other):
        m_data(0), m_size(0)
    {
        cout << "Move konstruktor: size = " << other.m_size << ", 0x" << hex << &other << " -> 0x" << this << dec << endl << "    ";
        *this = move(other);
    }

    // Move assignment operator
    MovableStore& operator =(MovableStore&& other)
    {
        cout << "Move ertekadas operator: size = " << other.m_size << ", 0x" << hex << &other << " -> 0x" << this << dec << endl;
        if (&other == this)
            return *this;
        delete[] m_data;
        m_data = other.m_data;
        m_size = other.m_size;
        other.release();
        return *this;
    }
Amint láthatjuk a move függvényekben rvalue referencia van a paraméterlistában. Az értékadás operátor törli az esetleges tartalmat, ha van. Ha nincs, vagyis nullpointer van az m_data-ban, akkor a delete nem fog csinálni semmit. Ezután a pointert és a hosszt átmove-olja a paraméterből. Természetesen ügyelni kell az önmagunkra másolásra, ahogy mindig. A move konstruktorban pedig célszerű átirányítani a vezérlést a move értékadás operátorra, mert így nem lesz megduplázva a kód. Azonban ehhez egy STL-es függvényt is meg kell hívni: a move-ot. Ez nem csinál mást, minthogy rvalue referenciaként adja vissza a bemenetére adott értéket. Ahogy az elején már írtam, egy függvény, ami rvalue referenciát ad vissza, annak a visszatérési értéke rvalue lesz, ebből kitalálható, hogy a move rvalue-t csinál a beletett értékből. De miért kell ez? Nos, ezt is említettem már az elején: minden változó lvalue, márpedig az rvalue referencia paraméterünk lokális változó! Vagyis annak ellenére lvalue, hogy a típusa rvalue referencia. Hát igen, elég fura dolgokat produkál ez az új referenciatípus.

Megjegyzés: Az összes értékadás (=, +=, *=, stb.) operátor visszatérési értéke lvalue referencia, hiszen a baloldalát adjuk vissza. Az lvalue eredetileg innen kaphatta a nevét.

Egy kis teszt:

    cout << "Letrehozzuk m1-et:\n";
    MovableStore m1;
    cout << "m1 cime: 0x" << hex << &m1 << dec << "\n\nLetrehozzuk m2-t:\n";
    MovableStore m2(56);
    cout << "m2 cime: 0x" << hex << &m2 << dec << "\n\nAtmasoljuk m2-t m1-be:\n";
    m1 = m2;
    cout << "\nBemasolunk egy ideiglenes objektumot m1-be:\n";
    m1 = MovableStore(26);
    cout << "\nAtmozgatjuk m2-t m1-be:\n";
    m1 = move(m2);
    if (m2.isEmpty())
        cout << "\nm2 most mar ures.\n";

És az eredménye:

Letrehozzuk m1-et:
Default konstruktor: size = 32, 0x0018F87C
m1 cime: 0x0018F87C

Letrehozzuk m2-t:
Konstruktor: size = 56, 0x0018F86C
m2 cime: 0x0018F86C

Atmasoljuk m2-t m1-be:
Copy ertekadas operator: size = 56, 0x0018F86C -> 0x0018F87C

Bemasolunk egy ideiglenes objektumot m1-be:
Konstruktor: size = 26, 0x0018F70C
Move ertekadas operator: size = 26, 0x0018F70C -> 0x0018F87C
Destruktor: size = 0, 0x0018F70C

Atmozgatjuk m2-t m1-be:
Move ertekadas operator: size = 56, 0x0018F86C -> 0x0018F87C

m2 most mar ures.
Amint láthatjuk move-olásnál a move-olt objektum kiürül, és a másik megkapja a tartalmát. Eddig rendben is vagyunk.

Végeztem egy sebességtesztet: egy vectort feltöltöttem 1000000 rvalue MovableStore objektummal, majd töröltem az egészet, és ezt megismételtem 10-szer. A copy verzió 2.9282 s-ig futott, míg a move verzió 1.38276 s-ig. Tehát a gyorsulás 2.12-szörös, vagyis 112%-os. Ez azért elég jó, és még ha nem is minden esetben lesz ennyi, de azért biztosan számottevő lesz a javulás. Összesen 22099753 konstruktor futott le mindkét esetben. Ebből 10000000 az rvalue objektumé volt, és 12099753 pedig a vector újraméretezése miatt történt. A copy esetben 22099753 delete, new és memcpy történt, a move esetben viszont csak 10000000 delete és new. Mivel a vector átméretezésekor nem kellett megszüntetni és újra létrehozni a heapben tárolt adatterületünket. Emiatt lett gyorsabb. Egy vector<vector<MovableStore>> pedig még gyorsabb lehet a külső vector átméretezésekor, hiszen itt csak a belső vector objektumok fejlécobjektumait kell újrafoglalni. A move miatt a MovableStore-hoz hozzá sem fog érni. Természetesen itt nem a MovableStore move-szemantikája lesz a mérvadó, hanem a vectoré.

Azonban az rvalue referencia még egy dologra jó: ez pedig a stringösszeadás, és minden ahhoz hasonlatos feladat. Ha stringeket adunk össze, minden összeadás után létrejön egy új ideiglenes objektum, ami új stringet foglal le a memóriában, majd belemásolja a másik kettő összegét. Elég kölcséges, és nem túl hatékony folyamat. Mostantól kezdve viszont az első ideiglenes objektumban fogja összegezni a string tartalmát, és ezt a végén átmove-olja a célobjektumba. Itt vannak az operátoraink hozzá (én nem összefűzök két területet, hanem karakterenként összeadom őket a változatosság kedvéért):

class MovableStore
{
    ...

private:
    void add(const MovableStore& other)
    {
        if (other.m_size > m_size)
        {
            char *newData = new char[other.m_size];
            memcpy(newData, m_data, m_size);
            memcpy(newData + m_size, other.m_data + m_size, other.m_size - m_size);
            delete[] m_data;
            m_data = newData;
            m_size = other.m_size;
        }
        for (size_t i = 0; i < m_size; ++i)
            m_data[i] += other.m_data[i];
    }

    friend MovableStore operator +(const MovableStore& left, const MovableStore& right);
    friend MovableStore&& operator +(const MovableStore& left, MovableStore&& right);
    friend MovableStore&& operator +(MovableStore&& left, const MovableStore& right);
    friend MovableStore&& operator +(MovableStore&& left, MovableStore&& right);

};

MovableStore operator +(const MovableStore& left, const MovableStore& right)
{
    cout << "lvalue ref - lvalue ref osszeadas operator: 0x" << hex << &left << " + 0x" << &right << dec << endl;
    MovableStore temp = left;
    temp.add(right);
    return temp;
}

MovableStore&& operator +(const MovableStore& left, MovableStore&& right)
{
    cout << "lvalue ref - rvalue ref osszeadas operator: 0x" << hex << &left << " + 0x" << &right << dec << endl;
    right.add(left);
    return move(right);
}

MovableStore&& operator +(MovableStore&& left, const MovableStore& right)
{
    cout << "rvalue ref - lvalue ref osszeadas operator: 0x" << hex << &left << " + 0x" << &right << dec << endl;
    left.add(right);
    return move(left);
}

MovableStore&& operator +(MovableStore&& left, MovableStore&& right)
{
    cout << "rvalue ref - rvalue ref osszeadas operator: 0x" << hex << &left << " + 0x" << &right << dec << endl;
    left.add(right);
    right.clear();
    return move(left);
}
Hogy az összeadás operátorok bal oldalán is meg tudjuk különböztetni az lvalue és rvalue értékeket, ezért inkább az operátor overloadingot használtam az osztályon belüli operátor deklaráció helyett. Hát igen, 4 esetünk van, hiszen mindkét oldal lehet rvalue és konstans lvalue is. Ha az összeadás operátor meg tudná változtatni az lvalue értékeket is, akkor be kéne venni a nem konstans lvalue referenciát is, és így már 9 overloadot kéne írnunk. Szerencsére most nem ilyen rossz a helyzet.

Megjegyzés: Amikor két rvalue-nk van, akkor az egyiket módosítjuk, de a másikat nem szükséges törölni, mert az hamarosan önmagától megsemmisül.

Amint láthatjátok itt az összes olyan helyen, ahol valamelyik paraméter rvalue referencia, a visszatérés is rvalue referencia. Ezekben az egyik rvalue-t módosítjuk, így nem kell újat létrehozni, és időt spórolunk meg. A végén pedig ezt az objektumot adjuk vissza. Az előző tesztünk folytassuk az összeadás operátor meghívásával:

    cout << "\nMost lefoglalunk negy lvalue-t.\n";
    MovableStore x1, x2, x3, x4;
    cout << "Es az osszeguket az m1-be tesszuk.\n";
    m1 = x1 + x2 + x3 + x4;
    cout << "\nMost negy rvalue osszeget rakjuk az m1-be.\n";
    m1 = MovableStore(40) + MovableStore(40) + MovableStore(40) + MovableStore(40);
    cout << "\nVege.\n\n";
Itt két esetet vizsgálok, az egyik, ahol lvalue-kat ad össze, a másik, ahol rvalue-kat.

Most lefoglalunk negy lvalue-t.
Default konstruktor: size = 32, 0x0024FC20
Default konstruktor: size = 32, 0x0024FC18
Default konstruktor: size = 32, 0x0024FC10
Default konstruktor: size = 32, 0x0024FC08
Es az osszeguket az m1-be tesszuk.
lvalue ref - lvalue ref osszeadas operator: 0x0024FC20 + 0x0024FC18
Copy konstruktor: size = 32, 0x0024FC20 -> 0x0024FBF0
rvalue ref - lvalue ref osszeadas operator: 0x0024FBF0 + 0x0024FC10
rvalue ref - lvalue ref osszeadas operator: 0x0024FBF0 + 0x0024FC08
Move ertekadas operator: size = 32, 0x0024FBF0 -> 0x0024FBF8
Destruktor: size = 0, 0x0024FBF0

Most negy rvalue osszeget rakjuk az m1-be.
Konstruktor: size = 40, 0x0024FC40
Konstruktor: size = 40, 0x0024FC38
Konstruktor: size = 40, 0x0024FC30
Konstruktor: size = 40, 0x0024FC28
rvalue ref - rvalue ref osszeadas operator: 0x0024FC28 + 0x0024FC30
rvalue ref - rvalue ref osszeadas operator: 0x0024FC28 + 0x0024FC38
rvalue ref - rvalue ref osszeadas operator: 0x0024FC28 + 0x0024FC40
Move ertekadas operator: size = 40, 0x0024FC28 -> 0x0024FBF8
Destruktor: size = 0, 0x0024FC28
Destruktor: size = 0, 0x0024FC30
Destruktor: size = 0, 0x0024FC38
Destruktor: size = 0, 0x0024FC40

Vege.

Destruktor: size = 32, 0x0024FC08
Destruktor: size = 32, 0x0024FC10
Destruktor: size = 32, 0x0024FC18
Destruktor: size = 32, 0x0024FC20
Destruktor: size = 0, 0x0024FC00
Destruktor: size = 40, 0x0024FBF8
Az lvalue-k összeadásánál először le kell másolnunk az egyik lvalue-t, már őket nem változtathatjuk meg. Majd ebben az új ideiglenes objektumban szépen összeadogatjuk az értékeket, és a végén átmove-oljuk a célhelyre.

Az rvalue-k összeadásánál nincs szükség másolásra, az egyikben szépen összeadogatja az értékeket, majd a végén a célhelyre move-olja.

A vége után láthatjuk még a négy lvalue, valamint m1 és a kiürült m2 destruktorát.

 

Szólj hozzá!

2010.01.07. 20:31 koszperv

Új autó jól seper

Címkék: auto cpp cpp0x

A C++0x-ben az auto kulcsszónak új értelmet adtak. Annyira, hogy a régi értelmében már nem is szabad használni. Tehát ez tilos:

auto int r;

Az új értelme az, hogy megpróbálja kitalálni milyen típust is akarunk írni. Így sok esetben helyet spórolunk meg magunknak, mivel az auto csak 4 karakter, míg a típus tipikusan hosszabb. Változó inicializálásakor az auto a típus helyett áll:

auto x = 5;
const auto *v = &x, u = 6;
auto x = {1, 2};
Megjegyzés: egy auto szó csak egyetlen típust helyettesíthet. Így a példában v és u esetén is intet jelöl. Ha 6.0 állna a végén, akkor hibát jelezne a fordító.

Szerepelhet new után is. Itt az auto automatikusan meghatározza a mögötte zárójelben lévő kifejezés típusát, és olyan objektumot hoz létre. A new egy pointert ad vissza arra az objektumra:

auto x = new auto('a');   
A szabványban nem láttam semmi arra utaló megjegyzést, hogy a zárójelben lévő kifejezést végrehajtja-e, illetve hogy mivel fogja inicializálni a memóriát. De a Visual Studioban elég egyértelműen végrehajtódik a kifejezés, és a végeredményével töltődik fel a lefoglalt terület.

Ezen felül az auto használható még az újfajta függvénydeklarációra is (ami egyébként erősen hajaz a lambda-függvények alakjára):

auto add(int a, int b) -> int
{
    return a + b;
}

Amint láthatjuk a paraméterlista után áll a visszatérési érték típusa. Itt a visszatérési értékre típusára mindig szükség van a lamba-függvényekkel ellentétben. És hogy ez az alak mire jó? Arról majd a perfect forwardingnál.

Megjegyzés: A VS2010 bétáját próbálgatva még elég sok dolgot hibának vél szerkesztés közben, és ezért aláhúzza őket. De lefordítani le tudja.

Lapzárta után érkezett: Azért az dolog nem fenékig tejfel. Egy jelentős problémáról megfeledkeztek: az autót nem lehet template paraméterként használni. Ez pedig baj. Mert hogy az újonnan bevezetett smart pointerek, shared_ptr és társai, a típust a template paraméterükbe kérik. Vagyis a kód csak hosszabb lessz: 

shared_ptr<auto> sx(new string("Hello"));
shared_ptr<string> sx(new string("Hello"));

Még valami: Bár iterátorciklusoknál nagyon jól jön az, hogy nem kell kiírni az iterátor hosszú nevét, akad egy kis gond: a begin mindig csak nem konstans iterátort ad vissza. Így ha mi konstans iterátort szeretnénk, akkor megintcsak ki kéne írni a teljes nevet. Ezért találták ki a cbegin metódust, ami csak abban különbözik a begin-től, hogy konstans iterátort ad vissza. Természetesen a párja a cend. Van belőle fordított irányba működő is: crbegin és crend. (Halmozzuk a betüket és az élvezeteket. Remélem a jövőben nem folytatják az egy szó - egy betű rövidítést.) A ciklus végén az cend és a crend felcserélhetők end-del és rend-del. Teljes kompatibilisek, amennyire én tudom.

    vector<int> vx;
    for (int i = 0; i < 10; ++i) vx.push_back(i);
    for (auto i = vx.cbegin(); i != vx.cend(); ++i)
        cout << *i << " ";
    cout << endl;
    for (auto i = vx.cbegin(); i != vx.end(); ++i)
        cout << *i << " ";
    cout << endl;
    for (auto i = vx.crbegin(); i != vx.crend(); ++i)
        cout << *i << " ";
    cout << endl;
    for (auto i = vx.crbegin(); i != vx.rend(); ++i)
        cout << *i << " ";
    cout << endl;

Szólj hozzá!

2010.01.06. 00:28 koszperv

[](){}();

Igen, ez egy szabályos kifejezés C++0x-ben. Mégpedig egy üres lambda-függvény meghívását takarja. A lambda-függvények az egyik legfontosabb újítás a C++0x-ben az rvalue-referenciák és az inicializálások automatikus típusegyeztetése mellett.

Egy lambda-függvényt mindig egy [] párral kezdünk. Ebben vannak az elfogott változóink felsorolása. Az elfogás azt jelenti, hogy a külső függvény egyik lokális változóját elérhetőve tesszük a lambda-függvény számára. Ez kétféleképpen történhet meg: érték szerint, és referencia szerint. A változókat egyszerűen vesszővel elválasztva soroljuk fel a szögletes zárójelek között. Amelyik változót referencia szerint adunk át, az elé tegyünk egy & jelet. Így:

[a, &b, c]Itt az a és b értékszerint adódik át, a b referenciaszerint. Mivel a lambda-függvény valójában egy teljesen szabályos functor objektum, ezek a változók a tagváltozói lesznek. Ha nem akarunk vacakolni a felsorolással, használhatjuk az = és a & szimbólumokat is automatikus elfogáshoz. Az = minden nem felsorolt változót érték szerint fog el, a & minden nem felsorolt változót referencia szerint.

[&, a, c]Ez a kód az a és c változókat érték szerint fogja el, az összes többit referencia szerint. Globális változókat a lambda-függvény automatikusan látja, így azokat nem kell elfogni. Tagváltozókat pedig nem lehet, viszont a thist el lehet fogni, igaz, csak érték szerint. A = és & szimbólumok automatikusan elfogják a thist, és mindig érték szerint. Ha pedig van this, akkor ugyanúgy használhatjuk a tagváltozókat a lambda-függvényben, mint a szülőfüggvényben. Ez azt jelenti, hogy látja a private és protected tagváltozókat is!

Ezután jön a paraméterlista. Ez egy teljesen szokványos paraméterlista. Ha nincs egyetlen paraméter sem, akkor a paraméterlista a zárójeleivel együtt elhagyható. A paraméterlistát követheti a visszatérési értek típusa egy -> jelet követően. Így:

(int x, int y) -> floatA visszatérési érték típusát csak akkor kell megadni, ha több return is van a függvényben. Egyetlen return esetén a visszaadott érték típusát képes automatikusan meghatározni. Ha visszatéresi érték típusát megadjuk kötelező kitenni a paraméterlistát, még akkor is, ha az üres.

Ezután jöhet a lambda-függvény utolsó része, a kódja. Ez a szabályos függvénytörzs. Ha értéket akarunk visszaadni, akkor használhatjuk a returnt.

Ahogy már említettem, a lambda-függvény valójában egy teljesen szabályos functor. Vagyis a függvény maga a () operátor lesz benne. Ráadásu abból is a const verzió. Ez pedig azt jelenti, hogy azokat az elfogott változókat, amiket érték szerint fogtunk el, nem tudja megváltoztatni. A referencia szerint elfogottakat viszont igen. Ha mégis meg akarjuk őket változtatni, akkor a függvény törzse elé a mutable kulcsszót kell írni. Így a nem const () operátort fogja létrehozni, és a functor tagváltozói mind írhatók lesznek. Persze egy pointer által mutatott értékre nem vonatkozik a constság, így azt nyugodtan átírhatjuk mutable nélkül is. Ugyanez áll az elfogott this által mutatott tagváltozókra.

void mul(vector<int>& v, int factor)
{
    for_each(v.begin(), v.end(), [factor](int &value){value *= factor;});
}
Ez a kód példáulk megszorozza egy vektor összes elemét factorral.

 

Szólj hozzá!

2010.01.03. 20:12 koszperv

boost::shared_ptr mc = gcnew my_class(); avagy a menedzselt osztály esete a smart pointerrel

Címkék: c# cpp interop shared ptr

Na azért ennyire én se vagyok perverz. De ma a .NET-es C++ interopról lesz szó. Az alapjait mindenki elsajátíthatja az MSDN-en. Ma inkább arról az igen fontos problémáról lesz szó, ami onnan kimaradt. Vagyis arról, hogyan burkoljunk be C++-os osztályokat menedzselt osztályokkal.

Bár látható egy példa erre az MSDN-en, sajnos az csak akkor működik, ha mi hozhatjuk létre new-val, vagy egyéb generáló függvénnyel a natív objektumot. Ez viszonylag egyszerű: a konstruktorban létrehozzuk a natív osztályt, eltároljuk a natív objektumra mutató pointert, a destruktorban és a finalizerben pedig delete-tel töröljük a natív objektumot. Azért kell destruktort és finalizert írni, mert a destruktor az IDisposable Dispose metódusának felel meg. Ha ezt elfelejtjük meghívni, akkor szép kis memory leakünk lesz. Ezt idővel a finalizer meg tudja szüntetni. Szóval egyfajta óvintézkedés. Különösen fontos, ha nemcsak mi használjuk ezeket az osztályokat, hanem mások is hozzáférhetnek kívülről. (Megjegyzés: A C#-os Dispose-zal ellentétben itt automatikusan elnyomódik a finalizer. Vagyis a destructor és a finalizer közül mindig csak az egyik fog meghívódni.)

ref class MyClass
{
private:
    my_class *ptr;
    void DeleteObject() { delete ptr; ptr = NULL; }
public:
    MyClass() { ptr = new my_class; }
    ~MyClass() { DeleteObject(); }
    !MyClass() { DeleteObject(); }
};
Azonban az esetek többségében nem hozhatunk létre szabadon objektumokat egy C++ API-n, hanem inkább lekérdezzük őket. Mondjuk egy ilyen függvénnyel: MyClass* getCurrentMyClass(). Az így visszakapott pointert is be kell burkolnunk valamilyen osztállyal, azonban ez az osztály jelentősen különbözni fog az imént bemutatottól. Előszöris nem hoz létre új objektumot, nem törli a natív objektumot, vagyis az élettartama nem kötődik a natív objektum élettartamához, és tetszőlegesen sok kapcsolódhat ugyanahhoz a natív objektumhoz. Vagyis ez az osztály egyfajta referencia lesz. Célszerű ha a nevének a végére teszünk egy Ref toldalékot is, hogy ezt jelezzük. Ez azért is célszerű, mert lehetnek olyan natív objektumok, amikre mindkét fajta menedszelt osztályt meg kell írni.

class my_class
{
private:
    int a;
public:
    void print() { cout << a << endl; }
    void increment() { ++a; }
    void setA(int p_a) { a = p_a; }
    int getA() { return a; }
};

ref class MyClassRef
{
private:
    my_class *ptr;
public:
    MyClassRef(my_class *p) { ptr = p; }
    void Increment() { ptr->increment(); }
    property int A
    {
        void set(int a) { ptr->setA(a); }
        int get() { return ptr->getA(); }
    }
};
Ha pontosan meg tudjuk mondani, hogy egy natív objektum mikor jön létre és mikor pusztul el, akkor elméletileg nincs szükség külön Ref osztályra. Mert hogy tarthatunk egy Dictionaryt vagy mapet, ami minden elő pointerhez a megfelelő menedzselt burkolóobjektumot rendeli hozzá. Sajnos általában nem áll fenn ez az eset, így most maradunk a Ref osztályoknál.

Nem esett szó arról, hogy C++-ban létezik konstság is. Ami viszont a C#-ban nincsen meg, így csak szimulálhatjuk azt.

1. módszer: Hagyjuk figyelmen kívül. Ez a legegyszerűbb, de a legveszélyesebb is. Ilyenkor nem várt működés történhet, ha egy konstans objektumot módosítunk, hiszen a C++ API nincs rá felkészülve. Ha pedig nem írható memóriában van a konstans objektum, akkor a program elszáll.

2. módszer: Dobjunk kivételt, ha az adott menedzselt objektum natív konstans objektumra mutat. Ennek a módszernek a hátránya, hogy a usert fogja szórakoztatni azzal a hibával, amit a mi hanyag programozásunk okozott. Mivel hogy konstansba beleírni az mindig programozási hiba, sosem adat- vagy userhiba. Úgyhogy ehhez kiterjedt tesztelés kell.

3. módszer: Csináljunk kétfajta menedzselt osztályt. Egyet a konstans objektumoknak, egyet meg a változtathatóaknak. Ilyenkor jó ötlet, ha a változtatható osztályt a konstansból származtatjuk le. Hiszen így nem kell megírni duplán a konstans függvényeket. Valamint azokkal a függvényekkel sem lesz gond, amik a paraméterként kapják be a konstans objektumot. Mert ezeknek működniük kell a változtatható objektumokra is, vagyis a változtatható objektumoknak implicit módon kasztolhatónak kell lenniük a konstans objektumokra. Ezt vagy operátorral érhetjük el, vagy leszármaztatással. A leszármaztatás egyszerűbb és olcsóbb is, mert nem kell létrehozni új objektumot hozzá.

4. módszer: Lehetséges, hogy csak szigorú előírásaink vannak, amik csak egy-két speciális osztály publikálását engedélyezik. Ilyenkor hasonlóan járhatunk el, mint a 3. módszerben, csak nem osztályokat kell csinálnunk, hanem interface-eket. Elég egyetlen osztály megírnunk, ami implementálja a konstans és a változtatható interfészt is.

class my_class
{
private:
    int m_number;
    string m_string;
public:
    void print() const { cout << m_number << ":" << m_string << endl; }
    void setNumber(int n) { m_number = n; }
    int getNumber() const { return m_number; }
    void setString(const string& s) { m_string = s; }
    void getString(string& s) const { s = m_string; }
    void increase() { ++m_number; }
};

public interface class IConstMyClass
{
    property int Number
    {
        int get();
    }
    property String^ Str
    {
        String^ get();
    }
    void Print();
};

public interface class IMyClass: IConstMyClass
{
    property int Number
    {
        void set(int);
    }
    property String^ Str
    {
        void set(String^);
    }
    void Increase();
};

public ref class MyClassref: IMyClass
{
private:
    my_class *ptr;
public:
    MyClass(my_class *p)
        : ptr(p)
    {
    }
    MyClass(const my_class *p)
        : ptr(const_cast<my_class*>(p))
    {
    }
    virtual void Print() { ptr->print(); }
    virtual void Increase() { ptr->increase(); }
    property int Number
    {
        virtual int get() { return ptr->getNumber(); }
        virtual void set(int a) { ptr->setNumber(a); }
    }
    property String^ Str
    {
        virtual String^ get()
        {
            string s;
            ptr->getString(s);
            return gcnew String(s.c_str());
        }
        virtual void set(String^ s)
        {
            using namespace Runtime::InteropServices;
            const char* chars = (const char*)(Marshal::StringToHGlobalAnsi(s)).ToPointer();
            ptr->setString(chars);
            Marshal::FreeHGlobal(IntPtr((void*)chars));
        }
    }
};
Azonban van ezzel még egy kis probléma. Mégpedig ez:

class node
{
public:
    node *getFirstChild();
    const node *getFirstChild() const;
};
A két függvény teljesen megegyezik, csak a konstságukban térnek el. Ezt nem tudjuk másképp megoldani, minthogy más-más nevet adunk a két függvénynek a menedzselt osztályokban. Az egyik lehet mondjuk GetFirstChild, a másik meg GetFirstChildConst. Bár ez nem valami szép megoldás.

Most meg jöjjön a nap fénypontja, a smart pointer. Igazából ez csak elsőre tűnik nehéznek, valójában ugyanolyan könnyű mint a sima pointer. Simán be kell tenni a menedzselt osztályba a smart pointert, és az ugyanúgy fog működni, mintha egy natív osztályban lenne. Egy kis probléma persze van a dologgal: menedzselt osztályban nem lehet natív típus. Így a heapen kell neki helyetfoglalni a konstruktorban, és törölni a destruktorban. De azért ne feledjük, hogy ez az osztály is csak egy ilyen Ref osztály, valójában a célobjektumot se nem hozza létre, se nem törli. Valamint azt se, hogy így egy pointer pointerét fogja tartalmazni az osztályunk, tehát valahogy így kell majd hivatkoznunk a tagokra: (*ptr)->.

public ref class MyClass2Ref: IMyClass
{
private:
    shared_ptr<my_class> *ptr;
    void Free()
    {
        delete ptr;
        ptr = NULL;
    }
public:
    MyClass2Ref(my_class *p)
        : ptr(new shared_ptr<my_class>(p))
    {
    }
    MyClass2Ref(const my_class *p)
        : ptr(new shared_ptr<my_class>(const_cast<my_class*>(p)))
    {
    }
    virtual void Print() { (*ptr)->print(); }
    virtual void Increase() { (*ptr)->increase(); }
    property int Number
    {
        virtual int get() { return (*ptr)->getNumber(); }
        virtual void set(int a) { (*ptr)->setNumber(a); }
    }
    property String^ Str
    {
        virtual String^ get()
        {
            string s;
            (*ptr)->getString(s);
            return gcnew String(s.c_str());
        }
        virtual void set(String^ s)
        {
            using namespace Runtime::InteropServices;
            const char* chars = (const char*)(Marshal::StringToHGlobalAnsi(s)).ToPointer();
            (*ptr)->setString(chars);
            Marshal::FreeHGlobal(IntPtr((void*)chars));
        }
    }
    ~MyClass2Ref()
    {
        Free();
    }
    !MyClass2Ref()
    {
        Free();
    }
}
Az osztály meghívása: IMyClass^ mc = gcnew MyClass2(new my_class);
mc->Number = 5;
mc->Str = "Hello!";
mc->Print();
A shared_ptr egyébként ma már nemcsak a boostban érhető el, hanem az std::tr1-es névtérben is a <memory> header file-ból. Visual Studioban ez már egy ideje benne van, GCC-ben be kell kapcsolni valamilyen kisérleti módot, hogy látható legyen.

 

Szólj hozzá!

2010.01.02. 12:10 koszperv

Csak párhuzamosan!

Címkék: intel programozás cpu párhuzamos programming gcc gpu cuda multitasking multithreading concrt tbb openmp

Az idei PDC-n elég sok szó esett a párhuzamos programozásról, legfőképpent a Microsoft új libraryjéről, a concurrency run-time-ról, vagyis a ConCRT-ről. Ezért úgy gondoltam végzek egy kis tesztet ezzel a még meg sem jelent libraryvel, valamint a piacon lévő egyéb ingyenes megoldásokkal. A teszt magja az a prímszámkeresés, amit a PDC-n is használtak. Sajnos azonban az gépemben nincs 4 magos CPU, csak 2 magos, így a gyorsulás is ennek megfelelően 2 alatti. De nem csak a CPU-n teszteltem a párhuzamos libraryk teljesítményét, hanem az nVidia CUDA-jával a videókártyámon is. Ez utóbbi 14 multiprocesszort tartalmaz, azaz egyszerre 112 hardverthreadet tud futtatni. A konfiguráció: Intel Core2 Duo E8400 3.0 GHz, memória: DDR2 1 GHz, videókártya: 8800GT core clock: 600 MHz, Memória: 900 MHz, shader: 1500 MHz. Lássuk hát az eredményeket:

Párhuzamosítás
Használt fordító és libraryEgy thread idejeIdő több threadenGyorsulás
MSVC 10.0 Beta 2 Express
ConCRT
17.4297 s12.099 s1.44058
MSVC 9.0 Express
Intel TBB 2.2
16.6818 s9.1218 s1.82878
MSVC 10.0 Beta 2 Express
Intel TBB 2.2
17.407 s9.16248 s1.89981
GCC 4.4.2
OpenMP 3.0
22.749 s12.683 s1.79366
MSVC 9.0 Express
CUDA 1. módszer
17.9816 s
17.6502 s
6.37564 s
6.91229 s
2.82035
2.55346
MSVC 9.0 Express
CUDA 2. módszer
17.5794 s
17.4557 s
5.8124 s
6.44076 s
3.02446
2.7102

 

 

Mind release-ben futott default beállításokkal. GCC-nél bekapcsoltam a sebességoptimalizációt, ami MSVC-nél alapból be van kapcsolva, de ez sem segített rajta. A mérés egy 30000000-s egésztömbön zajlott, amibe meghatározott sorrendbe 1 és 50000 közötti számokat tettem. Majd ezt 10-szer lefuttattam. Először egszer lefuttattam a párhuzamos verziót, hogy az esetleges indulási jelenségek, mint például a library inicializálása, ne számítson bele a mérésbe. Majd lefuttattam egy sima 1 threades verziót a CPU-n. És végül a párhuzamos verziót. A gyorsulás egyszerűen a két idő hányadosa.

Amint látható a gyorsulás az Intel Threading Building Blocks és az OpenMP esetén 80% körüli, ami igen jó. Ellentétben a ConCrt-vel, ahol csak 40% körüli gyorsulást sikerült elérni. Jól látható még, hogy a GCC eleve hátrányból indul, egyszerűen gyengébben optimalizálja a kódot. De még így sikerül beérnie a ConCrt-t alig egy másodpercre. Ez olyan, mint amikor a Forma 1-ben két pilóta eszeveszett küzdelmet folytat az utolsó pontszerző helyért. Kemény verseny, de a két pilótán kívül senkit nem érdekel. Az is jól látszik, hogy a CUDA tetemes előnyre tett szert, jóval gyorsabb, mint a CPU-n futó tesztek. Azért engebb nem nyűgözött le, általában azt szokták mondani, hogy a GPU-k teljesítménye egy nagyságrenddel nagyobb a CPU-knál. Hát itt ez most nem áll meg. Ez úgy 56% 40%-kal gyorsabb csak a leggyorsabb CPU-s tesztnél. (A nagyobb értékek túlhúzott GPU-nál jelentkeztek. A túlhúzott GPU sebessége ez volt: core: 700 MHz, RAM: 1010 MHz, Shader: 1750 MHz) Kétféle CUDA teszt van, az első módszerben egyszerűen párhuzamosítottam a kódot: minden thread egy-egy számot tesztel. Mivel 32-esével kötegelve vannak a threadek, ez azt jelenti, hogy ha egy számot hosszú ideig tesztel, az 31 másik threadet visszafog. Ezért azt csináltam a második módszerben, hogy 32 threadenként tesztel egy számot. Így csak az utolsó menetben lehet némi üresjárat. Láthatóan gyorsabb is.

Nos, a ConCRT nem győzött meg. Először is lassú (legalábbis alapbeállítások mellett), másodszor is kizárólag Visual Studioval lehet használni. Ez pedig azt jelenti, hogyha a távoli jövőben a programunkat szeretnénk esetleg más platformokra is átültetni, nem célszerű használni. Olyan egyszerű eseteknél mint ez is, ahol csak pár threadre van szükség, nem lehet gond az átírás, de amikor már taszkokat kezdünk készíteni, aszinkron ágensek küldözgetnek jobbra-balra üzeneteket, akkor már komoly gondban leszünk. Tehát jól meg kell fontolni a használatát. Mármint akkor, ha release-re sikerül begyorsítaniuk. (Bár persze az is lehet, hogy az express verzió miatt volt lassítva, ki tudja.)

Gondolom többeket érdekel a kód is. Nos, itt jönnek.

Először is egy prím tesztelése a CPU-n így történt:

bool isPrime(int x)
{
    if (x <= 2) return x == 2;
    if (x % 2 == 0) return false;
    int sqrtx = static_cast<int>(ceil(sqrt(static_cast<double>(x))));
    for (int i = 3; i <= sqrtx; i += 2)
        if (x % i == 0) return false;
    return true;
}
Ez gyakorlatilag megegyezik a PDC-n látott kóddal. Ezután a VS2008-as Intel TBB kód:

class PrimeTestFunctor
{
private:
    int *src;
    bool *result;
public:
    PrimeTestFunctor(int *p_src, bool *p_result):
      src(p_src), result(p_result)
      {
      }

    void operator()(const blocked_range<size_t>& r) const
    {
        for (size_t i = r.begin(); i != r.end(); ++i)
            result[i] = isPrime(src[i]);
    }
};

void parallelPrimeTest(int *src, bool *result, size_t length)
{
    parallel_for(blocked_range<size_t>(0, length), PrimeTestFunctor(src, result));
}

A 2008-as Visual Studioban még functort kellett alkalmazni a TBB használatához. De a 2010-esben már lehet lambdafüggvényt is. Voilà:

void parallelPrimeTest(int *src, bool *result, size_t length)
{
    parallel_for(blocked_range<size_t>(0, length),
        [&] (const blocked_range<size_t>& r)
        {
            for (size_t i = r.begin(); i != r.end(); ++i)
                result[i] = isPrime(src[i]);
        });
}
Ugye mennyivel rövidebb? És végül a concurrency runtime-mal így néz ki a kód:

void parallelPrimeTest2(int *src, bool *result, size_t length)
{
    parallel_for(static_cast<size_t>(0), length,
        [&] (size_t i)
        {
            result[i] = isPrime(src[i]);
        });
}
Az 1. CUDA módszer:

__device__ bool isPrime(int x)
{
    if (x <= 2) return x == 2;
    if (x % 2 == 0) return false;
    int sqrtx = static_cast<int>(ceilf(sqrtf(x))); // int sqrtx = static_cast<int>(__fsqrt_ru(x));
    for (int i = 3; i <= sqrtx; i += 2)
        if (x % i == 0) return false;
    return true;
}

__global__ void checkPrime(int* source, int* destination)
{
    int index = threadIdx.x + blockIdx.x * blockDim.x;
    destination[index] = isPrime(source[index]);
}
Ebből a checkPrime függvény hívható a CPU-ról, a másik belső függvény, ami nagy valószínűséggel inline-osodik. A 2. CUDA módszer kicsit bonyolultabb:

const int numbersPerBlock = 16;
const int blockSize = numbersPerBlock * 32;
__shared__ volatile int values[numbersPerBlock];
__shared__ volatile int numbers[numbersPerBlock];

__global__ void checkPrime2(int* source, int* destination)
{
    // Kiszamitjuk az offszeteket.
    int sharedOffset = threadIdx.x / 32;
    int globalOffset = blockIdx.x * numbersPerBlock + sharedOffset;

    // Beolvassuk a szamot.
    int thread = threadIdx.x % 32;
    if (thread == 0)
        values[sharedOffset] = source[globalOffset];
    int x = values[sharedOffset];

    // Jelezzuk, hogy a szamrol meg lehet, hogy prim.
    if (thread == 0)
        numbers[sharedOffset] = 1;

    // Vegigmegyunk a lehetseges osztokon.
    if (x <= 2)
    {
        if (thread == 0)
            destination[globalOffset] = x == 2 ? 1 : 0;
        return;
    }
    if (x % 2 == 0)
    {
        if (thread == 0)
            destination[globalOffset] = 0;
        return;
    }

    int sqrtx = static_cast<int>(ceilf(sqrtf(x))); // int sqrtx = static_cast<int>(__fsqrt_ru(x));
    for (int i = 3; i <= sqrtx; i += 64)
    {
        if (((i + thread * 2) > sqrtx) || (numbers[sharedOffset] == 0))
            break;
        else if ((x % (i + thread * 2)) == 0)
            numbers[sharedOffset] = 0;
    }

    // Kiirjuk az eredmenyt.
    if (thread == 0)
        destination[globalOffset] = numbers[sharedOffset];
}
Itt jól látható, hogy a shared memóriát aktívan használja, hogy egy warpon belül a threadek jelezzenek egymásnak. A volatile azért szükséges, hogy mindig a memóriához férjen hozza, ne optimalizálja ki regiszterekbe a változót.

A jövőben fogok még egy mérést végezni. Az a Conway-féle életjáték sebességét fogja tesztelni. Remélhetőleg az már jobban áll a GPU-nak. De ott bejön az SSE2 is konkurrenciának.

 

3 komment

2010.01.01. 00:23 koszperv

2010

Vége a C++0x-nek! Éljen a C++1x!

:)

 

Szólj hozzá!

2009.12.26. 18:46 koszperv

A határ a csillagos ég, de maximum 2 GB

Címkék: stream streamoff fstream ifstream ofstream

Emlékeztek még, amikor egy file nem lehetett nagyobb, mint 2 GB? Nem, nem a FAT32-re gondolok. Hanem a páréves forditókra, amikben a streamoff típus 32 bites előjeles szám volt. Ezáltal az STL képtelen volt kezelni 2 GB-nál nagyobb file-okat. Pedig ekkora file-ok ma már teljesen természetesek.

GCC-ben ez az időszak állítólag a 3.3-as verzióig tartott, de nekem még tavaly is volt bajom vele. Persze, lehet, hogy éppen egy 3.3-assal. Mindenesetre a 4.4.2-esben leteszteltem, és az már egészen biztosan 64 bites streamoffot használ. Visual Studioban az első verzió, amelyikben lecserélték 64 bitesre, az a 10-es. (64 bites platformon már 64 bites a streamoff egy ideje, de asszem a 32 bites kódok kicsit még gyakoribbak.) Sajnos ez még béta 2-es fázisban van, csak jövőre jön ki. Így az MSVC felhasználók még szívhatnak vele egy ideig.

 

Szólj hozzá!

2009.12.26. 17:30 koszperv

static - nem static

Címkék: static namespace

A legújabb C++ szabvány szerint a static kulcsszót többé nem lehet alkalmazni globális változókra és függvényekre. Ha jól tudom, az indoklás az, hogy így már túl sok értelme lenne a static szónak. Hát, fogjuk rá. De az ajánlott helyettesítése se jobb nála: ugyanis a névtelen névteret kell használni helyette! Mi lehet logikátlanabb egy névtelen névtérnél? Nos, magyarán szólva ezt a kódot:

static int a;

Erre kell cserélni:

namespace
{
    int a;
}

A névtelen névtérben deklarált és definiált dolgok a névteren kívülről is elérhetők mindenféle kettőspontozás nélkül, viszont más forrásfile nem látja őket. Tehát ugyanazt csinálja, mint amit a static csinált. Hát sok értelme nem volt.

Megjegyzem egyetlen általam ismert fordító sem ad rá warningot.

 

Szólj hozzá!

2009.12.26. 13:46 koszperv

const vs define

Címkék: konstans constant const define #define

Már az ősidőktől kezdve folyik a harc a define-nal és a consttal létrehozott konstansok használata között. Az előbbi egy C örökség, benne a preprocesszálás problémáival, az utóbbi meg túlságosan is új a régivágású C-ről áttért programozóknak. A define mellett szól, hogy aki használja, az pontosan tudja, hogy azt a fordító hogyan fogja felhasználni. Így gyakorlatilag biztos lehet benne, hogy a konstans inline-osodik, és nem történik majd szükségtelen memóriahozzáférés. Valamint a megszokás is ezt a módszert támogatja. A constról viszont azt állítják, hogy szintén inline-osodik, ha tud, bár kötelezően létrejön egy érték is a memóriában, hogy hivatkozásokat és pointereket lehessen rácímezni. Valamint a const képes objektumokat és tömböket is tartalmazni. (A define is, csak annak még korlátozottabb a felhasználási területe.)

Vagyis úgy látszik a constnak nincsenek hátrányai, előnyei azonban annál inkább. És ez a szabvány szerint így is van. De a fordítóprogramgyártók azért ügyeltek arra, hogy ne legyen minden fenékig tejfel. Ugyanis a const tartalma létrejön minden C++ file-ban, amiben szerepel. Tehát ha valaki csinál egy központi headerfile-t, és abba rak bele minden konstanst, akkor bizony keletkezik mindegyikből egy-egy példány forrásonként. Ezeket a példányokat vagy a fordítónak, vagy a linkernek össze kéne vonnia, mint ahogy ezt teszi a string literálokkal is, de mégsem teszi meg. Egyszerűen nem csinálták meg. A Visual Studio és a GCC úgyanúgy bent hagyja több példányban az értékeket. Amíg kicsi a programunk ezzel nincs is semmi baj, de amikor már több száz vagy több ezer forrásunk lesz, és több tucat, vagy akár száz konstansunk, akkor bizony már súlyos gondok lehetnek a program méretével. (Kicsit úgy viselkedik ez a const kulcsszó, mintha static lenne, csak éppen az értékét nem lehet megváltoztatni. Hupsz! A staticról a következő posztban bővebben.)

Lehet a problémán segíteni bizonyos megkötésekkel. Például, ha string konstanst akarunk létrehozni, akkor ne const char *-t vagy const char []-t használjunk, hanem std::stringet. Ekkor ugyanis csak egy üres string objektum jön létre háromszor, és a program inicializálásakor töltődik bele az érték. Mivel a string itt egy string literál, ezért az csak egyszer fog szerepelni a kódban. (Ez a módszer csak Visual Studioban működik, GCC-ben a string továbbra is többször fog szerepelni, mégha beállítjuk a hely optimalizálási opciókat is.) Másik módszer, ami nemcsak stringekre működik, hogy normál változókat használunk, és a header file-ban csak egy extern hivatkozás van rájuk. Ez a módszer azonban még a define-nál is rosszabb.

Akkor egy kis gyakorlat:

const std::string s = "Hello!";
const char c[] = "Haliho!";
const int a = 0x12345678;

#include <iostream>
#include <iomanip>
#include "header.h"

void f1()
{
    std::cout << "Address of s:" << std::hex << &s << " Address of c:" << reinterpret_cast<void*>(c) << " Address of a:" << &a << std::endl;
}
És csináljunk még két forrást f2 és f3 függvényekkel, amik pontosan ugyanígy néznek ki a nevükön kívül. Ha meghívjuk őket, látni fogjuk, hogy minden cím különbözik. A kódban egy darab Hello! lesz, valamint 3 darab Haliho! es 3 db 0x12345678.

Ez a probléma kizárólag azokat a constokat érinti, amik header file-okban vannak. És a igazából akkor okoz csak gondot, ha a projektünk elég nagy, vagy nagyon kevés helyünk van, mint mondjuk egy beágyazott rendszernél vagy telefonnál. Tehát ezekben az esetekben jobb, ha header file-okban define-t használunk const helyett, mert pillanatnyilag ez a legjobb megoldás. Legalábbis, amíg a fórdítógyártók észhez nem térnek.

 

 

3 komment

süti beállítások módosítása