Sortie de GCC 6

Posté par  . Édité par bubar🦥, Davy Defaud, M5oul, patrick_g, palm123, ZeroHeure et Benoît Sibaud. Modéré par bubar🦥. Licence CC By‑SA.
91
23
avr.
2016
GNU

La sortie de la nouvelle version majeure du compilateur GCC du projet GNU va être annoncée. Écrit à l’origine par Richard Stallman, le logiciel GCC (GNU Compiler Collection) est le compilateur de référence du monde du logiciel libre. Il accepte des codes sources écrits en C, C++, Objective-C, Fortran, Java, Go et Ada et fonctionne sur une multitude d’architectures.

La suite de la dépêche vous propose en avance de phase une revue de certaines parties des améliorations et nouvelles fonctionnalités. Alors que GCC devenait un peu plus lent à chaque publication d’une nouvelle version, cette mouture marque un tournant en étant plus rapide que les deux versions précédentes, et plus rapide que d’autres compilateurs dans la plupart des situations, tout en générant souvent des binaires plus petits.

logo GCC

Sommaire

Introduction

De nouvelles optimisations, un raffinement de celles existantes, ainsi que de nouvelles fonctionnalités pour les langages et architectures prises en charge, sont à l’ordre du jour. L’amélioration de l’usage par les développeurs continue par l’optimisation des alertes et des outils à sa disposition.

GCC suit donc désormais un système de numérotation de versions majeures en .1 puis .x. Cette 6.1 est la première version stable, tout comme 5.1 le fut.

Nouvelles fonctionnalités à la compilation

En particulier, l’optimisation à l’édition des liens a été améliorée (tant pour les performances du code généré que les performances du compilateur), et on voit pour la première fois arriver la possibilité de décharger le processeur de certains calculs sur la puce graphique avec les puces d’AMD et la bibliothèque OpenMP !

Nouvelles optimisations

  • L’analyse d’aliasing de types gère maintenant mieux les accès à des pointeurs différents. Cela résulte en une amélioration des informations de type de l’ordre de 20 à 30 % pour certains programmes C++ de très haut niveau (donc avec des types complexes). Cette meilleure prédiction de type lors du déréférencement de pointeurs permet d’activer plusieurs optimisations.

    • Ces optimisations cassent bien sûr le code si les déréférencements se faisaient avec des constructions de typage ambigu (comme une union en C ou en utilisant reinterpret_cast en C++). Ce genre de programme pourrait maintenant avoir besoin de l’option -fno-strict-aliasing pour être compilé correctement. Les typages ambigus invalides sur des variables globales sont maintenant rapportées par l’alerte (warning) spécifique -Wodr-type-mismatch.
  • Cette même analyse prend maintenant en charge les attributs GNU weakref et alias, ce qui permet d’utiliser une variable et son alias dans une même unité de compilation, ce qui arrive souvent avec les optimisations à l’édition des liens.

  • La propagation de valeurs fait maintenant l’hypothèse que le pointeur this des classes C++ est non nul. Cela permet de se passer de nombreuses vérifications de non‐nullité de pointeurs, tout en cassant des bases de code qui se reposaient sur ce comportement indéfini du langage (comme Qt 5, Chromium et Kdevelop !). Il est possible d’utiliser -fno-delete-null-pointer-checks pour maintenir la compatibilité du code, et d’identifier les portions qui posent problème avec des tests dynamiques utilisant l’option -fsanitize=undefined.

  • Nouvelles fonctionnalités d’optimisation inter‐procédurales. Rappelons que le compilateur décide de l’inlining des fonctions : c’est‐à‐dire que le code va être dupliqué à chaque endroit où elle est appelée, gagnant ainsi un appel de fonction (et les sauvegarde et création de contexte de pile afférents), mais perdant en taille de code (et donc mettant plus de pression sur le cache et les décodeurs). Il décide aussi de cloner les fonctions : par exemple, si une fonction à deux arguments f(a, b) est appelée soit par f(1, b) soit par f(2, b), elle peut être clonée en deux fonctions différentes f1(b) et f2(b). Il faut bien sûr que le compilateur soit sûr de la valeur de a et cette information s’obtient par la propagation des constantes ou une analyse statique.

    • La passe d’inlining ou de clonage des fonctions repose sur des heuristiques de tailles et de durée d’exécution du code. Ces heuristiques sont maintenant plus précises grâce à une analyse des sauts, réalisée avant la construction du profil du programme.
    • Le clonage des fonctions élimine maintenant des paramètres des fonctions de manière plus agressive.

