Comme de nombreux développeurs, je m’intéresse à Rust depuis un certain temps. Non seulement parce qu’il apparaît dans tant de titres sur les nouvelles des pirates informatiques, ou à cause de l’approche novatrice que le langage adopte en matière de sécurité et de performance, mais aussi parce que les gens semblent en parler avec un sens particulier de l’amour et de l’admiration. En plus de cela, Rust m’intéresse particulièrement car il partage certains des mêmes objectifs et caractéristiques de ma langue préférée: Swift. Comme j’ai récemment pris le temps d’essayer Rust dans de petits projets personnels, je voulais prendre un peu de temps pour documenter mes impressions sur la langue, en particulier dans la façon dont elle se compare à Swift.

La vue d’ensemble

Rust et Swift ont beaucoup de choses en commun : ce sont deux langages compilés dotés de systèmes de type puissants et modernes et axés sur la sécurité. Des fonctionnalités telles que les types algébriques et la gestion de première classe des valeurs facultatives aident à déplacer de nombreuses classes d’erreurs de l’exécution à la compilation dans ces deux langages.

Alors, en quoi ces langues diffèrent-elles? La meilleure façon de caractériser la différence est:

Swift facilite l’écriture de code sécurisé.
La rouille rend difficile l’écriture de code dangereux.

Ces deux déclarations peuvent sembler équivalentes, mais il existe une distinction importante. Les deux langages ont des outils pour atteindre la sécurité, mais ils font des compromis différents pour y parvenir: Swift privilégie l’ergonomie au détriment de la performance, tandis que Rust privilégie la performance au détriment de l’ergonomie.

Le compromis: Performance vs Ergonomie

La plus grande façon de démontrer cette différence de priorité est dans l’approche que ces langages ont de la gestion de la mémoire. Je vais commencer par Rust car l’approche du langage en matière de gestion de la mémoire est l’un de ses arguments de vente uniques.

Dans Rust, la mémoire est principalement gérée statiquement (oui, il existe d’autres modes de gestion de la mémoire comme le comptage de références, mais nous les ignorerons pour l’instant). Cela signifie que le compilateur Rust analyse votre programme et, selon un ensemble de règles, décide du moment où la mémoire doit être allouée et libérée.

Pour assurer la sécurité, Rust utilise une nouvelle stratégie appelée vérification des emprunts. La façon dont cela fonctionne en pratique est que, en tant que programmeur, chaque fois que vous passez une variable (c’est-à-dire une référence à un emplacement de mémoire), vous devez spécifier si la référence est mutable ou immuable. Le compilateur utilise ensuite un ensemble de règles pour s’assurer que vous ne pouvez pas muter un seul morceau de mémoire à deux endroits à la fois, ce qui permet de prouver que votre programme n’a pas de courses de données.

Cette approche a des propriétés très bénéfiques en ce qui concerne l’utilisation et les performances de la mémoire. La vérification d’emprunt peut être très parcimonieuse avec la mémoire, car elle évite généralement de copier des valeurs. Cela évite également la surcharge de performances d’une solution telle que la collecte des ordures, car le travail est effectué au moment de la compilation plutôt qu’à l’exécution.

Cependant, il présente certains inconvénients en ce qui concerne la facilité d’utilisation. En raison de la nature de la propriété dans Rust, certains modèles de conception ne fonctionnent tout simplement pas dans Rust. Par exemple, il n’est pas trivial d’implémenter quelque chose comme une liste doublement chaînée ou une variable globale. Cela devient probablement plus intuitif avec le temps, et il existe des solutions de contournement pour ces problèmes, mais Rust impose certainement des limitations au programmeur qui ne sont pas présentes dans d’autres langages.

Bien qu’on ne parle pas si souvent de Rouille, Swift a également une histoire intéressante en matière de gestion de la mémoire.

