Multiprocessing in Python

Hozzászólások

Régebbi méréseim szerint a threading és a multiprocessing bár sok szálra dob szét, mégsem kapsz hamarabb eredményt. Átverésnek érzem ezeket.
Jobban jársz, ha a Python kódot egyszálasra hagyod és PyPy-vel futtatod.

Persze azóta lehet, hogy jobb lett.

A numpy az nyereség, de a multiprocessing számomra csekély nyereségnek tűnik Pyton esetén, amikor tényleg kell a tempó. Például:

https://pastebin.com/40aPrUMv
https://pastebin.com/1mz5DWpj - rustc-vel egyszerűen lefordítva (-O itt hülyére optimalizál, az itt most csalás lenne)

A futásidőben látható különbség magáért beszél.

Nem feledkeztél meg a már említett GIL létezéséről?
Az nagyon nagy hátrány lehet, a Rust meg azt hiszem, mentes tőle.
Nomeg arról, hogy a Rust az gépi kód, a Python az interpreter. Esetleg nézd meg pypy-vel, ha működik! Valamennyit gyorsít a memóriában végzett műveleteken.

Kíváncsiságból lefuttattam különböző Python interpretereken (Mindegyik teszt ugyanazon a i5 6600k procin, Win10-en futott, a stabilabb eredmény miatt az n-et átírtam 3000*10000 -re, illetve 3000*10000*100 -ra Cython esetében):

https://paste2.org/MIJ5M7Jk

Érdekességek:
- Python 2 esetében WSL vs "natív" win32-es Python 2 között 2.5-3x sebességkülönbség van
- PyPy WSL vs "natív" win32-es PYPY között: 10-20x különbség
- Első Cython kísérletem volt, de fájdalommentesen ment a dolog (pip install cython, váltózótípusok definiálása)
- Az IronPython elengedi a GIL-t, de sebességben még nem az igazi

GIL nélküli hello.pyx fájl:


def hello(long thid, long n):
    cdef long res = 0
    with nogil:
        res = _hello(thid, n)
    print res


cdef inline long _hello(long thid, long n) nogil:
    cdef long i = 0
    cdef long res = 0
    while i < n:
        res += i
        i += 1
    return res

ezután az eredeti fájlba simán beimportáltam:


import pyximport
pyximport.install()
import hello

a thread indítást átírtam

hello.hello

function-re, mást nem módosítottam, python3 test_multi.py -val indítva a fordítást észrevétlenül végzi a Cython.

Kipróbáltam egy (számomra) kevésbé ismert Jittert: https://numba.pydata.org/ eléggé meggyőző eredményekkel (3000*10000*100 futás esetében):

Python 3.6.4 + Numba * 100
Multi-Thread: 0.099s
Multi-Process: 0.097s
Single: 0.001s

- "from numba import jit" + "@jit" dekorátolon kívül mást nem módosítottam az eredeti forráskódon még a típusokat se definiáltam.
- Próbáltam növelni 3000*10000*10000*i -re az iterációszámot, de sehogy se tudtam elmozdítani 0.001s-ről, szóval valami durva optimalizációt csinál

Kicsit módosítottam a kódon, hogy kicselezzem az optimizert, így már mérhető volt az eredmény (n = 3000*10000*10)


@jit
def hello(thid, n):
    i = 1
    res = 1
    while i < n:
        res += i / res
        i += 1
    print(res)

Multi-Thread: 7.751s, Multi-Process: 2.015s, Single: 7.666s
@jit(nogil=True, fastmath=True) -re módosítva a dekorátort:


Numba:  Multi-Thread: 1.677s, Multi-Process: 1.643s, Single: 6.167s
Pypy:   Multi-Thread: 13.35s, Multi-Process: 3.422s, Single: 13.268s
Cython: Multi-Thread: 1.723s, Multi-Process: 1.846s, Single: 6.310s

Tud CUDA-ra meg AOT is fordítani, de azokat nem próbáltam. (Nincs GPU driver egyelőre WSL-ben)

Tehát módosítatlan forráskód mellett ugyanolyan gyors, mint a típus definíciókkal ellátott Cython kód, 2x gyorsabb, mint Pypy és a GIL elengedése is egyszerű.

Update: int-et double-ra cserélve a Cython kódba 3x gyorsulást eredményezett, így már hasonló eredményeket hozott, mint a numba

Ügyes. Arra figyelj, hogy a for() ciklusba rejtett összegzést egy erősebb optimalizáció a fordítóban ma már gyakran kiszámolja.
C esetén ezért azt szoktam csinálni, hogy önállóan fordított fájlban van a számolás és az értékeket a meghívótól kapja, ami külön lesz fordítva és a linker már nem szimulálja végig.
A két fájlt tehát önállóan fordítom és csak a linkelés közös. Ellenkező esetben a fordító szimulátora -O2 esetén konstanst helyettesít be. Jópárszor átvert engem, ahogy a fenti Rust példában is megjegyeztem, hogy a -O szintén konstanst dobhat a ciklusok szolgai végrehajtása helyett.

FFT-nél az algoritmus annyira nyakatekert, hogy a fordító nem tudja végigszimulálni. Főleg ha külön fájlban van fordítva az FFT és másik fájlból jön rá az adathalmaz. Rust esetén crate-t ("lib") csináltam magából az FFT algoritmusból, hogy véletlenül se tudja összeoptimalizálni az őt meghívó programban levő adatokkal.

