Exemple de calcul parallèle Python porté en F #

J'ai porté l'exemple de calcul parallèle de la documentation Python vers F #. Je pense que vous pouvez commencer en comparant les styles d'écriture.

GIL

J'ai essayé le multithreading en Python.

J'ai essayé le calcul parallèle, mais cela ne s'est pas beaucoup amélioré. Les articles suivants sont mentionnés.

Python utilise "Global Interpreter Lock (GIL)" comme mécanisme pour garantir la cohérence de l'exécution du programme.
Apparemment, Python Thread est pris en charge pour gérer le "blocage des E / S" lors des appels système ... Il semble que cela ne vise pas à accélérer en premier lieu ... ..

La documentation explique comment.

(Global Interpreter Lock) Un mécanisme utilisé par l'interpréteur CPython pour garantir qu'un seul thread exécute le bytecode Python à la fois. Cela simplifie l'implémentation de CPython en rendant le modèle objet (y compris les types intégrés importants tels que dict) implicitement sûr pour l'accès simultané. Le verrouillage de l'intégralité de l'interpréteur facilite la lecture multithread de l'interpréteur au détriment de la parallélisation encourue par les machines multiprocesseurs.

Cependant, certains modules d'extension standards ou externes sont conçus pour désactiver GIL lors de calculs lourds tels que la compression et le hachage. De plus, GIL est toujours libéré lors du traitement des E / S.

Dans le passé, des interpréteurs "multithread gratuits" (qui verrouillent les données en service à un grain plus fin) ont été développés, mais ont échoué en raison de performances médiocres sur un processeur unique typique. On pense que les tentatives pour résoudre ce problème de performances rendent la mise en œuvre plus complexe et augmentent les coûts de maintenance.

ProcessPoolExecutor

Les calculs parallèles en Python sont multi-processus plutôt que multi-thread. ProcessPoolExecutor prend en charge la gestion des processus et l'appel de fonctions entre les processus. Par défaut, il crée autant de processus de travail qu'il y a de processeurs logiques et alloue le traitement.

Modifiez l'exemple ProcessPoolExecutor dans la documentation. Vérifiez le temps requis en basculant entre parallèle et série. Vous pouvez le comparer avec le multithreading simplement en le remplaçant par ThreadPoolExecutor.

parallel-sample.py


import concurrent.futures
import math

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419]

def is_prime(n):
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main_parallel(cls):
    with cls() as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print('%d is prime: %s' % (number, prime))

def main():
    for number, prime in zip(PRIMES, map(is_prime, PRIMES)):
        print('%d is prime: %s' % (number, prime))

if __name__ == '__main__':
    import sys
    if sys.argv[-1] == "--process":
        main_parallel(concurrent.futures.ProcessPoolExecutor)
    elif sys.argv[-1] == "--thread":
        main_parallel(concurrent.futures.ThreadPoolExecutor)
    else:
        main()

Résultat d'exécution


$ time python parallel-sample.py
112272535095293 is prime: True
112582705942171 is prime: True
112272535095293 is prime: True
115280095190773 is prime: True
115797848077099 is prime: True
1099726899285419 is prime: False

real    0m2.745s
user    0m2.703s
sys     0m0.031s

$ time python parallel-sample.py --process
112272535095293 is prime: True
112582705942171 is prime: True
112272535095293 is prime: True
115280095190773 is prime: True
115797848077099 is prime: True
1099726899285419 is prime: False

real    0m0.983s
user    0m3.984s
sys     0m0.172s

$ time python parallel-sample.py --thread
112272535095293 is prime: True
112582705942171 is prime: True
112272535095293 is prime: True
115280095190773 is prime: True
115797848077099 is prime: True
1099726899285419 is prime: False

real    0m5.527s
user    0m5.422s
sys     0m0.109s

Le multi-processus accélère, mais le multi-thread ralentit.

F#

Port vers F #. Puisqu'il n'y a aucun problème à utiliser le multithreading dans le .NET Framework, nous omettons l'implémentation du multiprocessing.

parallel-sample.fsx


let PRIMES = [
    112272535095293L
    112582705942171L
    112272535095293L
    115280095190773L
    115797848077099L
    1099726899285419L]

let is_prime n =
    if n < 2L then
        false
    elif n = 2L then
        true
    elif n % 2L = 0L then
        false
    else
    let sqrt_n = int64 (floor (sqrt (float n)))
    seq {
        for i in {3L .. 2L .. sqrt_n + 1L} do
            if n % i = 0L then
                yield false
        yield true }
    |> Seq.head

let is_prime_async n = async { return is_prime n }

let main_parallel() =
    PRIMES
    |> Seq.map is_prime_async
    |> Async.Parallel
    |> Async.RunSynchronously
    |> Seq.zip PRIMES
    |> Seq.iter (fun (number, prime) -> printfn "%d is prime: %b" number prime)

let main() =
    PRIMES
    |> Seq.map is_prime
    |> Seq.zip PRIMES
    |> Seq.iter (fun (number, prime) -> printfn "%d is prime: %b" number prime)

match Array.last (System.Environment.GetCommandLineArgs()) with
| "--thread" -> main_parallel()
| _          -> main()

Résultat d'exécution


$ time mono parallel-sample.exe
112272535095293 is prime: true
112582705942171 is prime: true
112272535095293 is prime: true
115280095190773 is prime: true
115797848077099 is prime: true
1099726899285419 is prime: false

real    0m0.662s
user    0m0.625s
sys     0m0.031s

$ time mono parallel-sample.exe --thread
112272535095293 is prime: true
112582705942171 is prime: true
112272535095293 is prime: true
115280095190773 is prime: true
115797848077099 is prime: true
1099726899285419 is prime: false

real    0m0.308s
user    0m0.813s
sys     0m0.078s

L'effet est difficile à voir à moins qu'il ne soit un peu plus lourd, mais la vitesse s'améliore.

Flux de processus

Il est facile de voir ce qui se passe avec ʻasync et ʻAsync en regardant les types.

Fonction asynchroniser


let is_prime n = ...        // int64 -> bool
let is_prime_async n = ...  // int64 -> Async<bool>

Appliquer la liste des arguments


    PRIMES
    |> Seq.map is_prime_async  // seq<Async<bool>>

Combinez plusieurs processus asynchrones


    |> Async.Parallel          // Async<bool array>

Exécute un traitement asynchrone et renvoie le résultat sous forme de tableau


    |> Async.RunSynchronously  // bool array

Les calculs parallèles sont effectués dans le pool de threads. Même si vous passez un grand nombre de calculs à la fois, ils seront planifiés en conséquence.

Recommended Posts