Améliorations des performances du code généré par les optimisations au moment de l’édition des liens (link‐time optimization, LTO).

  • Les attributs warning et error sont maintenant préservés à l’édition des liens, on peut donc compiler des programmes avec à la fois l’optimisation à l’édition des liens et l’amélioration de la robustesse des sources face aux attaques de type buffer / stack overflow permise par l’option -D_FORTIFY_SOURCE=2.

  • La fusion de types définie par le standard Fortran 2008 a été corrigée, permettant l’interopérabilité entre des programmes C et Fortran. Plus de détails sur ce point sont disponibles dans les notes de version sur le site de GCC.

  • Comme précisé plus haut, plus d’informations sur les types sont passées à l’édition des liens ce qui permet une meilleure précision sur les types lors de la LTO en cas d’aliasing.

  • La taille des fichiers d’objets LTO a été réduite : par exemple, sur Firefox 46.0, on gagne 11 %.

  • La parallélisation de la phase d’optimisation à l’édition des liens (qu’on active par l’option -flto=n) a été significativement améliorée : les données étudiées en partitionnant le programme (pour découper en blocs optimisables indépendamment et donc en parallèle). Par exemple, toujours sur Firefox 46.0, ces données ont été réduites de 66 % !

  • Le greffon de l’éditeur de liens (gold ou bfd) a été étendu de sorte à passer des informations sur le type de binaire généré par le back‐end de GCC. Cela permet de configurer le générateur de code pour prendre en charge une édition de liens (avec optimisation, bien sûr) incrémentale ! Il suffit de passer l’option -r à gcc et d’utiliser un éditeur de liens à greffons. Pour rappel, la prise en charge des greffons des éditeurs de liens à greffons a été activée dans la version précédente de GCC pour réduire la quantité d’information nécessaire à la phase de LTO qui était stockée dans les bibliothèques statiques (fichiers .a qui sont en réalité une collection de fichiers .o). Il n’y a cependant pas de magie, mais un nouveau compromis est possible :

    • soit on édite les liens avec ld -r, qui réalise la LTO lors de l’édition finale des liens et réalise avec les informations transmises individuellement sur chaque objets une optimisation globale (donc lente à l’édition des liens) sur le programme ;
    • soit on édite les liens avec gcc -r, qui produit l’objet binaire avec LTO sur les informations dont il dispose, mais qui ne reviendra pas sur les optimisations déjà réalisées quand il arrivera à l’édition finale des liens. Cette dernière édition des liens sera ainsi plus rapide mais manquera peut‐être des opportunités qu’une LTO globale aurait vues.

Honza Hubička, développeur LibreOffice, propose un article de comparaison de compilations entre différentes versions de GCC, mais aussi avec LLVM, en utilisant les LTO sur GCC 6.

Nouvelles informations sur les erreurs et alertes à la compilation

Peut‐être suite à la pression mise par clang (du projet LLVM) sur les aspects de facilitation du travail du programmeur, il y a déjà plusieurs années de cela, les développeurs de GCC améliorent depuis plusieurs versions les messages d’erreur en vue d’améliorer la productivité des utilisateurs. Voir en particulier cette publication détaillée de Mark Wielaard.

