Pourquoi les compilateurs C ++ n'optimisent-ils pas cette affectation booléenne conditionnelle comme une affectation inconditionnelle?

117

Considérez la fonction suivante:

void func(bool& flag)
{
    if(!flag) flag=true;
}

Il me semble que si l'indicateur a une valeur booléenne valide, cela équivaudrait à le définir inconditionnellement true, comme ceci:

void func(bool& flag)
{
    flag=true;
}

Pourtant, ni gcc ni clang ne l'optimisent de cette façon - les deux génèrent les éléments suivants au -O3niveau de l'optimisation:

_Z4funcRb:
.LFB0:
    .cfi_startproc
    cmp BYTE PTR [rdi], 0
    jne .L1
    mov BYTE PTR [rdi], 1
.L1:
    rep ret

Ma question est la suivante: est-ce simplement que le code est trop spécial pour être optimisé, ou y a-t-il de bonnes raisons pour lesquelles une telle optimisation serait indésirable, étant donné qu'il flagne s'agit pas d'une référence volatile? Il semble que la seule raison qui pourrait être est que cela flagpourrait d'une manière ou d'une autre avoir une valeur non- trueou- falsesans comportement indéfini au moment de la lecture, mais je ne suis pas sûr que cela soit possible.

Ruslan
la source
8
Avez-vous des preuves qu'il s'agit d'une "optimisation"?
David Schwartz
1
@ 200_success Je ne pense pas que mettre une ligne de code avec un balisage non fonctionnel comme titre soit une bonne chose. Si vous voulez un titre plus spécifique, très bien, mais choisissez une phrase en anglais et essayez d'éviter le code (par exemple, pourquoi les compilateurs n'optimisent-ils pas les écritures conditionnelles en écritures inconditionnelles quand ils peuvent prouver qu'elles sont équivalentes? Ou similaires). De plus, comme les backticks ne sont pas rendus, ne les utilisez pas dans le titre même si vous utilisez du code.
Bakuriu
2
@Ruslan, bien qu'il ne semble pas faire cette optimisation pour la fonction elle-même, quand il peut intégrer le code, il semble le faire pour la version intégrée. Il en résulte souvent simplement une constante de temps de compilation 1. godbolt.org/g/swe0tc
Evan Teran

Réponses:

102

Cela peut avoir un impact négatif sur les performances du programme en raison de considérations de cohérence du cache . Ecrire à flagchaque fois func()est appelé salirait la ligne de cache contenant. Cela se produira indépendamment du fait que la valeur en cours d'écriture correspond exactement aux bits trouvés à l'adresse de destination avant l'écriture.


ÉDITER

hvd a fourni une autre bonne raison qui empêche une telle optimisation. C'est un argument plus convaincant contre l'optimisation proposée, car elle peut entraîner un comportement indéfini, alors que ma réponse (originale) ne traitait que des aspects de performance.

Après un peu plus de réflexion, je peux proposer un autre exemple pour lequel les compilateurs devraient être fortement interdits - à moins qu'ils ne puissent prouver que la transformation est sûre pour un contexte particulier - d'introduire l'écriture inconditionnelle. Considérez ce code:

const bool foo = true;

int main()
{
    func(const_cast<bool&>(foo));
}

Avec une écriture inconditionnelle, func()cela déclenche définitivement un comportement indéfini (l'écriture dans la mémoire morte mettra fin au programme, même si l'effet de l'écriture serait autrement un no-op).

Léon
la source
7
Cela peut également avoir un impact positif sur les performances puisque vous vous débarrassez d'une branche. Je ne pense donc pas que ce cas particulier ait un sens à discuter sans un système très spécifique à l'esprit.
Lundin
3
La définition du comportement @Yakk n'est pas affectée par la plate-forme cible. Dire qu'il mettra fin au programme est incorrect, mais l'UB lui-même peut avoir des conséquences profondes, y compris des démons nasaux.
John Dvorak
16
@Yakk Cela dépend de ce que l'on entend par "mémoire en lecture seule". Non, ce n'est pas dans une puce ROM, mais c'est très souvent dans une section chargée sur une page qui n'a pas d'accès en écriture activé, et vous obtiendrez par exemple un signal SIGSEGV ou une exception STATUS_ACCESS_VIOLATION lorsque vous essayez d'écrire dessus.
Random832
5
"cela déclenche définitivement un comportement indéfini". Non. Le comportement non défini est une propriété de la machine abstraite. C'est ce que dit le code qui détermine si UB est présent. Les compilateurs ne peuvent pas le provoquer (bien que s'il est bogué, un compilateur peut entraîner un comportement incorrect des programmes).
Eric M Schmidt
7
C'est le rejet de constpour passer dans une fonction qui peut modifier les données qui sont la source du comportement indéfini, pas l'écriture inconditionnelle. Docteur, ça fait mal quand je fais ça ....
Spencer
48

