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?
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())
.
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.
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.
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 .
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.