Comment nous avons traduit 10 millions de lignes de code C++ vers le standard C++14 (puis vers C++17)

Il y a quelque temps (à l'automne 2016), lors du développement de la prochaine version de la plateforme technologique 1C:Enterprise, la question s'est posée au sein de l'équipe de développement de la prise en charge du nouveau standard. C ++ 14 dans notre code. La transition vers un nouveau standard, comme nous l'avions supposé, nous permettrait d'écrire beaucoup de choses de manière plus élégante, plus simple et plus fiable, et simplifierait le support et la maintenance du code. Et il ne semble y avoir rien d'extraordinaire dans la traduction, si ce n'est l'ampleur de la base de code et les spécificités de notre code.

Pour ceux qui ne le savent pas, 1C:Enterprise est un environnement permettant le développement rapide d'applications métiers multiplateformes et un environnement d'exécution pour leur exécution sur différents systèmes d'exploitation et SGBD. De manière générale, le produit contient :

Nous essayons autant que possible d'écrire le même code pour différents systèmes d'exploitation - la base de code du serveur est commune à 99 %, la base de code client est d'environ 95 %. La plateforme technologique 1C:Enterprise est principalement écrite en C++ et les caractéristiques approximatives du code sont données ci-dessous :

  • 10 millions de lignes de code C++,
  • 14 mille fichiers,
  • 60 mille cours,
  • un demi-million de méthodes.

Et tout cela devait être traduit en C++14. Aujourd'hui, nous allons vous expliquer comment nous avons fait cela et ce que nous avons rencontré au cours du processus.

Comment nous avons traduit 10 millions de lignes de code C++ vers le standard C++14 (puis vers C++17)

Avertissement

Tout ce qui est écrit ci-dessous sur le travail lent/rapide, la (pas) grande consommation de mémoire par les implémentations de classes standard dans diverses bibliothèques signifie une chose : c'est vrai POUR NOUS. Il est fort possible que les implémentations standards soient les mieux adaptées à vos tâches. Nous sommes partis de nos propres tâches : nous avons pris des données typiques de nos clients, exécuté des scénarios typiques sur celles-ci, examiné les performances, la quantité de mémoire consommée, etc., et analysé si nous et nos clients étions satisfaits ou non de ces résultats. . Et ils ont agi en fonction de cela.

Ce que nous avions

Initialement, nous avons écrit le code de la plateforme 1C:Enterprise 8 dans Microsoft Visual Studio. Le projet a démarré au début des années 2000 et nous avions une version uniquement Windows. Naturellement, depuis lors, le code a été activement développé et de nombreux mécanismes ont été complètement réécrits. Mais le code a été écrit selon la norme de 1998, et, par exemple, nos crochets droits ont été séparés par des espaces pour que la compilation réussisse, comme ceci :

vector<vector<int> > IntV;

En 2006, avec la sortie de la version 8.1 de la plateforme, nous avons commencé à prendre en charge Linux et sommes passés à une bibliothèque standard tierce. Port STL. L'une des raisons de cette transition était de travailler avec des lignes larges. Dans notre code, nous utilisons std::wstring, qui est basé sur le type wchar_t, partout. Sa taille sous Windows est de 2 octets et sous Linux, la valeur par défaut est de 4 octets. Cela a conduit à une incompatibilité de nos protocoles binaires entre client et serveur, ainsi qu'à diverses données persistantes. En utilisant les options gcc, vous pouvez spécifier que la taille de wchar_t lors de la compilation est également de 2 octets, mais vous pouvez alors oublier d'utiliser la bibliothèque standard du compilateur, car il utilise la glibc, qui à son tour est compilée pour un wchar_t de 4 octets. D'autres raisons étaient une meilleure implémentation des classes standard, la prise en charge des tables de hachage et même l'émulation de la sémantique du déplacement à l'intérieur des conteneurs, que nous avons activement utilisée. Et une autre raison, comme on dit, était la performance des cordes. Nous avions notre propre cours pour les cordes, parce que... En raison des spécificités de notre logiciel, les opérations sur les chaînes sont très largement utilisées et pour nous, cela est essentiel.

Notre chaîne est basée sur des idées d'optimisation de chaîne exprimées au début des années 2000. Andreï Alexandrescu. Plus tard, quand Alexandrescu travaillait chez Facebook, à sa suggestion, une ligne a été utilisée dans le moteur Facebook qui fonctionnait sur des principes similaires (voir bibliothèque folie).

Notre ligne a utilisé deux technologies d'optimisation principales :

  1. Pour les valeurs courtes, un tampon interne dans l'objet chaîne lui-même est utilisé (ne nécessitant pas d'allocation de mémoire supplémentaire).
  2. Pour tous les autres, la mécanique est utilisée Copie sur écriture. La valeur de chaîne est stockée au même endroit et un compteur de référence est utilisé lors de l'affectation/modification.

