Compilation fonctions constantes

en cpp

Le problème exposé dans ce sujet a été résolu.

Coucou,

Je me posais une question peut-être un peu bête.

Dans mon code, j’ai des fonctions que je fournis ensuite en paramètres. Est-ce que ces fonctions sont calculées puis passées en paramètres, ou alors passées en paramètres et recalculées à chaque fois ?

Par "calculée" j’entends par là le passage d’un truc du style

1
2
3
int a = 5;
int b = 6;
return a+b;
1
return 5+6;

Bon, bien sûr, là n’importe quel compilateur devrait faire le boulot. Mais mon code est un peu plus complexe qu’une simple addition de variables constantes, mais logiquement c’est la même chose qui se passe.


Extraits du code en question :

1
2
3
4
5
6
7
8
9
p1 = vector<complex<double>>(3,0.*i); 
p1[2] = 1. + 0.*i;
p1[0] = -0.123 + 0.745*i;
p2 = vector<complex<double>>(1,1. + 0.*i);

Polynome nume(p1), deno(p2);
//

fonctionRationnelle = [nume, deno](Homogene point){return evaluationAuPoint(nume, deno, point);}; // j'utilise fonction par la suite

C’est très peu probable que le compilateur s’autorise ce genre d’optimisation dans ton cas. Le plus gros problème, c’est que tes variables ne sont pas déclarés constantes. Si elles sont par malheur accessible depuis une autre unité de compilation, le compilateur n’a aucun moyen de savoir si elles sont modifié à un moment ou non. De plus, utiliser des vector veux dire que tu es sensé faire des allocations dynamiques, donc tu as des effets de bords, ce qui est assez gênant pour les optimisations. Par la même occasion, si tes valeurs p1 et p2 sont aussi déclarés non constantes et qu’elles sont accessible depuis une autre unité de compilation, il y a toujours des risques pour qu’elles soient modifiés autre part, ce qui empêche le compilateur de faire des optimisations.

J’ai fait quelques tests sur godbolt.org et il semblerais que les vecteurs soient un soucis majeur. Lorsque tu créé fonctionRationnelle, le compilateur est forcé de copier nume et deno parce que tu les prends par valeur dans ta lambda. Et apparemment ça casse bien les optimisations.

Voici le code que j’ai testé:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <array>
#include <complex>
#include <vector>

auto g1() {
    const std::vector<std::complex<double>> a{{1.0, 1.0}, {1.2, 1.5}};
    const std::vector<std::complex<double>> b{{1.0, 2.0}, {8.2, 5.5}};
    return [a, b](const double c) {
        return a[0] + a[1] + b[0] + b[1] + c;
    };
}

auto g2() {
    const std::array<std::complex<double>, 2> a{{{1.0, 1.0}, {1.2, 1.5}}};
    const std::array<std::complex<double>, 2> b{{{1.0, 2.0}, {8.2, 5.5}}};
    return [a, b](const double c) {
        return a[0] + a[1] + b[0] + b[1] + c;
    };
}

auto h1() {
    return g1()(5.0);
}

auto h2() {
    return g2()(5.0);
}

Avec clang 5.0, h1 est forcé de faire un certain nombre d’allocations et est particulièrement complexe. En comparaison, h2 est très simple et se contente d’utiliser les résultats pré-calculés.

Avec le lien que j’ai mis au début, tu peux aussi voir la différence entre main et f: dans main les variables sont public et non constantes, ce qui fait que le compilateur ne peux pas connaitre leur valeur au moment où main s’exécute. Pour f, les variables n’étant pas accessibles à une autre unité de compilation, il peut voir qu’elles ne sont effectivement constantes et optimise la fonction.

le compilateur est forcé de copier nume et deno parce que tu les prends par valeur dans ta lambda

Si je les passe en référence, ça évite de les copier ?

J’ai recodé à la main la fonction telle qu’elle pourrait être optimisé … et on est censé gagner un facteur 2, ce qui est énorme

Tu peux en effet passer nume et deno par référence, mais il faut alors faire bien attention à ce que la durée de vie de ces variables soient au moins aussi grande que celle de la lambda. Dans mon exemple, ce n’est pas le cas et le compilateur va se contenter de faire de la merde parce que tu accèdes à une référence vers une variable qui n’existe plus.

J’ai fait une autre exemple pour comparer des versions un peu plus complexes de g1 et g2: https://godbolt.org/g/ZGU236. De ce que j’arrive à voir, à chaque appel de g1 le compilateur va allouer les deux vecteurs (effet de bord forcé), va faire quelques préparations basés sur les valeurs dans les vecteurs qu’il sait constantes et fait le même genre de boucle que dans g2. Finalement, le compilateur désalloue les vecteurs. En comparaison, dans g2 seul la boucle est faite. À toi d’en tirer les conclusions qu’il te faut.

Si tout est constexpr/const avec des std::array en lieu et place des vecteur, ca doit se tenter (mais il faudrait un exemple minimal).

Car c’est tout l’intérêt du constexpr, de calculer à la compilation des choses pas triviales.

Edit: en fait les opérations constexpr sur les complexes ont été proposées 2016, j’ignore si elles ont étés acceptées pour C++17

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte