Hlavní navigace

Vlákno názorů k článku Rychlost CPythonu 3.11 a 3.12 v porovnání s JIT a AOT překladači Pythonu (2) od ZmatenejStrejda - Vyzkoušel jsem si kódy z repozitáře pana autora...

  • Článek je starý, nové názory již nelze přidávat.
  • 23. 11. 2023 22:39

    ZmatenejStrejda

    Vyzkoušel jsem si kódy z repozitáře pana autora a dovolím si rozebrat nebo doplnit některé informace z článku.
    (Určitě se podívejte na odstavec 4)

    Testoval jsem nativní implementaci v C a numba7 primárně na velikosti 4096 a 25500 iteracích.

    Stroje (všechny nastaveny 1 vlákno na jádro)
    1. 1x A8-7650K ~ 1x(4 jádra) @ 4.4GHz
    2. 2x Xeon E5-2620 v3 ~ 2x(6 jader) @ 2.40GHz
    3. 2x AMD EPYC 7452 ~ 2x(32 jader)

    1) Pokud chceme měřit skutečnou rychlost výpočtu, tak dle mého názoru, měřit celkový běh programu nedává moc smysl a mnohem přínosnější je měřit jen čas provedení výpočetní funkce, protože zápis na standardní výstup je celkem pomalý. A to například pomocí gettimeofday() z knihovny sys/time.h a pro python time.time() z knihovny time. Například na stroji 3. trvá nativní výpočet 3.514 sekund a tisk na stdout 2.771 sekund, takže IO prodlužuje dobu běhu programu o cca 78%. Je sice pravda že tam to zdržení je a bude, ale dle mého názoru není zajímavé dokud nebudeme optimalizovat IO.

    2) Ofast, Obad, aneb velmi rychlá střelba do nohy. -Ofast v nativní implementaci nepřináší téměř žádný další výkon oproti -O3. Navíc při dalším rozšiřování programu se dřív nebo později projeví chybným nebo nezvyklým chováním programu.
    Myslím že lepší volbou jsou například -mtune=native a -march=native

    3) Sice Amdahlův zákon nás omezuje, ale naštěstí je tu Gustafsonův zákon. Jednoduše řečeno, Amdahlův říká něco jako že pokud v programu je nějaká sekvenční část, tak více jader nepomůže. Gustafsonův zákon na to jde obráceně a říká něco ve smyslu, že pokud máš příliš mnoho jader, tak zvětši řešený problém a zachováš si tím zrychlení. Tyto zákony se objevují taky pod pojmy slabé a silné škálování.

    4) Výrok: "výpočet ve více vláknech v C lze realizovat mnohem složitěji" Není pravdivý. Již nějakou dobu je tu s námi knihovna OpenMP, která ve světě C/C++ působí jako zázrak, protože pomocí jednoduchého makra
    #pragma omp parallel for paralelizujeme naší smyčku. Pak stačí jen kompilovat kód s flagem -fopenmp a mít v systému balíček openmp. Nejjednodušší použití vypadá například takto

    #pragma omp parallel for
    for (int y = 0; y < height; y++) {

    což vypadá mnohem více zadarmo než

    @jit((int64, int64, int64, UniTuple(UniTuple(int64, 3), 256)), nopython=True, parallel=True)
    def calc_mandelbrot(width, height, maxiter, palette):
    Samozřejmě musíme stejně jako v případě numby vnořit proměnnou cy dovnitř prvního cyklu

    5) Zde je menší porovnání
    C ~ 1 jádro,
    OpenMP ~ všechny jádra,
    Numba ~ všechny jádra

    stroj     | 2048   4096
    __________|_____________________
    1. C      | 61.73  246.7
    1. OpenMP | 16.80  67.21
    1. Numba  | 32.55  130.9
              |
    2. C      | 67.52  269.8
    2. OpenMP | 6.951  27.68
    2. Numba  | 19.28  76.72
              |
    3. C      | 54.24  217.6
    3. OpenMP | 0.883  3.514
    3. Numba  | 2.800  11.10

    Na závěr bych chtěl dodat, že mě velice mile překvapilo kam až to python výkonově dotáhl.

  • 24. 11. 2023 8:36

    Pavel Tišnovský
    Zlatý podporovatel

    Díky za doplnění.

    Trošku jsem popravdě nepochopil bod 1 - to je přece tématem hned několika kapitol, aby to čtenáře upozornilo, že výsledky benchmarků jsou jedna věc a co měříme věc druhá. V další části článku se už I/O neprojevuje, ale JIT ano (naschvál, to nemůžu vyhodit, protože to vede ke špatným závěrům)

    2) Ofast tam je naschvál (zmíněno v článu), aby se ukázalo, že můžeme i "šidit" a obejít IEEE 754. Resp. naopak, aby to ukázalo, že všechny ostatní benchmarky produkují na pixel přesné výsledky a má smysl je porovnávat, ale tento konkrétní prostě cheatuje.

    Za 4 moc díky! Měl jsem to do článku dát, to je pravda.

    24. 11. 2023, 08:36 editováno autorem komentáře

  • 24. 11. 2023 18:31

    ZmatenejStrejda

    Ten bod 1 jsem myslel tak, že proč se snažit eliminovat vliv IO na dobu výpočtu, tím že ho zmenšíme, když ho nemusíme měřit vůbec. (myšleno v případě kdy nás zajímá pouze výkon výpočtů)

    Navíc i print může být ovlivněn například tím jak rychle někdo zpracovává data v z roury, do níž náš print cpe data. (Jsou lepší způsoby jak dostat data na disk než pomocí formátovaného výstupu)

  • 24. 11. 2023 8:54

    atarist

    OpenMP to tedy nejak "rozbalí", ale umožní přístup do stejného pole? Předpokládám, že bez dalších kontrol na problémy typu souběžné čtení, zápis atd.? (ale to podle všeho nezvládá ani Numba, jestli jsem to dobře pochopil).

  • 24. 11. 2023 8:57

    cc

    Problém openmp je hlavně ten, že paralelizuje jednu smyčku, pokud má člověk těch smyček 20 a člověk to takto použije, tak se to bude synchronizovat 20x - v takovém případě je vždycky lepší si udělat ten dispatch vlastní a vykašlat se na omp.

  • 24. 11. 2023 19:37

    ZmatenejStrejda

    To asi záleží jestli potřebujete paralelismus rychle a jednoduše, nebo jste ochotný si to udělat sám efektivněji.

    Navíc omp neumí jen hloupé rozbalování smyček ale i má nástroje na synchronizaci a něco jako vlastní obal vláken a mutexů. A kdo ví co dalšího.

    Zde jednoduchá ukázka vláken

    void mujprint(int k){
            usleep(k * 1000);
            printf("%d\n", k);
    }
    int main (int argc, char** argv){
            #pragma omp parallel //paralelní sekce
            {
                    #pragma omp master //pouze vlákno 0
                    {
                            mujprint(5);
    
                            #pragma omp task //spawn tasku
                            mujprint(4);
    
                            #pragma omp task
                            mujprint(3);
    
                            #pragma omp task
                            mujprint(2);
    
                            #pragma omp taskwait //počká na všechny tasky
                            mujprint(1);
                    }
            }
            return 0;
    }

    Výstup toho to programu je při kompilaci s -fopenmp

    5
    2
    3
    4
    1

    a s vypnutým OpenMP

    5
    4
    3
    2
    1

    Další výhodou použití OpenMP je možnost si zvoli při kompilaci jestli poběží paralelně, nebo ne. Bez přepínače -fopenmp se omp makra ignorují.

  • 25. 11. 2023 21:56

    cc

    Podle mě pokud kdokoliv sáhne po C++, tak chce právě tu efektivitu.

    Expresivita openmp je podle mě mizerná a upřímně já jako uživatel aplikace chci i možnost zvolit si kolik třeba některé operace v té appce použijou vláken. Prostě nechci aby nějaký průchod polem co má 1000 prvků hned vytížil všechny thready, které se pak budou hádat o cache lines sousedních elementů...

  • 26. 11. 2023 0:42

    ZmatenejStrejda

    Také by se dalo říci, že kdokoliv chce pravou nefalšovanou efektivitu, tak sáhne po assembleru.

    Expresivita openmp je podle mě dostačující a já jako uživatel jsem snad nikdy neviděl apku co by uměla podrobnější nastavení než prostě počet použitých vláken.

    OpenMP není o tom aby vše bylo absolutně perfektní a dokonalé. Je tu pro ty co jsou ochotni za jednoduchost zaplatit určitým snížením efektivity.

    Když něco programuji co není hotové a do 2 minut to mám rychlejší třeba 10x, tak mě ty 2 minuty tolik nebolí když pak musím něco předělat.

    Řešení na váš požadavek jsem teda z hlavy nedal a musel jsem to vyhledat a zde by mohlo být řešení, je tedy trochu expresivnější než bych sám chtěl, ale vypadá to, že funguje.

    #include <stdio.h>
    #include <omp.h>
    
    int main() {
      #pragma omp teams num_teams(1) thread_limit(2)
      {
        #pragma omp parallel for
        for(int i = 0; i < 8; ++i){
          int thread_id = omp_get_thread_num();
          printf("Hello from FOR_1 from thread %d in team %d on idx %d\n", thread_id, omp_get_team_num(), i);
        }
      }
      return 0;
    }
    Hello from FOR_1 from thread 1 in team 0 on idx 4
    Hello from FOR_1 from thread 1 in team 0 on idx 5
    Hello from FOR_1 from thread 1 in team 0 on idx 6
    Hello from FOR_1 from thread 1 in team 0 on idx 7
    Hello from FOR_1 from thread 0 in team 0 on idx 0
    Hello from FOR_1 from thread 0 in team 0 on idx 1
    Hello from FOR_1 from thread 0 in team 0 on idx 2
    Hello from FOR_1 from thread 0 in team 0 on idx 3

    Dokonce můžeme například zpracovat 2 smyčky zároveň, každou pomocí 2 vláken

    #include <stdio.h>
    #include <omp.h>
    
    int main() {
      #pragma omp teams num_teams(2) thread_limit(2)
      {
        if (omp_get_team_num() == 0)
          #pragma omp parallel for
          for(int i = 0; i < 4; ++i){
            int thread_id = omp_get_thread_num();
            printf("Hello from FOR_1 from thread %d in team %d on idx %d\n", thread_id, omp_get_team_num(), i);
          }
    
        if (omp_get_team_num() == 1)
          #pragma omp parallel for
          for(int i = 0; i < 4; ++i){
            int thread_id = omp_get_thread_num();
            printf("Hello from FOR_2 from thread %d in team %d on idx %d\n", thread_id, omp_get_team_num(), i);
          }
      }
      return 0;
    }
    Hello from FOR_1 from thread 1 in team 0 on idx 2
    Hello from FOR_1 from thread 1 in team 0 on idx 3
    Hello from FOR_1 from thread 0 in team 0 on idx 0
    Hello from FOR_1 from thread 0 in team 0 on idx 1
    Hello from FOR_2 from thread 0 in team 1 on idx 0
    Hello from FOR_2 from thread 0 in team 1 on idx 1
    Hello from FOR_2 from thread 1 in team 1 on idx 2
    Hello from FOR_2 from thread 1 in team 1 on idx 3


    V případě vybíjení cache lines,
    by mohla fungovat klauzule schedule(static, "počet_prvků"), která nastaví, že každé vlákno zpracuje určitý počet prvků. Mám dojem že ve bez použití klauzule schedule, se prostě práce dělí počtem vláken (rozestup je maximální). Pokud v prvním kódu přidáme za "parallel for" klauzuli shedule(static,1), tak je výstup takovýto

    Hello from FOR_1 from thread 0 in team 0 on idx 0
    Hello from FOR_1 from thread 0 in team 0 on idx 2
    Hello from FOR_1 from thread 0 in team 0 on idx 4
    Hello from FOR_1 from thread 0 in team 0 on idx 6
    Hello from FOR_1 from thread 1 in team 0 on idx 1
    Hello from FOR_1 from thread 1 in team 0 on idx 3
    Hello from FOR_1 from thread 1 in team 0 on idx 5
    Hello from FOR_1 from thread 1 in team 0 on idx 7

    Pro více-dimenzionální případ bych zvážil ještě použití klauzule collapse

    Hlavně to neberte tak že se tu snažím tlačit OpenMP jako všelék. Všelék to rozhodně není. OpenMP není ani lepší než klasická vlákna, ani horší, je prostě jiné.

    Myslím, že je dobré aby lidé věděli že existuje a na co je a není vhodné.

    Btw blízké době budu studovat jeho použití pro programování na GPU, tak se kdyžtak podělím.

  • 25. 11. 2023 22:42

    ZmatenejStrejda

    Souběžné čtení a zápis už je na programátorovi stejně jako kdyby to udělal ručně přes vlákna.
    Některé operace ovšem jdou udělat velice jednoduše.
    Například chcete udělat histogram (spočítá se četnost výskytů prvků v datech). V té chvíli je velký problém s přístupem do pole histogramu.
    Při použití vlastních vláken mě napadají 3 přístupy: mutex(y), atomické operace, každé vlákno vlastní histogram (nejrychlejší).
    Ale v tomto vám taky může OpenMP usnadnit práci.

    Zde od nejpomalejšího po nejrychlejší

    void histo_1(uint8_t * data, uint32_t * histogram){
            #pragma omp parallel for
            for(uint64_t i = 0; i < SIZE; ++i){
                    #pragma omp critical
                    histogram[data[i]]++;
            }
    }
    void histo_3(uint8_t * data, uint32_t * histogram){
            #pragma omp parallel for
            for(uint64_t i = 0; i < SIZE; ++i){
                    #pragma omp atomic
                    histogram[data[i]]++;
            }
    }
    void histo_4(uint8_t * data, uint32_t * histogram){
            #pragma omp parallel for reduction(+:histogram[:256])
            for(uint64_t i = 0; i < SIZE; ++i){
                    histogram[data[i]]++;
            }
    }

    Je dost možné že Numba umí něco podobného.

  • 24. 11. 2023 9:00

    atarist

    Můžeš prosím nějak doplnit tu info o Amdahlově a Gustafsonově zákonu. IMHO Amdahlův zákon nastavuje tvrdé limity, přes které to nejde. Prostě pokud 10% problému bude nutno řešit "sériově", nikdy se nedá dostat na speedup větší než 10x, i při nekonečném počtu jader. Gustafson mi s tímto nějak nepomůže - může pouze vylepšit to, do jaké míry se k tomu 10x speedupu přiblížím.

  • 24. 11. 2023 19:05

    ZmatenejStrejda

    Podle mě je to hlavně o přístupu k problematice. Pokud tvým jediným požadavkem je rychlost, tak pak ano Gustafson ti nijak nepomůže.