Pour accélérer la compilation de la plateforme, nous avons exclu l'implémentation du flux de notre variante STLPort (que nous n'avons pas utilisée), cela nous a donné une compilation environ 20 % plus rapide. Par la suite, nous avons dû faire un usage limité Boost. Boost utilise beaucoup le flux, notamment dans ses API de service (par exemple pour la journalisation), nous avons donc dû le modifier pour supprimer l'utilisation du flux. Cela nous a rendu difficile la migration vers de nouvelles versions de Boost.

La troisième voie

Lors du passage à la norme C++14, nous avons envisagé les options suivantes :

  1. Mettez à niveau le STLPort que nous avons modifié vers la norme C++14. L'option est très difficile, car... la prise en charge de STLPort a été interrompue en 2010 et nous devions créer nous-mêmes tout son code.
  2. Transition vers une autre implémentation STL compatible avec C++14. Il est hautement souhaitable que cette implémentation soit pour Windows et Linux.
  3. Lors de la compilation pour chaque système d'exploitation, utilisez la bibliothèque intégrée au compilateur correspondant.

La première option a été carrément rejetée en raison de trop de travail.

Nous avons réfléchi pendant un certain temps à la deuxième option ; considéré comme candidat libc++, mais à cette époque, cela ne fonctionnait pas sous Windows. Pour porter libc++ sur Windows, vous devrez faire beaucoup de travail - par exemple, écrire vous-même tout ce qui concerne les threads, la synchronisation des threads et l'atomicité, puisque libc++ est utilisé dans ces domaines API POSIX.

Et nous avons choisi la troisième voie.

Transition

Nous avons donc dû remplacer l'utilisation de STLPort par les bibliothèques des compilateurs correspondants (Visual Studio 2015 pour Windows, gcc 7 pour Linux, clang 8 pour macOS).

Heureusement, notre code a été écrit principalement selon des directives et n'a pas utilisé toutes sortes d'astuces astucieuses, de sorte que la migration vers de nouvelles bibliothèques s'est déroulée relativement facilement, à l'aide de scripts qui ont remplacé les noms de types, de classes, d'espaces de noms et d'inclusions dans la source. des dossiers. La migration a concerné 10 000 fichiers sources (sur 14 000). wchar_t a été remplacé par char16_t ; nous avons décidé d'abandonner l'utilisation de wchar_t, car char16_t prend 2 octets sur tous les systèmes d'exploitation et ne gâche pas la compatibilité du code entre Windows et Linux.

Il y a eu quelques petites aventures. Par exemple, dans STLPort, un itérateur pouvait être implicitement converti en pointeur vers un élément, et à certains endroits de notre code, cela a été utilisé. Dans les nouvelles bibliothèques, cela n’était plus possible et ces passages devaient être analysés et réécrits manuellement.

Ainsi, la migration du code est terminée, le code est compilé pour tous les systèmes d'exploitation. C'est l'heure des tests.