Quelques exemples :

  • après l’apparition de messages d’erreur plus explicites et en couleurs dans les versions précédentes, la version 6.1 de GCC indique maintenant l’ensemble des caractères qui posent problème plutôt qu’un seul ;
  • les messages d’erreur sont agrémentés de recommandations sur la manière de résoudre le problème (par exemple une faute de frappe sur un nom de variable ou remplacer . par -> sur un pointeur) ;
  • certaines fautes de frappe d’arguments sur la ligne de commande gcc sont maintenant détectées et une suggestion est faite à l’utilisateur (par exemple, tenter d’éditer des liens avec la bibliothèque static-fortran au lieu de static-gfortran) ;
  • le développeur est maintenant prévenu lorsqu’il effectue certaines comparaisons tautologiques ou encore lorsque des chaînes de if … else … if contiennent plusieurs fois la même condition ;
  • de nombreuses alertes supplémentaires sont levées. Parmi elles, les indentations trompeuses sont maintenant détectées et le compilateur lance une alerte avec le flag -Wmisleading-indentation. Le déni plausible d’Apple sur la faille goto fail; deviendra impossible à tenir à l’avenir. Concernant cette nouvelle alerte, voir la publication sur le blog Red Hat.

De manière générale il est fortement recommandé de compiler tout code avec -Wall -Wextra et de traiter toutes les alertes remontées par le compilateur ! Selon le principe « pas de fenêtre brisée » (no broken window), manquer de soin par petites touches sur un projet incite à prendre de moins en moins soin du code. Il devient vite peu fiable et très coûteux à maintenir et à faire évoluer.

Tests dynamiques de code

Dans la lignée des outils qui instrumentent le code, souvent portés depuis clang, et qui permettent de détecter des problèmes à l’exécution dans la famille des fsanitize= :

  • Une nouvelle option a été ajoutée parmi celles qui permettent de détecter certains problèmes lors de tests dynamiques au développement : on peut maintenant vérifier les bornes des tableaux C de type array de manière plus stricte qu’auparavant, via l’option -fsanitize=bounds-strict. Cela active la vérification déjà existante -fsanitize=bounds et instrumente le code pour les tableaux de longueur variable.

Nouvelles bibliothèques et fonctionnalités

  • Implémentation de la spécification d’OpenMP en version 4.5 pour les compilateurs C et C++.

  • Les compilateurs C/C++ permettent d’utiliser des attributs au sein des énumérations (par exemple, marquer via un attribut déprécié l’une des valeurs d’une énumération).

  • Le compilateur C++ suppose que le code est en C++ 2014 par défaut (contre C++ 98 auparavant). L’activation du C++ 14 strict se fait avec -std=c++14. Sinon on bénéficie des extensions GNU au langage, correspondant à -std=gnu++14.

  • Le compilateur et la bibliothèque standard C++ (libstdc++) proposent les concepts et quelques extensions du futur standard C++ 2017 de manière expérimentale. En particulier, les rapports techniques (fonctionnalités expérimentales considérées pour inclusion éventuelle dans les futures évolutions du standard) File Systems ou Library Fundamentals v2.

  • Améliorations de la bibliothèque libgccjit qui permet de compiler du code à la volée.

  • Prise en charge de la nouvelle bibliothèque standard C Musl sous Linux (architectures AArch64 / ARM / MIPS / PowerPC / i386 / x32 / x86_64). Rappelons que cette nouvelle bibliothèque se veut à la fois performante et très légère, ce qui permet de la compiler en statique dans les exécutables sans qu’ils grossissent démesurément.

Nouveautés sur les architectures gérées

