• [^] # Re: C'est pourtant évident.

    Posté par . En réponse à la dépêche Coder efficacement, bonnes pratiques et erreurs à éviter. Évalué à 7.

    C'est faux. GCC le fait, ICC le fait

    Le C peut le faire ?? Je me coucherai moins bête ce soir !
    C’est imprécis mais c’est pas faux.

    Non, ce n'est pas imprécis. tu dis que les langages impératifs ne permettent pas l'optimisation des fonctions récursives terminales. C'est factuellement faux, car comme le dit Turbo, l'important est de savoir si la forme est récursive « simple » ou bien récursive terminale. Et ceci est indépendant de la nature du langage (fonctionnel, impératif, etc.). En fait, les algorithmes qui transforment une récursion terminale en boucle sont connus depuis un moment, et il y a une équivalence automatique. Donc les compilateurs pour langages comme OCaml (ou F#), ou comme Scala, ou même Common LISP, qui sont tous multi-paradigmes, pourraient tous implémenter la récursion terminale en théorie. Ensuite, pour prendre l'exemple de Scala/Clojure/Java, il n'y a rien, ni dans la JVM, ni dans les langages eux-mêmes, qui empêche la transformation d'une fonction récursive terminale du type :

    let fact n = 
     let rec factorial n acc = match n with
     | 0 | 1 -> acc
     | _ -> factorial (n-1) (n*acc)
     factorial n 1

    ... en un code bas-niveau du genre (en pseudo-C) :

    int 
    fact(int n)
    {
     int acc = 1;
    FACTORIAL:
     if (n == 0 || n == 1)
     return acc;
     acc *= n;
     n -= 1;
     goto FACTORIAL;
    }

    Étant donné que la JVM a une instruction goto pour le bytecode, transformer une fonction récursive terminale en boucle à l'aide d'un goto est trivial. Par contre il est possible que la machine à pile qui est à la base de la JVM ajoute des contraintes sur la façon de générer le code (je ne suis pas du tout expert ni même connaisseur du fonctionnement de la JVM).

    Concernant Python, Ruby, etc., il y a plein d'optimisations qui ne sont jamais faites dans ces langages, dues à la nature dynamique de ceux-ci (dynamique en termes de types, mais aussi pour la génération de code elle-même). Un truc qui serait optimisé dans beaucoup de cas en C+libC serait par exemple1 :

    /* Terrible, terrible way of copying strings. Assumes NULL-terminated strings, makes no checks, etc. */
    char* bad_strcpy(char *dst, const char *src)
    {
     for (size_t i = 0; i < strlen(src); ++i) 
     dst[i] = src[i];
     return dst;
    }

    Dans cet exemple, le compilateur C a le droit de transformer le code ainsi :

    /* Terrible, terrible way of copying strings. Assumes NULL-terminated strings, makes no checks, etc. */
    char* bad_strcpy(char *dst, const char *src)
    {
     size_t src_len = strlen(src);
     for (size_t i = 0; i < src_len; ++i) 
     dst[i] = src[i];
     return dst;
    }

    En Perl/Ruby/Python, de par la nature dynamique de ces langages, il faut nécessairement effectuer cette optimisation à la main. Maintenant, voilà le côté rigolo de ces langages : je connais mal Python, mais j'ai pas mal programmé avec Perl, et un peu avec Ruby. De ce que je vois, aucun de ces langages n'est purement impératif : ils ont tous des constructions fonctionnelles (par exemple : map et grep en Perl). Ils proposent des trucs genre les fermetures (closures en Anglais), qui sont apparues avec les premiers langages fonctionnels. De même, des langages impératifs destinés à la programmation parallèle, comme Habanero (Habanero Java, Habanero C, qui sont des dérivés sur langage X10) proposent des trucs qu'on trouve généralement dans les langages fonctionnels, comme le principe de future — et qui se trouve désormais aussi dans le standard de C++11. Tiens, en parlant de C++, la méta-programmation par templates se fait en effectuant de la programmation fonctionnelle (tout est « write once », tout est valeur).

    Enfin, j'ai une dernière remarque : les ordinateurs qui suivent le modèle d'exécution de von Neumann2 sont par définition des machines qui fonctionnent selon un principe impératif/itératif : il y a un compteur de programme (PC), qui est incrémenté à chaque cycle (ou bien, en cas de branchement, à qui on affecte une nouvelle adresse pour la prochaine instruction). Il n'y a aucune trace de comportement fonctionnel. Tout le génie de ceux qui proposent des langages comme les dialectes de LISP, ML, Haskell, etc., est justement de proposer une façon d'exprimer les programmes sous une forme de bien plus haut niveau, non-impérative (et donc, permettant au programmeur de ne pas penser en termes de ce qu'attend la machine), mais de malgré tout réussir à convertir ces programmes à nouveau sous une forme impérative compréhensible par une machine de von Neumann de façon efficace.

    Backus (le papa de Fortran) a d'ailleurs expliqué qu'il avait commis une grande erreur en créant Fortran, et que le futur devrait se construire sur la programmation fonctionnelle (le lien que je donne est son discours/sa présentation donné lors de son acceptation pour le prix Turing).

    [1] Comme la libC est standard, elle est du coup « magique ». Le compilateur a le droit de faire des trucs étranges avec tant que ça respecte la sémantique des fonctions standard.
    [2] Soit 99% des ordinateurs de la planète à travers les âges. Il y a des machines différentes, qui n'utilisent pas du tout les concepts de compteur de programme, mais elles ont malheureusement toutes échoué à dépasser les autres en termes de performances.