web-dev-qa-db-fra.com

Comment fonctionne l'échange de variables XOR?

Quelqu'un peut-il m'expliquer comment XOR l'échange de deux variables sans variable temporaire fonctionne?

void xorSwap (int *x, int *y)
{
    if (x != y) {
        *x ^= *y;
        *y ^= *x;
        *x ^= *y;
    }
}

Je comprends ce qu'il fait, mais quelqu'un peut-il me guider dans la logique de son fonctionnement?

68
mmcdole

Vous pouvez voir comment cela fonctionne en effectuant la substitution:

x1 = x0 xor y0
y2 = x1 xor y0
x2 = x1 xor y2

Substitution,

x1 = x0 xor y0
y2 = (x0 xor y0) xor y0
x2 = (x0 xor y0) xor ((x0 xor y0) xor y0)

Parce que xor est entièrement associatif et commutatif:

y2 = x0 xor (y0 xor y0)
x2 = (x0 xor x0) xor (y0 xor y0) xor y0

Puisque x xor x == 0 pour tout x,

y2 = x0 xor 0
x2 = 0 xor 0 xor y0

Et depuis x xor 0 == x pour tout x,

y2 = x0
x2 = y0

Et l'échange est fait.

124
Greg Hewgill

D'autres personnes l'ont expliqué, maintenant je veux expliquer pourquoi c'était une bonne idée, mais ce n'est pas le cas maintenant.

À l'époque où nous avions des processeurs simples à cycle unique ou à cycles multiples, il était moins coûteux d'utiliser cette astuce pour éviter des déréférences de mémoire coûteuses ou des déversements de registres dans la pile. Cependant, nous avons désormais des processeurs avec d'énormes pipelines. Le pipeline du P4 variait de 20 à 31 (ou plus) étapes dans leurs pipelines, où toute dépendance entre la lecture et l'écriture dans un registre pouvait entraîner le blocage de l'ensemble. Le swap xor a des dépendances très lourdes entre A et B qui ne comptent pas du tout mais bloquent le pipeline dans la pratique. Un pipeline bloqué provoque un chemin de code lent, et si cet échange est dans votre boucle interne, vous allez vous déplacer très lentement.

Dans la pratique générale, votre compilateur peut déterminer ce que vous voulez vraiment faire lorsque vous effectuez un échange avec une variable temporaire et peut le compiler en une seule instruction XCHG. L'utilisation du swap xor rend beaucoup plus difficile pour le compilateur de deviner votre intention et donc beaucoup moins susceptible de l'optimiser correctement. Sans parler de la maintenance du code, etc.

91
Patrick

J'aime y penser graphiquement plutôt que numériquement.

Disons que vous commencez par x = 11 et y = 5 En binaire (et je vais utiliser une hypothétique machine 4 bits), voici x et y

       x: |1|0|1|1|   -> 8 + 2 + 1
       y: |0|1|0|1|   -> 4 + 1

Maintenant pour moi, XOR est une opération inversée et le faire deux fois est un miroir:

     x^y: |1|1|1|0|
 (x^y)^y: |1|0|1|1|   <- ooh!  Check it out - x came back
 (x^y)^x: |0|1|0|1|   <- ooh!  y came back too!
50
plinth

En voici une qui devrait être un peu plus facile à grogner:

int x = 10, y = 7;

y = x + y; //x = 10, y = 17
x = y - x; //x = 7, y = 17
y = y - x; //x = 7, y = 10

Maintenant, on peut comprendre le truc XOR un peu plus facilement en comprenant que ^ peut être considéré comme + o - . Tout comme:

x + y - ((x + y) - x) == x 

, alors:

x ^ y ^ ((x ^ y) ^ x) == x
33
Matt J

La raison pour laquelle cela fonctionne est parce que XOR ne perd pas d'informations. Vous pouvez faire la même chose avec l'addition et la soustraction ordinaires si vous pouvez ignorer le débordement. Par exemple, si la paire de variables A, B contient à l'origine les valeurs 1,2, vous pouvez les échanger comme ceci:

 // A,B  = 1,2
A = A+B // 3,2
B = A-B // 3,1
A = A-B // 2,1

BTW il y a une vieille astuce pour encoder une liste chaînée bidirectionnelle dans un seul "pointeur". Supposons que vous ayez une liste de blocs de mémoire aux adresses A, B et C. Le premier mot de chaque bloc est respectivement:

 // first Word of each block is sum of addresses of prior and next block
 0 + &B   // first Word of block A
&A + &C   // first Word of block B
&B + 0    // first Word of block C

Si vous avez accès au bloc A, il vous donne l'adresse de B. Pour accéder à C, vous prenez le "pointeur" de B et soustrayez A, et ainsi de suite. Cela fonctionne aussi bien à l'envers. Pour parcourir la liste, vous devez conserver les pointeurs sur deux blocs consécutifs. Bien sûr, vous utiliseriez XOR à la place de l'addition/soustraction, vous n'aurez donc pas à vous soucier du débordement.

Vous pouvez l'étendre à un "site Web lié" si vous voulez vous amuser.

12
Mike Dunlavey

La plupart des gens échangeraient deux variables x et y en utilisant une variable temporaire, comme ceci:

tmp = x
x = y
y = tmp

Voici une astuce de programmation pour échanger deux valeurs sans avoir besoin d'un temp:

x = x xor y
y = x xor y
x = x xor y

Plus de détails dans Swap two variables using XOR

Sur la ligne 1, nous combinons x et y (en utilisant XOR) pour obtenir cet "hybride" et nous le stockons en x. XOR est un excellent moyen de sauvegarder des informations, car vous pouvez les supprimer en faisant à nouveau un XOR.

Sur la ligne 2. Nous XOR l'hybride avec y, qui annule toutes les informations y, ne nous laissant qu'avec x. Nous sauvegardons ce résultat dans y, alors maintenant ils ont échangé.

Sur la dernière ligne, x a toujours la valeur hybride. Nous XOR encore une fois avec y (maintenant avec la valeur d'origine de x) pour supprimer toutes les traces de x de l'hybride. Cela nous laisse avec y, et l'échange est terminé!


L'ordinateur a en fait une variable "temp" implicite qui stocke les résultats intermédiaires avant de les réécrire dans un registre. Par exemple, si vous ajoutez 3 à un registre (en pseudocode en langage machine):

ADD 3 A // add 3 to register A

L'ALU (Arithmetic Logic Unit) est en fait ce qui exécute l'instruction 3 + A. Il prend les entrées (3, A) et crée un résultat (3 + A), que le CPU stocke ensuite dans le registre d'origine de A. Nous avons donc utilisé l'ALU comme espace de travail temporaire avant d'avoir la réponse finale.

Nous tenons les données temporaires implicites de l'ALU pour acquises, mais elles sont toujours là. De la même manière, l'ALU peut renvoyer le résultat intermédiaire du XOR dans le cas de x = x xor y, auquel cas le CPU le stocke dans le registre d'origine de x.

Parce que nous ne sommes pas habitués à penser à l'ALU pauvre et négligé, l'échange XOR semble magique car il n'a pas de variable temporaire explicite. Certaines machines ont une instruction XCHG d'échange en une étape pour échanger deux registres.

11
VonC

@ VonC a raison, c'est une astuce mathématique soignée. Imaginez des mots de 4 bits et voyez si cela aide.

Word1 ^= Word2;
Word2 ^= Word1;
Word1 ^= Word2;


Word1    Word2
0101     1111
after 1st xor
1010     1111
after 2nd xor
1010     0101
after 3rd xor
1111     0101
7
kenny

En passant, j'ai réinventé cette roue indépendamment il y a plusieurs années sous la forme d'un échange d'entiers en faisant:

a = a + b
b = a - b ( = a + b - b once expanded)
a = a - b ( = a + b - a once expanded).

(Ceci est mentionné ci-dessus d'une manière difficile à lire),

Le même raisonnement s'applique aux swaps xor: a ^ b ^ b = a et a ^ b ^ a = a. Puisque xor est commutatif, x ^ x = 0 et x ^ 0 = x, c'est assez facile à voir puisque

= a ^ b ^ b
= a ^ 0
= a

et

= a ^ b ^ a 
= a ^ a ^ b 
= 0 ^ b 
= b

J'espère que cela t'aides. Cette explication a déjà été donnée ... mais pas très clairement imo.

3
jheriko