Swift a deux types fondamentaux de variables : les types de référence et les types de valeur. En général, les types de référence sont alloués en tas et sont gérés par comptage de références. Cela signifie qu’au moment de l’exécution, le nombre de références à un objet compté de référence est suivi et que l’objet est désalloué lorsque le nombre atteint zéro. Le comptage des références dans Swift est toujours atomique: cela signifie que chaque fois qu’un comptage de références change, il doit y avoir une synchronisation entre tous les threads du PROCESSEUR. Cela a l’avantage d’éliminer la possibilité qu’une référence soit libérée par erreur dans une application multithread, mais a un coût de performance important car la synchronisation du PROCESSEUR est très coûteuse.

Rust dispose également d’outils pour le comptage de référence et le comptage de référence atomique, mais ceux-ci sont opt-in plutôt que d’être la valeur par défaut.

Les types de valeurs, en revanche, sont généralement alloués à la pile et leur mémoire est gérée statiquement. Cependant, le comportement des types de valeur dans Swift est très différent de la façon dont Rust gère la mémoire. Dans Swift, les types de valeur ont ce qu’on appelle le comportement « copie sur écriture », ce qui signifie que chaque fois qu’un type de valeur est écrit dans une nouvelle variable ou transmis à une fonction, une copie est effectuée.

La copie sur écriture par défaut atteint certains des mêmes objectifs de vérification des emprunts: en tant que programmeur, vous n’avez généralement jamais à vous soucier d’une valeur changeant mystérieusement en raison d’un effet secondaire inattendu ailleurs dans le programme. Cela nécessite également un peu moins de charge cognitive que la vérification des emprunts, car il existe des classes entières d’erreurs de compilation liées à la propriété dans Rust qui n’existent tout simplement pas dans Swift. Cependant, cela a un coût: ces copies supplémentaires nécessitent une utilisation supplémentaire de la mémoire et des cycles CPU pour terminer.

Dans Rust, il est également possible de copier des valeurs pour réduire au silence les erreurs de vérification des emprunts, mais cela ajoute du bruit visuel car les copies doivent être explicitement spécifiées.

Nous avons donc ici un bon exemple des compromis effectués par ces deux langages: Swift vous donne quelques hypothèses générales sur la façon dont la mémoire doit être gérée tout en maintenant un niveau de sécurité. C’est un peu comme la façon dont un programmeur C++ peut gérer la mémoire selon les meilleures pratiques avant de réfléchir à l’optimisation. Cela rend très facile d’entrer et d’écrire du code sans trop réfléchir aux détails de bas niveau, et en obtenant également des garanties de sécurité et d’exactitude d’exécution de base que vous n’obtiendriez pas dans un langage comme Python ou même Golang. Cependant, il vient avec des falaises de performance, qu’il est facile de tomber sans même s’en rendre compte jusqu’à ce que vous exécutiez votre programme. Il est possible d’écrire du code Swift haute performance, mais cela nécessite souvent un profilage et une optimisation soignés.

Rust, d’autre part, vous donne de nombreux outils spécifiques pour spécifier comment la mémoire doit être gérée, puis impose des restrictions strictes sur la façon dont vous les utilisez pour éviter les comportements dangereux. Cela vous donne de très belles caractéristiques de performance dès la sortie de la boîte, mais cela vous oblige à prendre en charge les frais cognitifs supplémentaires pour vous assurer que toutes les règles sont suivies.

Mon point à retenir de cela est que bien que ces langages aient des objectifs communs, ils ont des caractéristiques fondamentalement différentes qui se prêtent à différents cas d’utilisation. Rust, par exemple, semble le choix clair pour quelque chose comme le développement intégré, où l’utilisation optimale des cycles de mémoire et de CPU est extrêmement importante, et où la boucle de compilation-exécution de code peut être plus lente, il est donc utile de détecter tous les problèmes possibles au moment de la compilation. Alors que Swift pourrait être un meilleur choix pour quelque chose comme la science des données ou la logique sans serveur, où les performances sont une préoccupation secondaire, et il est utile de travailler plus près du domaine du problème sans avoir à considérer beaucoup de détails de bas niveau.

Dans tous les cas, je serai très intéressé de suivre ces deux langues à l’avenir, et je suivrai ce post avec plus d’observations sur la comparaison entre Swift et Rust.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.