Як працює обмін змінними XOR?


77

Хтось може пояснити мені, як працює обмін XOR двох змінних без змінної temp?

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

Я розумію, ЩО це робить, але чи може хтось провести мене через логіку того, як це працює?


8
Я думаю, що підмінна змінна xor засмоктує невпорядковані ядра виконання. Кожен наступний xor має залежність від читання-після-запису і повинен чекати завершення відповіді. для x86 вам краще просто кодувати як зазвичай. Компілятор повинен видавати щось гідне.
Calyth

Відповіді:


131

Ви можете побачити, як це працює, виконавши заміну:

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

Підставляючи,

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

Оскільки xor повністю асоціативний і комутативний:

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

Оскільки x xor x == 0для будь-якого х,

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

А оскільки x xor 0 == xдля будь-якого х,

y2 = x0
x2 = y0

І обмін зроблено.


Я не маю жодної ідеї, якщо ви побачите цей коментар через 11 років, але це найкраще пояснення, коли-небудь дякую!
Cantaff0rd

ближче до 12 років потому: як це працює із рядками (як при оберненні рядків)? Це тому, що ви не оперуєте значеннями ASCII, а скоріше двійковим поданням адрес пам'яті, що містять різні частини рядка?
bluppfisk 02

Я ледве втримуюся від бажання змінитись y2на y1. Це викликає мене , що у вас є x0і x1але потім використовувати y0і y2. : -]
Фріріх Раабе

96

Інші люди пояснювали це, тепер я хочу пояснити, чому це була гарна ідея, а зараз ні.

У той час, коли у нас були прості одноциклові або багатоциклові центральні процесори, було дешевше використовувати цей трюк, щоб уникнути дорогих переспрямувань пам'яті або переливання регістрів у стек. Однак зараз у нас є процесори з масивними конвеєрами. Конвеєр P4 варіювався від 20 до 31 (або близько того) етапів у своїх трубопроводах, де будь-яка залежність між читанням та записом у реєстрі могла призвести до того, що все це заглохне. Обмін xor має деякі дуже важкі залежності між A і B, які насправді взагалі не мають значення, але на практиці зупиняють трубопровід. Зупинений конвеєр спричиняє повільний шлях коду, і якщо цей своп знаходиться у вашому внутрішньому циклі, ви будете рухатися дуже повільно.

На загальній практиці ваш компілятор може зрозуміти, що ви насправді хочете зробити, коли виконуєте підкачку із змінною temp, і може скомпілювати її в одну інструкцію XCHG. Використання підкачки xor набагато важче для компілятора вгадати ваш намір і, отже, набагато менше шансів правильно його оптимізувати. Не кажучи вже про обслуговування коду тощо.


Так - як і всі трюки, що заощаджують пам’ять, це не так корисно в наші дні дешевої пам’яті.
Брюс Олдерман,

1
Однак, тим самим чином, вбудований системний процесор все ще має велику користь.
Пол Натан

1
@ Пол, це залежатиме від вашої ланцюжка інструментів. Спочатку я б протестував, щоб переконатися, що ваш вбудований компілятор ще не виконує цю оптимізацію.
Патрік

2
(Варто також зазначити, що з точки зору розміру, три XOR, швидше за все, більше, ніж один XCHG, залежно від архітектури. Ви можете заощадити більше місця, не використовуючи фокус xor.)
Патрік

54

Мені подобається думати про це графічно, а не числово.

Скажімо, ви починаєте з x = 11 та y = 5 У двійковій системі (і я збираюся використовувати гіпотетичну 4-бітову машину), ось x та y

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

Для мене XOR - це операція інвертування, і робити це двічі - дзеркально:

     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!

Дуже чітко. Після кожної операції XOR на кожному біті набагато легше зрозуміти, що відбувається. Я думаю, що важче зрозуміти XOR, оскільки на відміну від & та | операції, це набагато складніше зробити в голові. Арифметика XOR просто призводить до плутанини. Не бійтеся візуалізувати проблему. Компілятор готовий розрахувати математику, а не ви.
Мартін Шутт,

36

Ось такий, який має бути дещо простішим:

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

Тепер можна трохи легше зрозуміти фокус XOR, зрозумівши, що ^ можна сприймати як + або - . Так як:

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

, так:

x ^ y ^ ((x ^ y) ^ x) == x

@Matt J, дякую за приклад віднімання. Мені це допомогло мені це відчути.
mmcdole

3
Можливо, варто підкреслити, що ви не можете використовувати методи додавання або віднімання через переповнення великими числами.
MarkJ

Так це? У невеликих прикладах, які я розробив, все склалося нормально незалежно (припускаючи, що результат недотоку або переповнення є (результат% 2 ^ n)). Я можу щось кодувати, щоб перевірити.
Matt J

