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écrire des calculs qui effectuent des opérations à effet, dont la signification est décrite par des gestionnaires qui englobent les calculs. Ils sont une généralisation des gestionnaires d'exceptions et permettent aux mécanismes de flux de contrôle non-locaux tels que les exceptions résumables, les threads légers, les coroutines, les générateurs et les E/S asynchrones d'être exprimés de manière composable.
Les gestionnaires d'effets dans OCaml 5.0 doivent être considérés comme expérimentaux. Les gestionnaires d'effets sont exposés dans la bibliothèque standard comme une enveloppe fine autour de leurs implémentations dans le runtime. Ils ne sont pas supportés comme une fonctionnalité du langage avec une nouvelle syntaxe.
Le bout de codeci-dessous défini un effet (c'est-à-dire une opération) qui prend un argument entier et renvoie un résultat entier. Nous nommons cet effet Xchg.
Code ocaml : | Sélectionner tout |
1 2 3 4 5 | open Effect open Effect.Deep type _ Effect.t += Xchg: int -> int t let comp1 () = perform (Xchg 0) + perform (Xchg 1) |
L'effet d'échange Xchg est déclaré en étendant le type variante extensible prédéfini Effect.t avec un nouveau constructeur Xchg : int -> int t. La déclaration peut être lue intuitivement comme « l'effet Xchg prend un paramètre entier, et lorsque cet effet est exécuté, il retourne un entier ». Le calcul comp1 exécute l'effet deux fois en utilisant la primitive perform et retourne leur somme. L'effet Xchg peut être géré en implémentant un gestionnaire qui renvoie toujours le successeur de la valeur offerte :
Code ocaml : | Sélectionner tout |
1 2 3 4 5 6 7 | try_with comp1 () { effc = fun (type a) (eff: a t) -> match eff with | Xchg n -> Some (fun (k: (a, _) continuation) -> continue k (n+1)) | _ -> None } - : int = 3 |
try_with exécute le calcul comp1 () sous un gestionnaire d'effet qui gère l'effet Xchg. Comme mentionné précédemment, les gestionnaires d'effets sont une généralisation des gestionnaires d'exceptions. Comme pour les gestionnaires d'exception, lorsque le calcul exécute l'effet Xchg, le contrôle saute au gestionnaire correspondant. Cependant, contrairement aux gestionnaires d'exception, le gestionnaire reçoit également la continuation délimitée k, qui représente le calcul suspendu entre le point d'exécution et ce gestionnaire.
Le gestionnaire utilise la primitive continue pour reprendre le calcul suspendu avec le successeur de la valeur offerte. Dans cet exemple, le calcul comp1 exécute Xchg 0 et Xchg 1 et reçoit les valeurs 1 et 2 du gestionnaire respectivement. Par conséquent, l'expression entière est évaluée à 3.
Il est utile de noter que nous devons utiliser un type abstrait local (type a) dans le gestionnaire d'effets. Le type Effect.t est un GADT, et les déclarations d'effet peuvent avoir différents paramètres de type pour différents effets. Le paramètre de type a dans le type a Effect.t représente le type de la valeur retournée lors de l'exécution de l'effet. Du fait que eff a le type a Effect.t et du fait que Xchg n a le type int [C=OCaml]Effect.t, le vérificateur de type déduit que a doit être int, c'est pourquoi nous sommes autorisés à passer la valeur entière n+1 comme argument pour continuer k.
Un autre point à noter est que le cas fourre-tout | _ -> None est nécessaire lors de la gestion des effets. Ce cas peut être lu intuitivement comme « transmettre les effets non gérés au gestionnaire externe ».
Instructions d'installation
Le compilateur de base peut être installé comme un commutateur opam avec les commandes suivantes :
Code : | Sélectionner tout |
1 2 | opam update opam switch create 5.0.0 |
opam install domainslib
Source : OCaml
Et vous ?
Quel est votre avis sur le langage OCaml ?
A-t-on véritablement besoin du langage OCaml ? Où y a-t-il des besoins pour ce langage ?
Où se trouvent les pistes d'amélioration ?
Voir aussi :
C++ se classe mieux que Java pour la première fois dans l'histoire de l'indice de Tiobe, Java ne figure même plus dans le top 3 des langages les plus populaires
Rune, un langage de programmation systèmes, inspiré de Python et présenté comme étant rapide, de l'avis de certains développeurs, une bibliothèque aux langages existants aurait été préférable
Les petits langages sont-ils l'avenir de la programmation ? Oui, selon Christoffer Ekeroth, développeur d'applications web et de systèmes distribués