Puis-je indiquer l'optimiseur en donnant la plage d'un entier?

173

J'utilise un inttype 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 a char) n'est utilisée qu'en raison de l'efficacité du processeur.

Il semble que de nombreuses optimisations arithmétiques spéciales puissent être effectuées sur une si petite plage d'entiers. De nombreux appels de fonction sur ces nombres entiers peuvent être optimisés en 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 intc'est toujours dans cette petite plage, et est-il possible pour le compilateur de faire ces optimisations?

rolevax
la source
4
des optimisations de plage de valeurs existent dans de nombreux compilateurs, par exemple. llvm mais je ne connais aucun indice de langage pour le déclarer.
Remus Rusanu
2
Notez que si vous n'avez jamais de nombres négatifs, vous pourriez avoir de petits gains à utiliser des unsignedtypes car ils sont plus faciles à raisonner pour le compilateur.
user694733
4
@RemusRusanu: Pascal vous permet de définir des types de sous-plages , par exemple var value: 0..36;.
Edgar Bonet
7
" int (pas un caractère) est utilisé uniquement parce que l'efficacité du processeur. " Ce vieux morceau de sagesse conventionnelle n'est généralement pas très vrai. Les types étroits doivent parfois être étendus à zéro ou à un signe sur toute la largeur du registre, esp. lorsqu'il est utilisé comme index de tableau, mais parfois cela se produit gratuitement. Si vous disposez d'un tableau de ce type, la réduction de l'encombrement du cache l'emporte généralement sur tout le reste.
Peter Cordes
1
J'ai oublié de dire: intet unsigned intdoivent également être étendus de 32 à 64 bits sur la plupart des systèmes avec des pointeurs 64 bits. Notez que sur x86-64, les opérations sur les registres 32 bits s'étendent de zéro à 64 bits gratuitement (pas d'extension de signe, mais le débordement signé est un comportement non défini, de sorte que le compilateur peut simplement utiliser des mathématiques signées 64 bits s'il le souhaite). Ainsi, vous ne voyez que des instructions supplémentaires pour étendre les arguments de fonction 32 bits à zéro, pas les résultats du calcul. Vous le feriez pour des types non signés plus étroits.
Peter Cordes

Réponses:

230