Я думаю, що, припускаючи найбільш економну апаратну реалізацію інструкцій ADD та SUB, це працює належним чином навіть за наявності переповнення або недоливу. Я щойно це перевірив. Мені чогось не вистачає?
Matt J

Я гадаю, якщо у вас немає винятків для переповнення та переповнення, це буде працювати, звичайно.
MarkJ

14

Більшість людей міняють місцями дві змінні x та y, використовуючи тимчасову змінну, наприклад:

tmp = x
x = y
y = tmp

Ось акуратний трюк програмування, щоб поміняти місцями два значення, не потребуючи тимчасового:

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

Детальніше про обмін двома змінними за допомогою XOR

У рядку 1 ми поєднуємо x та y (використовуючи XOR), щоб отримати цей "гібрид", і зберігаємо його назад у x. XOR - це чудовий спосіб зберегти інформацію, оскільки ви можете видалити її, повторивши XOR.

У рядку 2. Ми XOR гібрид з y, який скасовує всю інформацію y, залишаючи нас лише з x. Ми зберігаємо цей результат назад у, тому тепер вони помінялися місцями.

В останньому рядку x все ще має гібридне значення. Ми знову використовуємо XOR з y (тепер із початковим значенням x), щоб видалити всі сліди x з гібриду. Це залишає нас з y, і обмін завершено!


Комп’ютер насправді має неявну змінну “temp”, яка зберігає проміжні результати перед тим, як записати їх назад у реєстр. Наприклад, якщо ви додаєте 3 до реєстру (у псевдокоді на машинній мові):

ADD 3 A // add 3 to register A

ALU (блок арифметичної логіки) насправді є тим, що виконує інструкцію 3 + A. Він бере вхідні дані (3, A) і створює результат (3 + A), який процесор потім зберігає назад у вихідному реєстрі A. Отже, ми використовували ALU як тимчасове місце для подряпин до того, як отримали остаточну відповідь.

Ми сприймаємо неявні тимчасові дані ALU як належне, але вони завжди є. Подібним чином ALU може повертати проміжний результат XOR у випадку x = x xor y, після чого ЦП зберігає його в оригінальному регістрі x.

Оскільки ми не звикли думати про бідний, занедбаний ALU, обмін XOR видається чарівним, оскільки в ньому немає явної тимчасової змінної. Деякі машини мають інструкцію обміну XCHG з 1 кроком для обміну двома регістрами.


4
Я це розумію, запитую, як це працює. Як використання ексклюзиву або значення дозволяє вам поміняти його
місцями

Проголосував за те, що це найбільш чітка та детальна відповідь, але хочу зазначити, що своп із змінною temp набагато
зручніший

1
Мені сподобалась оригінальна відповідь (із поясненнями), але дещо про ALU здається помилковим. Навіть на одноциклових (неконвеєрних) процесорах, на які ви натякаєте, можливість робити "x = (op за участю x)" в 1 інструкції має більше спільного з тим, що файл реєстру має вхідні та вихідні порти.
Matt J

14

ЧОМУ це працює, тому що XOR не втрачає інформацію. Ви можете зробити те ж саме зі звичайним додаванням і відніманням, якби ви могли ігнорувати переповнення. Наприклад, якщо пара змінних A, B спочатку містить значення 1,2, ви можете поміняти їх місцями таким чином:

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

До речі, існує старий фокус для кодування двостороннього пов'язаного списку в одному "покажчику". Припустимо, у вас є список блоків пам'яті за адресами A, B і C. Першим словом у кожному блоці є відповідно:

 // 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

Якщо у вас є доступ до блоку A, він надає вам адресу B. Щоб дістатися до C, ви берете "вказівник" у B і віднімаєте A тощо. Це працює так само добре назад. Щоб пройти вздовж списку, потрібно тримати покажчики на два послідовних блоки. Звичайно, ви використовуєте XOR замість додавання / зменшення, тому вам не доведеться турбуватися про переповнення.

Ви можете поширити це на "пов'язану мережу", якщо хочете трохи повеселитися.


Трюк з одним покажчиком є ​​досить приголомшливим, я не знав про це! Дякую!
Gab Royer

1
@Gab: Ласкаво просимо, і ваші знання англійської набагато кращі за мою французьку!
Mike Dunlavey

1
Для підходу +/- +1 (хоча intпереповнення - UB)
chux - Поновити Моніку

7

@VonC це правильно, це акуратний математичний фокус. Уявіть 4-бітові слова і перевірте, чи це допомагає.

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

5

В основному є 3 етапи підходу XOR:

a '= a XOR b (1)
b' = a 'XOR b (2)
a' = a 'XOR b' (3)