Les tests après la transition ont montré une baisse des performances (à certains endroits jusqu'à 20-30 %) et une augmentation de la consommation de mémoire (jusqu'à 10-15 %) par rapport à l'ancienne version du code. Cela était notamment dû aux performances sous-optimales des chaînes standard. Nous avons donc dû à nouveau utiliser notre propre ligne, légèrement modifiée.

Une fonctionnalité intéressante de l'implémentation des conteneurs dans les bibliothèques intégrées a également été révélée : les std::map et std::set vides (sans éléments) des bibliothèques intégrées allouent de la mémoire. Et en raison des fonctionnalités d'implémentation, à certains endroits du code, de nombreux conteneurs vides de ce type sont créés. Les conteneurs de mémoire standard sont un peu alloués pour un élément racine, mais pour nous, cela s'est avéré critique - dans un certain nombre de scénarios, nos performances ont considérablement diminué et la consommation de mémoire a augmenté (par rapport à STLPort). Par conséquent, dans notre code, nous avons remplacé ces deux types de conteneurs des bibliothèques intégrées par leur implémentation de Boost, où ces conteneurs n'avaient pas cette fonctionnalité, ce qui a résolu le problème de ralentissement et d'augmentation de la consommation de mémoire.

Comme cela arrive souvent après des changements à grande échelle dans de grands projets, la première itération du code source n'a pas fonctionné sans problème, et ici, en particulier, la prise en charge des itérateurs de débogage dans l'implémentation Windows s'est avérée utile. Pas à pas, nous avons avancé et au printemps 2017 (version 8.3.11 1C:Enterprise), la migration était terminée.

Les résultats de

La transition vers le standard C++14 nous a pris environ 6 mois. La plupart du temps, un développeur (mais très hautement qualifié) a travaillé sur le projet et, au stade final, des représentants des équipes responsables de domaines spécifiques ont rejoint le groupe - interface utilisateur, cluster de serveurs, outils de développement et d'administration, etc.

La transition a grandement simplifié notre travail de migration vers les dernières versions de la norme. Ainsi, la version 1C:Enterprise 8.3.14 (en développement, sortie prévue en début d'année prochaine) a déjà été transférée au standard C++17.

Après la migration, les développeurs disposent de plus d'options. Si auparavant nous avions notre propre version modifiée de STL et un espace de noms std, nous avons maintenant des classes standard des bibliothèques de compilateur intégrées dans l'espace de noms std, dans l'espace de noms stdx - nos lignes et conteneurs optimisés pour nos tâches, en boost - le dernière version de boost. Et le développeur utilise les classes les mieux adaptées pour résoudre ses problèmes.

L’implémentation « native » des constructeurs de mouvements aide également au développement (déplacer les constructeurs) pour un certain nombre de classes. Si une classe a un constructeur de déplacement et que cette classe est placée dans un conteneur, alors la STL optimise la copie des éléments à l'intérieur du conteneur (par exemple, lorsque le conteneur est développé et qu'il est nécessaire de modifier la capacité et de réallouer de la mémoire).

Voler dans la pommade

La conséquence peut-être la plus désagréable (mais pas critique) de la migration est que nous sommes confrontés à une augmentation du volume fichiers obj, et le résultat complet de la construction avec tous les fichiers intermédiaires a commencé à occuper 60 à 70 Go. Ce comportement est dû aux particularités des bibliothèques standards modernes, devenues moins critiques quant à la taille des fichiers de service générés. Cela n'affecte pas le fonctionnement de l'application compilée, mais cela entraîne un certain nombre d'inconvénients lors du développement, notamment en augmentant le temps de compilation. Les besoins en espace disque libre sur les serveurs de build et sur les machines des développeurs augmentent également. Nos développeurs travaillent en parallèle sur plusieurs versions de la plateforme, et des centaines de gigaoctets de fichiers intermédiaires créent parfois des difficultés dans leur travail. Le problème est désagréable, mais pas critique, nous avons reporté sa solution pour l'instant. Nous considérons la technologie comme l'une des options pour le résoudre construire l'unité (Google l'utilise notamment lors du développement du navigateur Chrome).

Source: habr.com

Ajouter un commentaire