В компьютерном программировании исключающая или побитовая операция (иногда сокращается до XOR-обмена ) — это алгоритм , который использует исключающую или побитовую операцию для обмена значений двух переменных без использования временной переменной, которая обычно требуется.
Алгоритм в первую очередь является новинкой и способом демонстрации свойств операции «исключительное или» . Иногда его обсуждают как оптимизацию программы , но практически нет случаев, когда замена через «исключительное или» дает преимущество по сравнению со стандартной, очевидной техникой.
Обычный обмен требует использования временной переменной хранения. Однако при использовании алгоритма обмена XOR временное хранение не требуется. Алгоритм выглядит следующим образом: [1] [2]
X := Y XOR X ; // Выполняем операцию XOR над значениями и сохраняем результат в X Y := X XOR Y ; // Выполняем операцию XOR над значениями и сохраняем результат в Y X := Y XOR X ; // Выполняем операцию XOR над значениями и сохраняем результат в X
Поскольку XOR является коммутативной операцией , либо X XOR Y, либо Y XOR X могут быть использованы взаимозаменяемо в любой из трех предыдущих строк. Обратите внимание, что на некоторых архитектурах первый операнд инструкции XOR указывает целевое местоположение, в котором сохраняется результат операции, что предотвращает эту взаимозаменяемость. Алгоритм обычно соответствует трем инструкциям машинного кода , представленным соответствующим псевдокодом и инструкциями по ассемблеру в трех строках следующей таблицы:
Псевдокод | Сборка IBM System/370 | x86 сборка | Сборка RISC-V |
---|---|---|---|
X := X XOR Y | XR R1,R2 | xor eax, ebx | xor x10, x11 |
Y := Y XOR X | XR R2,R1 | xor ebx, eax | xor x11, x10 |
X := X XOR Y | XR R1,R2 | xor eax, ebx | xor x10, x11 |
В приведенном выше примере кода ассемблера System/370 R1 и R2 являются отдельными регистрами , и каждая XR
операция оставляет свой результат в регистре, указанном в первом аргументе. При использовании ассемблера x86 значения X и Y находятся в регистрах eax и ebx (соответственно), а xor
результат операции помещается в первый регистр. В ассемблере RISC-V значения X и Y находятся в регистрах X10 и X11, а xor
результат операции помещается в первый регистр (то же самое, что и X86)
Однако в псевдокоде или версии или реализации на языке высокого уровня алгоритм терпит неудачу, если x и y используют одно и то же место хранения, поскольку значение, хранящееся в этом месте, будет обнулено первой инструкцией XOR, а затем останется нулевым; оно не будет «поменяно местами с самим собой». Это не то же самое, что если x и y имеют одинаковые значения. Проблема возникает только тогда, когда x и y используют одно и то же место хранения, в этом случае их значения уже должны быть равны. То есть, если x и y используют одно и то же место хранения, то строка:
X := X XOR Y
устанавливает x в ноль (потому что x = y, поэтому X XOR Y равно нулю) и устанавливает y в ноль (так как он использует одно и то же место хранения), в результате чего x и y теряют свои исходные значения.
Бинарная операция XOR над битовыми строками длины проявляет следующие свойства (где обозначает XOR): [a]
Предположим, что у нас есть два различных регистра R1
и , R2
как в таблице ниже, с начальными значениями A и B соответственно. Мы выполняем операции, указанные ниже, последовательно и уменьшаем наши результаты, используя свойства, перечисленные выше.
Шаг | Операция | Регистрация 1 | Регистрация 2 | Снижение |
---|---|---|---|---|
0 | Начальное значение | — | ||
1 | R1 := R1 XOR R2 | — | ||
2 | R2 := R1 XOR R2 | Л2 Л4 Л3 | ||
3 | R1 := R1 XOR R2 | Л1 Л2 Л4 Л3 |
Поскольку XOR можно интерпретировать как двоичное сложение, а пару бит можно интерпретировать как вектор в двумерном векторном пространстве над полем с двумя элементами , шаги в алгоритме можно интерпретировать как умножение на матрицы 2×2 над полем с двумя элементами. Для простоты предположим изначально, что x и y — это отдельные биты, а не битовые векторы.
Например, шаг:
X := X XOR Y
который также имеет неявное:
Д := Д
соответствует матрице как
Последовательность операций тогда выражается как:
(работа с двоичными значениями, поэтому ), которая выражает элементарную матрицу переключения двух строк (или столбцов) в терминах трансвекций (сдвигов) добавления одного элемента к другому.
Для обобщения, когда X и Y являются не отдельными битами, а векторами битов длины n , эти матрицы 2×2 заменяются блочными матрицами 2 n ×2 n , такими как
Эти матрицы оперируют значениями, а не переменными (с местами хранения), поэтому такая интерпретация абстрагируется от вопросов места хранения и проблемы того, что обе переменные совместно используют одно и то же место хранения.
Функция C , реализующая алгоритм обмена XOR:
void XorSwap ( int * x , int * y ) { if ( x == y ) return ; * x ^= * y ; * y ^= * x ; * x ^= * y ; }
Код сначала проверяет, являются ли адреса различными, и использует защитное предложение для раннего выхода из функции, если они равны. Без этой проверки, если бы они были равны, алгоритм свернул бы в тройку, *x ^= *x
что дало бы ноль.
Алгоритм обмена XOR также можно определить с помощью макроса:
#define XORSWAP_UNSAFE(a, b) \ ((a) ^= (b), (b) ^= (a), \ (a) ^= (b)) /* Не работает, если a и b являются одним и тем же объектом — в этом случае объекту присваивается ноль \ (0) */ #define XORSWAP(a, b) \ ((&(a) == &(b)) ? (a) /* Проверка на наличие различных адресов */ \ : XORSWAP_UNSAFE(a, b))
На современных архитектурах ЦП техника XOR может быть медленнее, чем использование временной переменной для выполнения подкачки. По крайней мере на последних процессорах x86, как AMD, так и Intel, перемещение между регистрами регулярно вызывает нулевую задержку. (Это называется MOV-устранением.) Даже если нет ни одного архитектурного регистра, доступного для использования, инструкция XCHG
будет по крайней мере такой же быстрой, как три XOR вместе взятые. Другая причина заключается в том, что современные ЦП стремятся выполнять инструкции параллельно через конвейеры инструкций . В технике XOR входы для каждой операции зависят от результатов предыдущей операции, поэтому они должны выполняться в строго последовательном порядке, что сводит на нет любые преимущества параллелизма на уровне инструкций . [3]
XOR-обмен также осложняется на практике псевдонимами . Если попытаться поменять содержимое некоторого местоположения с самим собой с помощью XOR, то это место будет обнулено, а его значение потеряно. Поэтому XOR-обмен не должен использоваться вслепую в языке высокого уровня, если псевдонимы возможны. Эта проблема не возникает, если метод используется в ассемблере для обмена содержимым двух регистров.
Похожие проблемы возникают при вызове по имени , как в устройстве Дженсена , где замена i
и A[i]
через временную переменную дает неверные результаты из-за связанной аргументы: замена через temp = i; i = A[i]; A[i] = temp
изменяет значение i
во втором операторе, что затем приводит к неверному i
значению A[i]
в третьем операторе.
Основной принцип алгоритма обмена XOR может быть применен к любой операции, соответствующей критериям L1–L4 выше. Замена XOR на сложение и вычитание дает несколько отличающихся, но в значительной степени эквивалентных формулировок. Например: [4]
void AddSwap ( беззнаковое целое * x , беззнаковое целое * y ) { * x = * x + * y ; * y = * x - * y ; * x = * x - * y ; }
В отличие от обмена XOR, этот вариант требует, чтобы базовый процессор или язык программирования использовали метод, такой как модульная арифметика или bignums, чтобы гарантировать, что вычисление X + Y
не может вызвать ошибку из-за целочисленного переполнения . Поэтому на практике он встречается еще реже, чем обмен XOR.
Однако реализация AddSwap
вышеизложенного на языке программирования C всегда работает даже в случае целочисленного переполнения, поскольку, согласно стандарту C, сложение и вычитание беззнаковых целых чисел следуют правилам модульной арифметики , т.е. выполняются в циклической группе , где — число бит . Действительно, корректность алгоритма следует из того факта, что формулы и справедливы в любой абелевой группе . Это обобщает доказательство для алгоритма обмена XOR: XOR — это и сложение, и вычитание в абелевой группе (которая является прямой суммой s копий ) .unsigned int
Это не выполняется при работе с signed int
типом (по умолчанию для int
). Переполнение знакового целого числа — это неопределенное поведение в C, и поэтому модульная арифметика не гарантируется стандартом, что может привести к неверным результатам.
Последовательность операций AddSwap
можно выразить через умножение матриц следующим образом:
На архитектурах, не имеющих специальной инструкции обмена, поскольку она избегает дополнительного временного регистра, для оптимального распределения регистров требуется алгоритм обмена XOR . Это особенно важно для компиляторов, использующих статическую форму одиночного присваивания для распределения регистров; эти компиляторы иногда создают программы, которым нужно поменять местами два регистра, когда ни один из регистров не свободен. Алгоритм обмена XOR позволяет избежать необходимости резервировать дополнительный регистр или выгружать какие-либо регистры в основную память. [5] Вариант сложения/вычитания также может использоваться для той же цели. [6]
Этот метод распределения регистров особенно актуален для компиляторов шейдеров GPU . На современных архитектурах GPU сброс переменных является дорогостоящим из-за ограниченной пропускной способности памяти и высокой задержки памяти, в то время как ограничение использования регистров может повысить производительность из-за динамического разбиения файла регистров . Поэтому алгоритм обмена XOR требуется некоторыми компиляторами GPU. [7]