Oui c'est possible. Par exemple, car gccvous pouvez utiliser __builtin_unreachablepour informer le compilateur des conditions impossibles, comme ceci:

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 , gcceffectue 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, que si votre code rompt un jour ces 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 / attraper les bogues avec des hypothèses plus facilement, vous pouvez utiliser une macro hybride assume / assert (crédits à @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 ( NDEBUG sans définition), il fonctionne comme un assertmessage d'erreur d'impression ordinaire et abortun programme d'ingénierie, et dans les versions de version, il utilise une hypothèse, produisant un code optimisé.

Notez, cependant, qu'il ne remplace pas les versions régulières assert- condreste dans les versions de version, vous ne devriez donc pas faire quelque chose comme assume(VeryExpensiveComputation()).

Peter Mortensen
la source
5
@Xofo, je ne l'ai pas compris, dans mon exemple cela se produit déjà, car la return 2branche a été éliminée du code par le compilateur.
6
Cependant, il semble que gcc ne puisse pas optimiser les fonctions en opérations magiques ou en recherche de table comme prévu par OP.
jingyu9575
19
@ user3528438, __builtin_expectest un indice non strict. __builtin_expect(e, c)devrait se lire comme " eest le plus susceptible d'évaluer c", et peut être utile pour optimiser la prédiction de branche, mais cela ne se limite pas eà toujours c, donc ne permet pas à l'optimiseur de rejeter d'autres cas. Regardez comment les branches sont organisées en assemblage .
6
En théorie, tout code qui provoque inconditionnellement un comportement indéfini pourrait être utilisé à la place de __builtin_unreachable().
CodesInChaos
14
À moins qu'il y ait une bizarrerie que je ne connais pas qui fasse de cette idée une mauvaise idée, il pourrait être judicieux de combiner cela avec assert, par exemple définir assumecomme assertquand NDEBUGn'est pas défini, et comme __builtin_unreachable()quand NDEBUGest défini. De cette façon, vous bénéficiez de l'hypothèse dans le code de production, mais dans une version de débogage, vous avez toujours une vérification explicite. Bien sûr, vous devez alors faire suffisamment de tests pour vous assurer que l'hypothèse sera satisfaite à l'état sauvage.
David Z
61

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

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

Lundin
la source
2
Ces types ne sont pas utilisés autant qu'ils devraient l'être (j'ai personnellement tendance à oublier qu'ils existent). Ils donnent un code à la fois rapide et portable, assez brillant. Et ils existent depuis 1999.
Lundin
C'est une bonne suggestion pour le cas général. La réponse de deniss montre une solution plus malléable pour des scénarios spécifiques.
Courses de légèreté en orbite
1
Le compilateur obtient uniquement les informations de plage 0-255 sur les systèmes où uint_fast8_test en fait un type 8 bits (par exemple unsigned char) comme sur x86 / ARM / MIPS / PPC ( godbolt.org/g/KNyc31 ). Au début du DEC Alpha avant 21164A , les chargements / magasins d'octets n'étaient pas pris en charge, donc toute implémentation sensée serait utilisée typedef uint32_t uint_fast8_t. AFAIK, il n'y a pas de mécanisme pour qu'un type ait des limites de plage supplémentaires avec la plupart des compilateurs (comme gcc), donc je suis à peu près certain uint_fast8_tqu'il se comporterait exactement comme unsigned intou quoi que ce soit dans ce cas.
Peter Cordes
( boolest spécial, et est limité à 0 ou 1, mais c'est un type intégré, non défini par les fichiers d'en-tête en termes de char, sur gcc / clang. Comme je l'ai dit, je ne pense pas que la plupart des compilateurs aient un mécanisme cela rendrait cela possible.)
Peter Cordes
1
Quoi qu'il en soit, uint_fast8_tc'est une bonne recommandation, car il utilisera un type 8 bits sur les plates-formes où c'est aussi efficace que unsigned int. (Je ne suis pas vraiment sûr de ce que les fasttypes sont censés être rapide pour , et si le compromis entre cache l' empreinte est censé faire partie.). x86 a un support étendu pour les opérations d'octet, même pour faire l'ajout d'octets avec une source de mémoire, vous n'avez donc même pas à faire une charge séparée à extension zéro (ce qui est également très bon marché). gcc crée uint_fast16_tun type 64 bits sur x86, ce qui est insensé pour la plupart des utilisations (contre 32 bits). godbolt.org/g/Rmq5bv .
Peter Cordes
8

La réponse actuelle est bonne pour le cas où vous savez avec certitude quelle est la plage, mais si vous voulez toujours un comportement correct lorsque la valeur est en dehors de la plage attendue, cela ne fonctionnera pas.

Dans ce cas, j'ai trouvé que 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 (que ce soit x == c) dans la logique de contrôle .
Cela fait allusion à l'optimiseur qui xest en fait une constante connue c, l'encourageant à intégrer et à optimiser le premier appel de fooséparément du reste, peut-être assez lourdement.

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

Exemple:

Pour que cette technique fonctionne, vous devez être un peu chanceux - il y a des cas où le compilateur décide de ne pas évaluer les choses de manière statique, et ils sont plutôt 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);
}

Utilisez simplement -O3et notez les constantes pré-évaluées 0x20et 0x30edans la sortie de l'assembleur .

user541686
la source
Voulez-vous pas if (x==c) foo(c) else foo(x)? Si seulement pour attraper les constexprimplémentations de foo?
MSalters
@MSalters: Je savais que quelqu'un allait demander ça !! J'ai inventé cette technique avant constexprétait une chose et je n'ai jamais pris la peine de la «mettre à jour» par la suite (même si je n'ai jamais vraiment pris la peine de m'inquiéter, constexprmême après), mais la raison pour laquelle je ne l'ai pas fait au départ était que je voulais faciliter la tâche du compilateur pour les factoriser en tant que code commun et supprimer la branche s'il décidait de les laisser comme des appels de méthode normaux et de ne pas les optimiser. Je m'attendais à ce que si j'introduisais, cil soit vraiment difficile pour le compilateur de c (désolé, mauvaise blague) que les deux soient le même code, bien que je ne l'ai jamais vérifié.
user541686
4

Je veux juste dire que si vous voulez une solution plus standard en C ++, vous pouvez utiliser l' [[noreturn]]attribut pour écrire la vôtre unreachable.

Je vais donc réutiliser l' excellent exemple de deniss 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 recevez un avertissement indiquant qu'une [[noreturn]]fonction retourne effectivement.

Conteur - Unslander Monica
la source
Cela fonctionne avec clang, quand ma solution originale ne fonctionne pas , donc une belle astuce et +1. Mais tout cela dépend beaucoup du compilateur (comme Peter Cordes nous l'a montré, icccela peut aggraver les performances), donc ce n'est toujours pas universellement applicable. Remarque mineure également: la unreachabledéfinition doit être disponible pour l'optimiseur et intégrée pour que cela fonctionne .