
elle apporte le support du parallélisme en mémoire partagée et les gestionnaires d'effets
OCaml, anciennement connu sous le nom d'Objective Caml, est l'implémentation la plus avancée du langage de programmation Caml, créé par Xavier Leroy, Jérôme Vouillon, Damien Doligez, Didier Rémy et leurs collaborateurs en 1996. Ce langage, de la famille des langages ML, est un projet open source dirigé et maintenu essentiellement par l'Inria. L’équipe en charge du langage de programmation multi-paradigme, OCaml, a annoncé la disponibilité de la version 5.0.0 du langage dans un post publié le 16 décembre. « Nous avons le plaisir de fêter les anniversaires de Jane Austen et d'Arthur C. Clarke en annonçant la sortie de la version 5.0.0 d'OCaml ».
OCaml est le successeur de Caml Light, auquel il a ajouté entre autres une couche de programmation objet. L'acronyme CAML provient de Categorical Abstract Machine Language, un modèle de machine abstraite qui n'est cependant plus utilisé dans les versions récentes de OCaml.
OCaml est utilisé dans des projets aussi divers que le logiciel de synchronisation de fichiers Unison, l'assistant de preuves formelles Coq ou la version Web de Facebook Messenger. Les facilités de traitement symbolique du langage permettent le développement d'outils de vérification statique, comme le projet SLAM pour des pilotes Windows écrits par Microsoft, ou ASTRÉE pour certains systèmes embarqués des Airbus A380.
Le point fort de cette nouvelle version majeure d'OCaml est le support tant attendu du parallélisme en mémoire partagée et des gestionnaires d'effets. Ce support multicore est l'aboutissement de plus de 8 ans d'efforts et a nécessité une réécriture complète de l'environnement d'exécution d'OCaml. Par conséquent, OCaml 5.0.0 devrait être une version d'OCaml plus expérimentale que les versions habituelles d'OCaml.
Dans cette version, le compilateur natif ne prend en charge que les architectures x86-64 et arm64. En termes de systèmes d'exploitation, Linux, les BSD, macOS et mingw64 sous Windows sont pris en charge.
Les responsables en charge du langage prévoient de restaurer la prise en charge de la plupart des architectures et systèmes d'exploitation précédemment pris en charge, et de résoudre les problèmes connus restants au cours de l'année prochaine. OCaml 5 en tant que langage est entièrement compatible avec OCaml 4 jusqu'aux caractéristiques de performance de vos programmes. En d'autres termes, tout code qui fonctionne avec OCaml 4 devrait fonctionner de la même manière avec OCaml 5. Les exceptions actuellement connues à cette règle sont
- la suppression de nombreuses fonctions et modules dépréciés depuis longtemps ;
- les modifications apportées à l'API d'exécution interne ;
- les performances des éphémères sont actuellement (et temporairement) fortement dégradées.
Caml est un langage fonctionnel augmenté de fonctionnalités permettant la programmation impérative. OCaml étend les possibilités du langage en permettant la programmation orientée objet et la programmation modulaire. Pour toutes ces raisons, OCaml entre dans la catégorie des langages multi-paradigme. Il intègre ces différents concepts dans un système de types hérité de ML, caractérisé par un typage statique, fort et inféré.
Le système de types permet une manipulation aisée de structures de données complexes : on peut aisément représenter des types algébriques, c'est-à-dire des types hiérarchisés et potentiellement récursifs (listes, arbres…), et les manipuler aisément à l'aide du filtrage par motif. Cela fait de OCaml un langage de choix dans les domaines demandant la manipulation de structures de données complexes, par exemple les compilateurs. Le typage fort, ainsi que l'absence de manipulation explicite de la mémoire (présence d'un ramasse-miettes) font de OCaml un langage très sûr. Il est aussi réputé pour ses performances, grâce à la présence d'un compilateur de code natif.
Possibilités de programmation parallèle dans OCaml
La bibliothèque standard d'OCaml expose des primitives de bas niveau pour la programmation parallèle. Il est recommandé aux utilisateurs d'utiliser des bibliothèques de programmation parallèle de plus haut niveau telles que domainslib. OCaml distingue la concurrence et le parallélisme et fournit des mécanismes distincts pour les exprimer. La concurrence est l'exécution superposée de tâches alors que le parallélisme est l'exécution simultanée de tâches.
En particulier, les tâches parallèles se chevauchent dans le temps, mais les tâches concurrentes peuvent ou non se chevaucher dans le temps. Les tâches peuvent s'exécuter simultanément en se cédant le contrôle les unes aux autres. Alors que la concurrence est un mécanisme de structuration des programmes, le parallélisme est un mécanisme permettant d'accélérer l'exécution de vos programmes.
Domaines
Les domaines sont les unités de parallélisme en OCaml. Le module Domain fournit les primitives pour créer et gérer les domaines. De nouveaux domaines peuvent être créés à l'aide de la fonction spawn.
Code ocaml : | Sélectionner tout |
1 2 3 | Domain.spawn (fun _ -> print_endline "I ran in parallel") I ran in parallel - : unit Domain.t = <abstr> |
La fonction spawn exécute le calcul donné en parallèle avec le domaine appelant. Les domaines sont des entités lourdes. Chaque domaine correspond à une rélation 1:1 à un thread du système d'exploitation. Chaque domaine possède également son propre état d'exécution, qui comprend des structures propres au domaine pour l'allocation de la mémoire. Ils sont donc relativement coûteux à créer et à démanteler. Il est recommandé que les programmes ne génèrent pas plus de domaines que de cœurs disponibles.
Joindre des domaines
Utilisons le programme pour calculer le nième nombre de Fibonacci en utilisant la récursion comme exemple courant. Le programme séquentiel pour calculer le nième nombre de Fibonacci est donné ci-dessous.
Code ocaml : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | (* fib.ml *) let n = try int_of_string Sys.argv.(1) with _ -> 1 let rec fib n = if n < 2 then 1 else fib (n - 1) + fib (n - 2) let main () = let r = fib n in Printf.printf "fib(%d) = %d\n%!" n r let _ = main () |
Le programme peut être compilé et évalué comme suit.
$ ocamlopt -o fib.exe fib.ml
$ ./fib.exe 42
fib(42) = 433494437
$ hyperfine './fib.exe 42' # Benchmarking
Benchmark 1: ./fib.exe 42
Time (mean ± sd): 1.193 s ± 0.006 s [User: 1.186 s, System: 0.003 s]
Range (min … max): 1.181 s … 1.202 s 10 runs
Nous constatons qu'il faut environ 1,2 seconde pour calculer le 42e nombre de Fibonacci. Les domaines créés peuvent être joints à l'aide de la fonction join pour obtenir leurs résultats. La fonction de jonction attend que le domaine cible se termine. Le programme suivant calcule le nième nombre de Fibonacci deux fois en parallèle.
Code ocaml : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | (* fib_twice.ml *) let n = int_of_string Sys.argv.(1) let rec fib n = if n < 2 then 1 else fib (n - 1) + fib (n - 2) let main () = let d1 = Domain.spawn (fun _ -> fib n) in let d2 = Domain.spawn (fun _ -> fib n) in let r1 = Domain.join d1 in Printf.printf "fib(%d) = %d\n%!" n r1; let r2 = Domain.join d2 in Printf.printf "fib(%d) = %d\n%!" n r2 let _ = main () |
Le programme génère deux domaines qui calculent le nième nombre de Fibonacci. La fonction spawn renvoie une valeur Domain.t qui peut être jointe pour obtenir le résultat du calcul parallèle. La fonction join bloque jusqu'à ce que le calcul soit terminé.
$ ocamlopt -o fib_twice.exe fib_twice.ml
$ ./fib_twice.exe 42
fib(42) = 433494437
fib(42) = 433494437
$ hyperfine './fib_twice.exe 42'
Benchmark 1: ./fib_twice.exe 42
Time (mean ± sd): 1.249 s ± 0.025 s [User: 2.451 s, System: 0.012 s]
Range (min … max): 1.221 s … 1.290 s 10 runs
Comme on peut le voir, calculer deux fois le nième nombre de Fibonacci a pris presque le même temps que de le calculer une fois grâce au parallélisme.
Domainslib : une bibliothèque pour la programmation parallèle imbriquée
Essayons de paralléliser la fonction Fibonacci. Les deux appels récursifs peuvent être exécutés en parallèle. Cependant, la parallélisation naïve des appels récursifs en créant des domaines pour chacun d'eux ne fonctionnera pas car elle crée trop de domaines.
Code ocaml : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | (* fib_par1.ml *) let n = try int_of_string Sys.argv.(1) with _ -> 1 let rec fib n = if n < 2 then 1 else begin let d1 = Domain.spawn (fun _ -> fib (n - 1)) in let d2 = Domain.spawn (fun _ -> fib (n - 2)) in Domain.join d1 + Domain.join d2 end let main () = let r = fib n in Printf.printf "fib(%d) = %d\n%!" n r let _ = main () fib(1) = 1 val n : int = 1 val fib : int -> int = <fun> val main : unit -> unit = <fun> |
$ ocamlopt -o fib_par1.exe fib_par1.ml
$ ./fib_par1.exe 42
Fatal error: exception Failure("failed to allocate domain")
OCaml a une limite de 128 domaines qui peuvent être actifs en même temps. Si l'on tente de créer plus de domaines, une exception sera levée.
Parallélisation de Fibonacci à l'aide de domainslib
La bibliothèque standard d'OCaml ne fournit que des primitives de bas niveau pour la programmation concurrente et parallèle, laissant les bibliothèques de programmation de haut niveau être développées et distribuées en dehors de la distribution du compilateur de base. Domainslib est une telle bibliothèque pour la programmation parallèle imbriquée, qui est illustrée par le parallélisme disponible dans le calcul récursif de Fibonacci. Utilisons domainslib pour paralléliser le programme récursif Fibonacci. Il est recommandé d'installer domainslib en utilisant le gestionnaire de paquets opam.
Domainslib fournit un mécanisme async/await pour lancer des tâches parallèles et attendre leurs résultats. En plus de ce mécanisme, domainslib fournit des itérateurs parallèles. A la base, domainslib a une implémentation efficace de la file d'attente de détournement de travail afin de partager efficacement les tâches avec d'autres domaines. Une implémentation parallèle du programme Fibonacci est donnée ci-dessous.
Code ocaml : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | (* fib_par2.ml *) let num_domains = int_of_string Sys.argv.(1) let n = int_of_string Sys.argv.(2) let rec fib n = if n < 2 then 1 else fib (n - 1) + fib (n - 2) module T = Domainslib.Task let rec fib_par pool n = if n > 20 then begin let a = T.async pool (fun _ -> fib_par pool (n-1)) in let b = T.async pool (fun _ -> fib_par pool (n-2)) in T.await pool a + T.await pool b end else fib n let main () = let pool = T.setup_pool ~num_additional_domains:(num_domains - 1) () in let res = T.run pool (fun _ -> fib_par pool n) in T.teardown_pool pool; Printf.printf "fib(%d) = %d\n" n res let _ = main () |
Le programme prend le nombre de domaines et l'entrée de la fonction Fibonacci comme premier et deuxième arguments de la ligne de commande respectivement.
Commençons par la fonction principale. Tout d'abord, un pool de domaines sur lesquels les tâches parallèles imbriquées seront exécutées est mis en place. Le domaine invoquant la fonction run participera également à l'exécution des tâches soumises au pool. La fonction parallèle Fibonacci fib_par dans la fonction run est invoquée. Enfin, nous démolissons le pool et imprimons le résultat.
Pour des entrées suffisamment grandes (n > 20), la fonction fib_par génère les appels récursifs gauche et droit de manière asynchrone dans le pool en utilisant la fonction async. La fonction asynchrone renvoie une promesse pour le résultat. Le résultat d'un calcul asynchrone est obtenu en attendant la promesse à l'aide de la fonction await. L'appel de la fonction await se bloque jusqu'à ce que la promesse soit résolue.
Pour les petites entrées, la fonction fib_par appelle simplement la fonction Fibonacci séquentielle fib. Il est important de passer en mode séquentiel pour les problèmes de petite taille. Sinon, le coût de la parallélisation sera plus important que le travail disponible.
Les résultats observés dépendent du nombre de cœurs disponibles sur la machine cible. L’exemple est écrit sur un MacBook Pro Quad-Core Intel Core i7 de 2.3 GHz avec 4 cœurs et 8 threads matériels. Il est raisonnable de s'attendre à une performance d'environ 4x sur 4 domaines pour des programmes parallèles avec peu de coordination entre les domaines, et lorsque la machine n'est pas sous la charge. Au-delà de 4 domaines, l'accélération sera probablement moins que linéaire.
Gestionnaires d'effets
Les gestionnaires d'effets sont un mécanisme de programmation modulaire avec des effets définis par l'utilisateur. Les gestionnaires d'effets permettent aux programmeurs de d...
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.