Outre les habituelles dépréciations et/ou suppression d’architectures, pour lesquelles personnes ne s’est manifesté pour les maintenir, on note à côté les améliorations et nouveautés suivantes :

  • Améliorations pour les architectures ARM : on notera la prise en charge de l’option -march=native sous AArch64 (architectures ARM 64 bits) pour que GCC détecte tout seul le processeur sur lequel il est exécuté, afin d’optimiser le code spécifiquement pour lui.

  • Prise en charge du langage intermédiaire HSA (pour les systèmes AMD, généralement avec un processeur central et un processeur graphique Radeon intégré) : en utilisant une extension pour la bibliothèque OpenMP du projet GNU (libgomp), on peut transformer des constructions OpenMP simples en langage HSAIL, pour les exécuter sur les puces graphiques d’AMD dont le pilote prend ce langage en charge.

  • Prise en charge des instructions vectorielles AVX512 (donc, comme leur nom l’indique, sur 512 bits) pour les encore rares processeurs Intel Xeon de la génération Skylake.

  • Prise en charge des nouvelles instructions monitorx and mwaitx d’AMD. Elles sont similaires aux instructions monitor et mwait déjà prises en charge dans le (vieux) jeu d’instructions complémentaires SSE3, en ajoutant de nouvelles fonctionnalités (un compte à rebours) et un nouvel encodage. Ces instructions surveillent une zone mémoire et réveillent le processeur lors d’un accès ou, maintenant, quand le compte à rebours est expiré.

  • Prise en charge des futurs processeurs AMD fondés sur l’architecture Zen. Cette nouvelle architecture amd64 (x86-64) ne sera plus basée sur le type Bulldozer (avec deux cœurs d’exécution entiers avec chacun leur cache de données partageant un cache d’instructions, les unités de calcul sur les flottants et parfois les étages de décodage d’instructions) mais sera toute nouvelle (et partagera vraisemblablement des idées de conception avec les processeurs ARM 64 bits conçus par AMD). Espérons que cela permette à AMD de se relancer dans la course à des processeurs x86 64 bits performants !

  • Prise en charge initiale des processeurs POWER9 d’IBM. On en sait peu sur ces processeurs à l’heure actuelle, si ce n’est qu’ils reposeront sur la spécification OpenPOWER ISA 3.0, avec de nouvelles instructions vectorielles VSX-3 et un bus de transfert de données entre processeurs central et graphique baptisé NVLink et conçu par NVIDIA. Comme d’habitude avec les processeurs POWER d'IBM, on peut s’attendre à des monstres de puissance de calcul.

  • Prise en charge du processeur z13 d’IBM, ainsi que des améliorations pour systèmes IBM S/390.

En résumé, GCC 6 avec les LTO compile mieux, plus vite et sort des binaires plus petits que toutes les autres versions antérieures. Il dépasse également le compilateur clang du projet LLVM. Pour ce dernier, seule une ancienne version 3.5 compile plus vite, mais en produisant des binaires jusqu’à 20 % plus gros. Toutes les autres versions de LLVM sont dépassées par cette nouvelle version majeure de GCC. Quant à la dernière version de LLVM et son usage des LTO, GCC 6 lui donne une leçon, puisqu’il est jusqu’à 40 % plus rapide. LLVM travaille déjà dans sa version en développement à essayer de rattraper ce retard.