Щоб зрозуміти, чому це працює, спочатку зверніть увагу на те, що:

  1. XOR видасть 1 лише тоді, коли рівно один з його операндів дорівнює 1, а інший - нуль;
  2. XOR є комутативним, тому XOR b = b XOR a;
  3. XOR асоціативний, тому (a XOR b) XOR c = a XOR (b XOR c); і
  4. a XOR a = 0 (це повинно бути очевидним із визначення в 1 вище)

Після кроку (1) двійкове представлення a матиме 1 біти лише в бітових положеннях, де a та b мають протилежні біти. Це або (ak = 1, bk = 0), або (ak = 0, bk = 1). Тепер, коли ми робимо заміну на кроці (2), отримуємо:

b '= (a XOR b) XOR b
= a XOR (b XOR b), оскільки XOR є асоціативним
= a XOR 0 через [4] вище
= a через визначення XOR (див. 1 вище)

Тепер ми можемо замінити на Крок (3):

a ”= (a XOR b) XOR a
= (b XOR a) XOR a, оскільки XOR є комутативним
= b XOR (a XOR a), оскільки XOR є асоціативним
= b XOR 0 через [4] вище
= b через визначення XOR (див. 1 вище)

Детальніша інформація тут: необхідна та достатня


3

Як додаткову примітку я кілька років тому самостійно винайшов це колесо у вигляді обміну цілих чисел, виконавши:

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

(Про це згадується вище важким для читання способом),

Точно такі ж міркування стосуються і xor-свопів: a ^ b ^ b = a і a ^ b ^ a = a. Оскільки xor є комутативним, x ^ x = 0 та x ^ 0 = x, це досить легко побачити з тих пір

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

і

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

Сподіваюся, це допомагає. Це пояснення вже було дано ... але не дуже чітко imo.


Waay пізно тут, але підписане ціле переповнення є невизначеною поведінкою в C та (старіші версії) C ++. Потенційно викликати UB просто для того, щоб "заощадити трохи місця" при обміні змінними - це дійсно погана ідея.
Ендрю Хенле,

3

Я просто хочу додати математичне пояснення, щоб відповідь була більш повною. У теорії груп XOR є абелевою групою , яку також називають комутативною групою. Це означає, що він відповідає п’яти вимогам: закриття, асоціативність, елемент ідентичності, зворотний елемент, комутативність.

Формула обміну XOR:

a = a XOR b
b = a XOR b
a = a XOR b 

Розгорніть формулу, підставте a, b попередньою формулою:

a = a XOR b
b = a XOR b = (a XOR b) XOR b
a = a XOR b = (a XOR b) XOR (a XOR b) XOR b

Комутативність означає "a XOR b", що дорівнює "b XOR a":

a = a XOR b
b = a XOR b = (a XOR b) XOR b
a = a XOR b = (a XOR b) XOR (a XOR b) XOR b 
            = (b XOR a) XOR (a XOR b) XOR b

Асоціативність означає "(a XOR b) XOR c", що дорівнює "a XOR (b XOR c)":

a = a XOR b
b = a XOR b = (a XOR b) XOR b 
            = a XOR (b XOR b)
a = a XOR b = (a XOR b) XOR (a XOR b) XOR b 
            = (b XOR a) XOR (a XOR b) XOR b 
            = b XOR (a XOR a) XOR (b XOR b)

Інверсний елемент у XOR - це сам, це означає, що будь-яке значення XOR саме по собі дає нуль:

a = a XOR b
b = a XOR b = (a XOR b) XOR b 
            = a XOR (b XOR b) 
            = a XOR 0
a = a XOR b = (a XOR b) XOR (a XOR b) XOR b 
            = (b XOR a) XOR (a XOR b) XOR b 
            = b XOR (a XOR a) XOR (b XOR b) 
            = b XOR 0 XOR 0

Елемент ідентичності в XOR дорівнює нулю, це означає, що будь-яке значення XOR з нулем залишається незмінним:

a = a XOR b
b = a XOR b = (a XOR b) XOR b 
            = a XOR (b XOR b) 
            = a XOR 0 
            = a
a = a XOR b = (a XOR b) XOR (a XOR b) XOR b 
            = (b XOR a) XOR (a XOR b) XOR b 
            = b XOR (a XOR a) XOR (b XOR b) 
            = b XOR 0 XOR 0 
            = b XOR 0
            = b

І ви можете отримати додаткову інформацію в теорії груп .


0

Інші розміщували пояснення, але я думаю, що це було б краще зрозуміти, якби це супроводжувалося хорошим прикладом.

Таблиця правди XOR

Якщо ми розглянемо вищезазначену таблицю істинності і візьмемо значення, A = 1100і B = 0101ми зможемо поміняти значення як такі:

A = 1100
B = 0101


A ^= B;     => A = 1100 XOR 0101
(A = 1001)

B ^= A;     => B = 0101 XOR 1001
(B = 1100)

A ^= B;     => A = 1001 XOR 1100
(A = 0101)


A = 0101
B = 1100
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.