web-dev-qa-db-fra.com

Puis-je suggérer l'optimiseur en donnant la plage d'un entier?

J'utilise un type int pour stocker une valeur. Selon la sémantique du programme, la valeur varie toujours dans une très petite plage (0 - 36), et int (pas un char) est utilisé uniquement en raison de l'efficacité du processeur.

Il semble que de nombreuses optimisations arithmétiques spéciales puissent être effectuées sur une gamme aussi petite d’entiers. De nombreux appels de fonction sur ces entiers peuvent être optimisés dans un petit ensemble d'opérations "magiques", et certaines fonctions peuvent même être optimisées dans des recherches de table.

Alors, est-il possible de dire au compilateur que ce int est toujours dans cette petite plage, et est-il possible pour le compilateur de faire ces optimisations?

171
rolevax

Oui c'est possible. Par exemple, pour gcc, vous pouvez utiliser __builtin_unreachable Pour indiquer au compilateur des conditions impossibles, comme suit:

if (value < 0 || value > 36) __builtin_unreachable();

Nous pouvons envelopper la condition ci-dessus dans une macro:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

Et utilisez-le comme ceci:

assume(x >= 0 && x <= 10);

Comme vous pouvez le voir , gcc effectue des optimisations en fonction de ces informations:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Produit:

func(int):
    mov     eax, 17
    ret

Un inconvénient, cependant, est que si votre code casse de telles hypothèses, vous obtenez un comportement indéfini .

Il ne vous avertit pas lorsque cela se produit, même dans les versions de débogage. Pour déboguer/tester/intercepter plus facilement les bogues contenant des hypothèses, vous pouvez utiliser une macro hybride assum/assert (crédit de @David Z), comme celle-ci:

#if defined(NDEBUG)
#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)
#else
#include <cassert>
#define assume(cond) assert(cond)
#endif

Dans les versions de débogage (avec NDEBUG non défini défini), cela fonctionne comme un ordinaire assert, message d'erreur d'impression et abort 'ing, et dans les versions récentes, il utilise une hypothèse, produisant un code optimisé.

Notez, cependant, que ce n'est pas un substitut pour assert - cond reste dans les versions de publication, vous ne devriez donc pas faire quelque chose comme assume(VeryExpensiveComputation()).

227
deniss

Il existe un support standard pour cela. Ce que vous devez faire est d'inclure stdint.h (cstdint) puis utilisez le type uint_fast8_t.

Cela indique au compilateur que vous utilisez uniquement des nombres compris entre 0 et 255, mais que vous pouvez utiliser un type plus grand si le code est plus rapide. De même, le compilateur peut supposer que la variable n'aura jamais une valeur supérieure à 255 et ensuite procéder aux optimisations en conséquence.

60
Lundin

La réponse actuelle est bonne pour le cas où vous savez à coup sûr quelle est la plage, mais si vous voulez quand même un comportement correct quand la valeur est en dehors de la plage attendue, alors cela ne fonctionnera pas.

Pour ce cas, j'ai trouvé cette technique peut fonctionner:

if (x == c)  // assume c is a constant
{
    foo(x);
}
else
{
    foo(x);
}

L'idée est un compromis code-données: vous déplacez 1 bit de données (si x == c) dans la logique de commande .
Ceci indique à l'optimiseur que x est en fait une constante connue c, l'encourageant à s'inscrire en ligne et à optimiser le premier appel de foo séparément du reste , peut-être assez lourdement.

Assurez-vous de factoriser le code dans un seul sous-programme foo, cependant - ne dupliquez pas le code.

Exemple:

Pour que cette technique fonctionne, vous devez avoir un peu de chance - il existe des cas où le compilateur décide de ne pas évaluer les choses de manière statique, et ils sont un peu arbitraires. Mais quand ça marche, ça marche bien:

#include <math.h>
#include <stdio.h>

unsigned foo(unsigned x)
{
    return x * (x + 1);
}

unsigned bar(unsigned x) { return foo(x + 1) + foo(2 * x); }

int main()
{
    unsigned x;
    scanf("%u", &x);
    unsigned r;
    if (x == 1)
    {
        r = bar(bar(x));
    }
    else if (x == 0)
    {
        r = bar(bar(x));
    }
    else
    {
        r = bar(x + 1);
    }
    printf("%#x\n", r);
}

Il suffit d'utiliser -O3 et notez les constantes pré-évaluées 0x20 et 0x30e in la sortie de l'assembleur .

9
Mehrdad

Je me contenterai de dire que si vous voulez une solution plus standard, vous pouvez utiliser le [[noreturn]] _ attribut pour écrire votre propre unreachable.

Donc, je vais ré-utiliser deniss ' excellent exemple pour démontrer:

namespace detail {
    [[noreturn]] void unreachable(){}
}

#define assume(cond) do { if (!(cond)) detail::unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Ce qui comme vous pouvez le voir , donne un code presque identique:

detail::unreachable():
        rep ret
func(int):
        movl    $17, %eax
        ret

L'inconvénient est bien sûr que vous obtenez un avertissement qu'un [[noreturn]] La fonction revient en effet.

5
StoryTeller