Aller plus loin

  • # Petite correction

    Posté par  . Évalué à 5.

    Les compilateurs C/C++ permettent d'utiliser des attributs sur les énumérations (par exemple, marquer un attribut comme déprécié au sein d'une énumération).

    Je suppose que ce sont des attributs sur les énumérateurs, non ? (après vérification, ça semble bien être ça). Les attributs sur les énumérations étaient déjà supportés en 5 (et peut-être même en 4.9).

    Donc c’est plutôt : « Les compilateurs C/C++ permettent d'utiliser des attributs au sein des énumérations (par exemple, marquer via un attribut déprécié l’une des valeurs d’une énumération). ».

    Sinon, ce sont de bonnes nouvelles tout ça. Clang garde quand même pour lui l’extensibilité et tous les outils (d’analyse statique ou de refactorisation, notamment) qui se greffent dessus, ou gcc évolue aussi de ce côté-là ?

    Mes commentaires sont en wtfpl. Une licence sur les commentaires, sérieux ? o_0

  • # Commentaire supprimé

    Posté par  . Évalué à 10.

    Ce commentaire a été supprimé par l’équipe de modération.

    • [^] # Re: LLVM vs GCC

      Posté par  (site web personnel) . Évalué à 6.

      Je n'utilise pas LLVM, mais un des liens donné dans la dépêche où le testeur fait des benchmarks de build de libreoffice montre que LLVM utilise beaucoup moins de RAM (6 Go vs 10 à 11 Go pour GCC). Juste pour remettre un peu de pondération.

      • [^] # Re: LLVM vs GCC

        Posté par  . Évalué à 2.

        Le développeur de LibreOffice explique qu'il lance 64 (!)jobs de compilation en parallèle avec make, ce qui explique cette consommation de mémoire moyennement élevée.

    • [^] # Re: LLVM vs GCC

      Posté par  . Évalué à 4.

      GCC date peut être de 1987, mais le compilateur des origines a été entièrement remplacé par EGCS qui date de la fin des années 90.

      LLVM aurait remplacé le "coeur" EGCS si la FSF n'avait pas eu une politique de saborder la modularité de GCC pour empêcher qu'on puisse y ajouter un frontend non libre.

      Résultat, LLVM poussé par Apple s'est envolé et le "vieux" GCC tente de résister.

      BeOS le faisait il y a 20 ans !

      • [^] # Re: LLVM vs GCC

        Posté par  . Évalué à 10.

        Résultat, LLVM poussé par Apple s'est envolé et le "vieux" GCC tente de résister.

        Il fait un peu plus que résister :)
        Il y a de la place pour différents compilateurs, il n'y a pas de mal à avoir du choix.

        Personnellement les dernières fois où j'ai fais du C++, je compilais avec LLVM lors des phases de développements et avec GCC pour les releases. Outre le fait que LLVM avait à l'époque de bien meilleurs messages d'erreur que gcc, ça permet de varier le compilateur et donc d'éviter de reposer sur une fonctionnalité (trop) spécifique.

        Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

        • [^] # Re: LLVM vs GCC

          Posté par  (site web personnel) . Évalué à 6.

          C'est une approche intéressante, par curiosité t'est-il arrivé d'avoir des bugs de production avec le binaire GCC que tu ne pouvais pas reproduire avec le binaire LLVM?

          • [^] # Re: LLVM vs GCC

            Posté par  . Évalué à 5.

            Non, mais j'étais loin de pousser le langage dans ses retranchements et au pire je pouvais toujours pour un cas donné travailler avec gcc.

            Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

      • [^] # Re: LLVM vs GCC

        Posté par  . Évalué à 4.

        LLVM est tres bien mais gcc supporte plus de langage et donc ce dernier est pas vraiment pres a mourir.

        • [^] # Re: LLVM vs GCC

          Posté par  . Évalué à 5.

          Il supporte aussi plus d'architectures et le code généré est (réputé) meilleur, mais je vois pas en quoi l'un turerait l'autre.

          Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

  • # Bien joué !

    Posté par  (site web personnel) . Évalué à 3.

    C'est cool que GCC avance bien. Même si c'est vrai que rentrer dans le code source et de rajouter un langage ou un architecture c'est très compliqué, c'est bien de voir qu'ils avancent à grands pas. En plus le support de la lib Musl est un très bon point pour ceux qui travaillent dans des environnements embarqués.

  • # libgccjit incompréhensible

    Posté par  . Évalué à 8. Dernière modification le 26 avril 2016 à 12:45.

    En lisant libgccjit, je me suis dis : « Super on va pouvoir intégrer du code directement en live. » Faille de sécurité garanti, mais plutôt rigolo à mettre en œuvre.

    Donc j’ai chercher libgccjit tutorial ce qui m’amène à ce hello world. Le code est le suivant :

        /* Smoketest example for libgccjit.so
           Copyright (C) 2014-2016 Free Software Foundation, Inc.
    
        This file is part of GCC.
    
        GCC is free software; you can redistribute it and/or modify it
        under the terms of the GNU General Public License as published by
        the Free Software Foundation; either version 3, or (at your option)
        any later version.
    
        GCC is distributed in the hope that it will be useful, but
        WITHOUT ANY WARRANTY; without even the implied warranty of
        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
        General Public License for more details.
    
        You should have received a copy of the GNU General Public License
        along with GCC; see the file COPYING3.  If not see
        <http://www.gnu.org/licenses/>.  */
    
        #include <libgccjit.h>
    
        #include <stdlib.h>
        #include <stdio.h>
    
        static void
        create_code (gcc_jit_context *ctxt)
        {
          /* Let's try to inject the equivalent of:
             void
             greet (const char *name)
             {
                printf ("hello %s\n", name);
             }
          */
          gcc_jit_type *void_type =
            gcc_jit_context_get_type (ctxt, GCC_JIT_TYPE_VOID);
          gcc_jit_type *const_char_ptr_type =
            gcc_jit_context_get_type (ctxt, GCC_JIT_TYPE_CONST_CHAR_PTR);
          gcc_jit_param *param_name =
            gcc_jit_context_new_param (ctxt, NULL, const_char_ptr_type, "name");
          gcc_jit_function *func =
            gcc_jit_context_new_function (ctxt, NULL,
                                          GCC_JIT_FUNCTION_EXPORTED,
                                          void_type,
                                          "greet",
                                          1, &param_name,
                                          0);
    
          gcc_jit_param *param_format =
            gcc_jit_context_new_param (ctxt, NULL, const_char_ptr_type, "format");
          gcc_jit_function *printf_func =
            gcc_jit_context_new_function (ctxt, NULL,
                          GCC_JIT_FUNCTION_IMPORTED,
                          gcc_jit_context_get_type (
                             ctxt, GCC_JIT_TYPE_INT),
                          "printf",
                          1, &param_format,
                          1);
          gcc_jit_rvalue *args[2];
          args[0] = gcc_jit_context_new_string_literal (ctxt, "hello %s\n");
          args[1] = gcc_jit_param_as_rvalue (param_name);
    
          gcc_jit_block *block = gcc_jit_function_new_block (func, NULL);
    
          gcc_jit_block_add_eval (
            block, NULL,
            gcc_jit_context_new_call (ctxt,
                                      NULL,
                                      printf_func,
                                      2, args));
          gcc_jit_block_end_with_void_return (block, NULL);
        }
    
        int
        main (int argc, char **argv)
        {
          gcc_jit_context *ctxt;
          gcc_jit_result *result;
    
          /* Get a "context" object for working with the library.  */
          ctxt = gcc_jit_context_acquire ();
          if (!ctxt)
            {
              fprintf (stderr, "NULL ctxt");
              exit (1);
            }
    
          /* Set some options on the context.
             Let's see the code being generated, in assembler form.  */
          gcc_jit_context_set_bool_option (
            ctxt,
            GCC_JIT_BOOL_OPTION_DUMP_GENERATED_CODE,
            0);
    
          /* Populate the context.  */
          create_code (ctxt);
    
          /* Compile the code.  */
          result = gcc_jit_context_compile (ctxt);
          if (!result)
            {
              fprintf (stderr, "NULL result");
              exit (1);
            }
    
          /* Extract the generated code from "result".  */
          typedef void (*fn_type) (const char *);
          fn_type greet =
            (fn_type)gcc_jit_result_get_code (result, "greet");
          if (!greet)
            {
              fprintf (stderr, "NULL greet");
              exit (1);
            }
    
          /* Now call the generated function: */
          greet ("world");
          fflush (stdout);
    
          gcc_jit_context_release (ctxt);
          gcc_jit_result_release (result);
          return 0;
        }

    Heu, je m’attendais plus à un truc contenant également l’interpréteur de source, un truc dans ce genre :

    const char source = "int hello (void) { int rc; rc = printf(\"Hello World\"); return rc;}"
    
    int test_fct()
    {
      /* (...) */
    
      /* Populate the context.  */
      compile_code (ctxt, source);
    
      /* Compile the code.  */
      result = gcc_jit_context_compile (ctxt);
      /* Check error */
    
      /* Extract the generated code from "result".  */
      typedef int(*fn_type) (void);
      fn_type greet =
            (fn_type)gcc_jit_result_get_code (result, "greet");
      /* Check error */
    
      /* Now call the generated function: */
      rc = greet ();
      fflush (stdout);
    
      gcc_jit_context_release (ctxt);
      gcc_jit_result_release (result);
      return 0;
    }

    On donne une chaine de caractère de code C, et on récupère du code assemblé pouvant être exécuté directement.

    J’ai surement raté un truc.

    • [^] # Re: libgccjit incompréhensible

      Posté par  . Évalué à 3.

      De ce que j'ai cru comprendre, GCCJIT permet d'accéder au back-end de GCC et permet donc d'implémenter un interpréteur pour ton propre langage, pas de réutiliser les front-end existant (C, C++, ADA, Go)… C'est plus proche de LLVM que de Clang si tu veux.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.