C++ szál deklarációja lassitja a programot

C++ szál deklarációja lassitja a programot
2022-03-06T14:29:57+01:00
2022-03-07T21:37:01+01:00
2022-10-15T21:20:20+02:00
minicooper
Üdv. megint én vagyok :) 
Mi miatt van az hogy ha deklarálok egy új szálat azonos algoritmussal mint a main függvényé és még csak el sem inditom joinba vagy detach-be felére esik a main thread sebessége. Az összes szál deklarálása után a main függvény 1/40-edes sebességgel fut.

    thread c2 (compute,&x2);     #ennek a deklarációja kb felére rontja a sebességet
    thread c3 (compute,&x3);    #ennek (is) a deklarációja tovább rontja
    thread c4 (compute,&x4);
    thread c5 (compute,&x5);
    thread c6 (compute,&x6);

    cout<<setprecision(200);
    cout << "Program runing ..." << endl;

    v=a/b;
    b+=2;
    printer.detach();

    while(rf)
    {
        if(x%2)
        {
            v-=(a/b);
            b+=2;
            x++;
        }
        else
        {
            v+=(a/b);
            b+=2;
            x++;
        }
        //cout<<"V:"<<v<<endl;

    }
Mutasd a teljes hozzászólást!

  • és még csak el sem inditom joinba vagy detach-be

    Ezek std::thread-ek akarnak lenni? Csak mert azok a doksi szerint egyből a konstruktor hívása után indulnak, nem kell külön indító metódust hívni rájuk:

    Threads begin execution immediately upon construction of the associated thread object (pending any OS scheduling delays), starting at the top-level function provided as a constructor argument.

    Azt, hogy hat párhuzamos szál miért fut külön-külön lassabban, mint egy szál magában, annak több oka is lehet. Első tippem a false sharing lenne: az egyes szálak által használt változók egymás mellett vannak a memóriában, ezért folyamatos küzdelem folyik azért, hogy kinek legyen írási joga az adott cache line-ra.

    Több kód nélkül szerintem csak tippelgetni tudunk. Jó lenne látni a compute és az x1..x6 deklarációit.
    Mutasd a teljes hozzászólást!
  • A compute x1-x6 ugyanaz a compute függvény 6 külön saját globális. Itt a teljes kód. Ne itélkezzetek mivel nagyon amatőr hobbi szintű kódoló vagyok :) 
    A célom egyszerűen paralellizálni/klónozni a compute függvényt a saját változóival de csak a probléma van vele.
    Gondolom a kódban több hiba is van tehát nem sértődök meg ha azokra is tud valaki tanácsot adni :)

    #include <iostream> #include <windows.h> #include <thread> #include <iomanip> #include <string> #include <vector> #include <time.h> using namespace std; void count_cycle(); void compute(unsigned long long *y); unsigned long long x=3; unsigned long long x2=3; unsigned long long x3=3; unsigned long long x4=3; unsigned long long x5=3; unsigned long long x6=3; int main() { register long double v=0,vb; register long double a=4,b=1; int cx=0; register unsigned long long target; register bool rf=true; register long cnts; //long double cnt[100]; string s,spi="3.1415926535897932384626433832795028841971693993751058209749445"; long double pi=3.1415926535897932384626433832795028841971693993751058209749445; thread printer (count_cycle); thread c2 (compute,&x2); thread c3 (compute,&x3); thread c4 (compute,&x4); thread c5 (compute,&x5); thread c6 (compute,&x6); cout<<setprecision(200); cout << "Program runing ..." << endl; v=a/b; b+=2; printer.detach(); //c2.detach(); //c3.detach(); //c4.detach(); //c5.detach(); //c6.detach(); while(rf) { if(x%2) { v-=(a/b); b+=2; x++; } else { v+=(a/b); b+=2; x++; } //cout<<"V:"<<v<<endl; } return 0; } void count_cycle() { unsigned long long xb=0,xa=0; while (1) { //xb=x+x2+x3+x4+x5+x6; xb=x; Sleep(5000); //xa=x+x2+x3+x4+x5+x6; xa=x; cout<<(xa-xb)/5000000<<"MOPS/s"<<endl; } } void compute(unsigned long long *y) { register long double v=0,vb; register long double a=4,b=1; while(1) { if(*y%2) { v-=(a/b); b+=2; *y+=1; } else { v+=(a/b); b+=2; *y+=1; } //cout<<"V:"<<v<<endl; } }
    Mutasd a teljes hozzászólást!
  • Hát igen, vannak egyéb gondok is. Ne aggódj, a többszálú programozás nem egyszerű, mindenki belefut az elején ilyen dolgokba

    Először is nincs szinkronizáció, amikor ugyanazt a változót több szálból próbálod elérni. Ez így nem definiált működés, mert nem garantált hogy az egyik szál látni fogja a másik szálból végzett módosításokat szinkronizáció nélkül. Valószínűleg ezért van, hogy nálam MSVC-ben Release módban fordítva mindig 0MOPS/s-t ír, ezért Debug módban futtattam. A C++-hoz nem értek, nem tudom mik az elterjedt szinkronizációs módszerek, de minimum ezek ígéretesnek tűnnek: std::promise, std::atomic.

    A false sharing biztosan belekavar. Nálam ha kikommentelem az extra szálak indítását, 130-at mér. A te eredeti kódoddal 10-et. Ha átírom a számoló kódot úgy, hogy a saját vermében saját másolattal dolgozzon, akkor felugrik 65 és 80 közé (eléggé ingadozik):

    void compute(unsigned long long *y) { register long double v=0,vb; register long double a=4,b=1; unsigned long long copy = *y; while(1) { if(copy %2) { v-=(a/b); b+=2; copy +=1; } else { v+=(a/b); b+=2; copy +=1; } //cout<<"V:"<<v<<endl; } }
    Ha figyelembe veszem hogy négymagos CPU-m van, akkor nem kell meglepődni rajta hogy hat szálat nem bír olyan gyorsan futtatni, mint egyet, hiszen a hat szálnak négy magért kell versenyeznie. Ha átírom úgy, hogy összesen négy szál legyen (egy main, meg három std::thread), akkor 95 és 120 között ingadozik, ami azért már megközelíti a kívánt sebességet.

    Ha csak tesztelni akartad hogy hogyan is működik a párhuzamosítás, ennyi talán elég is lesz. Ha komolyabb számítást szeretnél végezni, ahol a szálaknak néha információt kell cserélnie, az már egy sokkal mélyebb téma, amit egy hozzászólásban nem lehet könnyen összefoglalni. A lényeg hogy a minimumra kell szorítani a szálak közötti kommunikációt illetve a közös erőforrásokért való versengést ahhoz, hogy jól skálázódjon a megoldás.
    Mutasd a teljes hozzászólást!
  • Az egyetlen szál amelyik hozzányúl a másik változójához a printer ami 5 másodpercneként próbálja meg elérni. A többi szál egymástól teljesen független tehát az egyetlen hely ahol a szinkron hiány fel léphet szerintem a 5 másodpercenkénti egyszeri elérés amit a printer csinál. De kétlem hogy ez az 5 másodpercenkénti hozzányúlás kétszerezné a lefolyási időt ... ha jól számolom akkor +5 másodpercet "büntetést" kellene hogy jelentsen a szinkron hiánya amit azért kétlek. Javits ki ha tévedek :)
    Mutasd a teljes hozzászólást!
  • De kétlem hogy ez az 5 másodpercenkénti hozzányúlás kétszerezné a lefolyási időt

    Légy szíves olvasd el az egész hozzászólásomat. Azt a részt is, ami a false sharing-ről szól. Ha valami nem világos, akkor kérdezz nyugodtan
    Mutasd a teljes hozzászólást!
  • Tényleg utána kell olvass pár dolognak, mint Csaboka is említette...Bár ezek talán kicsit túl mutatnak a kezdő-hobby programozók szintjén, de tanulni mindig jó.

    Pl. Az x86-64 architectúra egyik ilyen sarok köve, hogy minden mag rendelkezik egy 64byte-os un. cache line területtel, ez a legkisebb egység amit betölt a felette álló cache-ből. Ha azt látja a memória vezérlő, hogy valahol ezen a címterületen bármi változott a context switch alatt akkor kidobja és újra elő szedi azt a 64 byte-t.

    Esetedben a globális változóid egymás mellett vannak a memóriában ezért jó eséllyel egy-max kettő cache line-ba kerülnek majd, így bizony konkurálni fognak a szálak egymással az adott memóriáért, ez okoz majd lassulást. De érdekes lenne megvizsgálni mi zavar még be, mert ez max az L1-L2 cache szintjén játszódik le.

    Kíváncsi lennék ha az x változókra tennél egy alignas(64) módosítót mi lenne az eredmény.
    Mindenestre érdemes játszani ilyenekkel, utána olvasva sok minden kiderül a processzor lelki világáról.
    Innen már csak egy lépés megérteni a spectre és meltdown sebezhetőségeket.
    Mutasd a teljes hozzászólást!
Tetszett amit olvastál? Szeretnél a jövőben is értesülni a hasonló érdekességekről?
abcd