Outre la réponse de Léon sur les performances:

Supposons que flagc'est true. Supposons que deux threads appellent constamment func(flag). La fonction telle qu'elle est écrite, dans ce cas, ne stocke rien dans flag, donc cela doit être thread-safe. Deux threads accèdent à la même mémoire, mais uniquement pour la lire. Définir inconditionnellement flagsur truesignifie que deux threads différents écriront dans la même mémoire. Ce n'est pas sûr, c'est dangereux même si les données en cours d'écriture sont identiques aux données déjà présentes.


la source
9
Je pense que c'est le résultat de l'application [intro.races]/21.
Griwes
10
Très intéressant. J'ai donc lu ceci comme suit: Le compilateur n'est jamais autorisé à "optimiser" une opération d'écriture là où la machine abstraite n'en aurait pas.
Martin Ba
3
@MartinBa Surtout. Mais si le compilateur peut prouver que cela n'a pas d'importance, par exemple parce qu'il peut prouver qu'aucun autre thread ne pourrait avoir accès à cette variable particulière, alors cela peut être correct.
13
Ceci n'est dangereux que si le système ciblé par le compilateur le rend dangereux . Je n'ai jamais développé sur un système où l'écriture 0x01sur un octet qui est déjà 0x01provoque un comportement "dangereux". Sur un système avec accès à la mémoire word ou dword, il le ferait; mais l'optimiseur doit en être conscient. Sur un système d'exploitation de PC ou de téléphone moderne, aucun problème ne se produit. Ce n'est donc pas une raison valable.
Yakk - Adam Nevraumont
4
@Yakk En fait, en pensant encore plus, je pense que c'est juste après tout, même pour les processeurs courants. Je pense que vous avez raison lorsque le processeur peut écrire directement dans la mémoire, mais supposons qu'il se flagtrouve dans une page de copie sur écriture. Maintenant, au niveau du processeur, le comportement peut être défini (défaut de page, laissez le système d'exploitation le gérer), mais au niveau du système d'exploitation, il peut encore être indéfini, non?
13

Je ne suis pas sûr du comportement de C ++ ici, mais en C, la mémoire peut changer car si la mémoire contient une valeur non nulle autre que 1, elle resterait inchangée avec la vérification, mais changée à 1 avec la vérification.

Mais comme je ne maîtrise pas très bien le C ++, je ne sais pas si cette situation est même possible.

glglgl
la source
Serait-ce toujours vrai _Bool?
Ruslan
5
En C, si la mémoire contient une valeur que l'ABI ne dit pas valide pour son type, alors c'est une représentation de trappe, et la lecture d'une représentation de trappe est un comportement indéfini. En C ++, cela ne peut se produire que lors de la lecture d'un objet non initialisé et lors de la lecture d'un objet non initialisé qui est UB. Mais si vous pouvez trouver un ABI qui indique que toute valeur non nulle est valide pour le type bool/ _Boolet signifie true, alors dans cette ABI particulière, vous avez probablement raison.
1
@Ruslan Avec les compilateurs qui utilisent l'Itanium ABI, et sur les processeurs ARM, C _Boolet C ++ boolsont soit du même type, soit des types compatibles qui suivent les mêmes règles. Avec MSVC, ils ont la même taille et le même alignement, mais il n'y a pas de déclaration officielle indiquant s'ils utilisent les mêmes règles.
Justin Time - Réintègre Monica
1
@JustinTime: C's <stdbool.h>comprend untypedef _Bool bool; Et oui, sur x86 (au moins dans l'ABI System V), bool/ _Booldoivent être 0 ou 1, avec les bits supérieurs de l'octet effacés. Je ne pense pas que cette explication soit plausible.
Peter Cordes
1
@JustinTime: C'est vrai, j'aurais dû juste souligner qu'il a certainement la même sémantique dans toutes les saveurs x86 de l'ABI System V, c'est sur quoi portait cette question. (Je peux dire parce que le premier argument à a funcété passé dans RDI, alors que Windows utiliserait RDX).
Peter Cordes