Illetve a másik jó megoldás, ha a feldolgozandó adatok külső adatfájlból jönnek. Ekkor viszont az I/O-t is beleméred. Lényeg, hogy a fordító ne tudja kiszámolni a ciklusok eredményét.

Próbáltam úgy is, hogy a "res += i" -t számoló function az n-t input()-ból kapja, de úgy is 1ms lett a futásidő, szóval nem tudom, hogy ilyen esetben hogyan tudja kiszámolni előre.

Viszont, ha kicserélem if i % 100: res += i -re, akkor már rendes időt kapok (1.6s), szóval biztosan valami csalás van benne. :)

Itt egy átverős példa:


#include <stdio.h>

void hello(long long n) {
    long long i = 0;
    long long res = 0;
    while (i < n) {
        res += i;
        i += 1;
    }
    printf("result: %Lu\n", res);
}

int main() {
    long long n = 3000*1000;
    hello(n);

    puts("Done!");
    return 0;
}

Fordítsd le olvasható asm kódra és nézz bele.
$ gcc -O2 -S test.c

Meglesz a függvény és tényleg ciklust látsz benne. Hurrá.
De a főprogram cseszik meghívni. Ellenben:


   movabsq $4499998500000, %rsi    <---- és itt a végeredmény
   xorl    %eax, %eax
   call    printf@PLT

Ettől mindig tartok, hogy a hamar összedobott egyszerű tesztjeimnél átver egy mai normális fordító.

Hát, jobb de nem az igazi. FFT benchmarkommal néztem imént J1900-as 4 magos HT mentes procin:

Egyszálas: Python3 --- 40 másodperc
Egyszálas: PyPy --- 2,29 másodperc

Multithread Python3 --- 10,9 másodperc ... rosszabb az egyszálas PyPy-nél.
Multithreas PyPy --- 2,78 másodperc ... kiterhelte a 4 CPU-t, de nem lett gyorsabb.

Rust simán egy szálon --- 0,35 másodperc
Rust 4 thread + SIMD --- 0,0466 másodperc

Még mindig úgy érzem, hogy ha sebességérzékeny a feladat, akkor nem a Python és a multithread-del való varázslás lesz a barátom. Inkább a PyPy egyszálon. Ha meg az sem elég, akkor a Rust, ami a szripthez képest egyetlen művelettel több (rustc -O teszt.rs).

Izé... sebességérzékeny feladat esetén az interpreteres nyelvek általában kiesnek. Nem arra valók.
A pypy egy JIT-szerű eszköz, de épp azt néztem, hogy amit én próbáltam egy processzel és többel, azt egy processzes verzióban lényegesen gyorsabban futtatja, mint a multiprocesszes változatot. :)

És még1x: Rust-ot C-vel hasonlíts, ne Pythonnal! :)

Attól függ, mit csinálsz. (történetesen a fenti cikkbe csak belebotlottam, nekem nem mondott sok újat)
Pár éve próbáltam egy a "tűzfal" (gyk: packet filter) logokat analizáló programot összedobni. Két magos, HT-s processzoron ha szekvenciálisan dolgoztam fel, jóval tovább tartott, mint amikor szétszedtem négy szálra, amiből egy olvasta a fájlt, tolta be a queue-ba, a többi meg boncolgatta a talált sorokat.

Ennyit találtam meg a maradványokból: https://github.com/haa-zee/python-sandbox/tree/master/probak/multiproce…

A multiprocess változat 4300000 soron 8, a szekvenciális 15 másodpercig futott a notebookomon.
De most direkt kipróbáltam, hogy egyetlen processzben megy a paralell feldolgozás, úgy már 18mp kell neki. (4-nél több nem gyorsít jelentősen, valahol 7.5mp körül van a leggyorsabb)

En ugy tanultam, hogy attol fugg, van-e haszna, hogy mit probal a programod csinalni.

Ha egymastol fuggetlen tobb szamolast, pl. akkor ezeket kulon processzorok tudjak futtatni, es hamarabb jon eredmeny. Ha egy logikai sorban van minden, akkor egy proci fog izzadni, a tobbi meg malmozik.

Szamolason kivul mas dolgokra is irtak, hogy jo. Pl. van egy interaktiv szal meg egy masik, ami szamol vagy valami mas lassu dolgot csinal, es nem lassitja be az elsot. Ebben az esetben lehet, hogy nem kapsz hamarabb eredmenyt, de a felhasznaloi elmeny jobb.

Es persze siman lehet az is, hogy valami olyan dolog hatarozza meg a sebesseget (pl. memoria, halozat, hattertar sebessege, felhasznaloi tevekenyseg, stb.) ami miatt teljesen mindegy, hogy a program maga egy vagy tobb szalon fut, mert nem ettol fugg.

Pl. van egy interaktiv szal meg egy masik, ami szamol...
Igen, például egy tkinteres process meg egy számolós process. És ezeket meg lehet úgy írni, hogy a számolós akár több gépen is fusson egyidejűleg, a grafikus pedig mindegyiktől gyűjtse be az adatokat.
(Bár értem, hogy inkább az egy adott gépen megosztott erőforrások és a végrehajtási sebesség a téma tárgya.)
--
eutlantis