Trao đổi biến XOR hoạt động như thế nào?


77

Ai đó có thể giải thích cho tôi cách hoán đổi XOR của hai biến không có biến tạm thời hoạt động như thế nào không?

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

Tôi hiểu nó làm GÌ, nhưng ai đó có thể hướng dẫn tôi qua logic về cách nó hoạt động không?


8
Tôi nghĩ rằng hoán đổi biến xor hút các lõi thực thi không theo thứ tự. Mỗi xor tiếp theo có một phụ thuộc đọc-sau-ghi và cần đợi câu trả lời hoàn tất. đối với x86, bạn chỉ nên viết mã như bình thường. Trình biên dịch sẽ phát ra một cái gì đó tốt.
Calyth

Câu trả lời:


131

Bạn có thể thấy nó hoạt động như thế nào bằng cách thực hiện thay thế:

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

Thay thế,

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

Bởi vì xor là hoàn toàn liên kết và giao hoán:

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

x xor x == 0với bất kỳ x,

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

Và vì x xor 0 == xbất kỳ x nào,

y2 = x0
x2 = y0

Và việc hoán đổi đã xong.


Tôi không có ý tưởng nếu bạn sẽ thấy bình luận này 11 năm sau nhưng đây là lời giải thích tốt nhất từng cảm ơn bạn!
Cantaff0rd

gần 12 năm sau: điều này hoạt động như thế nào với chuỗi (như trong đảo ngược chuỗi)? Có phải vì bạn không hoạt động trên các giá trị ASCII mà là trên biểu diễn nhị phân của các địa chỉ bộ nhớ chứa các phần khác nhau của chuỗi không?
bluppfisk

Tôi hầu như không thể cưỡng lại ý muốn thay đổi y2thành y1. Nó kích hoạt tôi rằng bạn có x0x1nhưng sau đó sử dụng y0y2. : -]
Frerich Raabe

96

Những người khác đã giải thích điều đó, bây giờ tôi muốn giải thích tại sao đó là một ý kiến ​​hay, nhưng bây giờ thì không.

Quay lại thời mà chúng ta có các CPU đơn chu kỳ hoặc đa chu kỳ, thì việc sử dụng thủ thuật này sẽ rẻ hơn để tránh tham chiếu bộ nhớ tốn kém hoặc tràn thanh ghi vào ngăn xếp. Tuy nhiên, thay vào đó, chúng ta có các CPU với đường ống lớn. Đường ống của P4 dao động từ 20 đến 31 (hoặc hơn) giai đoạn trong đường ống của chúng, nơi bất kỳ sự phụ thuộc nào giữa việc đọc và ghi vào một thanh ghi có thể khiến toàn bộ hoạt động bị đình trệ. Giao dịch hoán đổi xor có một số phụ thuộc rất lớn giữa A và B mà thực tế không quan trọng nhưng làm ngưng trệ đường ống trong thực tế. Một đường dẫn bị đình trệ gây ra một đường dẫn mã chậm và nếu sự hoán đổi này nằm trong vòng lặp bên trong của bạn, bạn sẽ di chuyển rất chậm.

Nói chung, trình biên dịch của bạn có thể tìm ra những gì bạn thực sự muốn làm khi thực hiện hoán đổi với một biến tạm thời và có thể biên dịch nó thành một lệnh XCHG duy nhất. Sử dụng hoán đổi xor khiến trình biên dịch khó đoán ý định của bạn hơn nhiều và do đó ít có khả năng tối ưu hóa nó một cách chính xác. Chưa kể đến việc bảo trì mã, v.v.


Đúng - giống như tất cả các thủ thuật tiết kiệm bộ nhớ, điều này không quá hữu ích trong những ngày bộ nhớ rẻ tiền.
Bruce Alderman

1
Tuy nhiên, cùng một mã thông báo, CPU hệ thống nhúng vẫn được hưởng lợi khá nhiều.
Paul Nathan

1
@Paul, nó phụ thuộc vào chuỗi công cụ của bạn. Tôi muốn kiểm tra nó trước để chắc chắn rằng trình biên dịch nhúng của bạn chưa thực hiện tối ưu hóa đó.
Patrick

2
(Cũng cần lưu ý rằng từ góc độ kích thước, ba XOR có thể lớn hơn một XCHG, tùy thuộc vào kiến ​​trúc. Bạn có thể tiết kiệm nhiều dung lượng hơn bằng cách không sử dụng thủ thuật xor.)
Patrick

54

Tôi thích nghĩ về nó bằng đồ thị hơn là số.

Giả sử bạn bắt đầu với x = 11 và y = 5 Trong hệ nhị phân (và tôi sẽ sử dụng máy 4 bit giả định), đây là x và y

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

Bây giờ với tôi, XOR là một hoạt động đảo ngược và thực hiện nó hai lần là một phản chiếu:

     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!

Rất rõ ràng. Việc theo dõi từng thao tác XOR trên từng bit giúp bạn hiểu được điều gì đang xảy ra dễ dàng hơn nhiều. Tôi nghĩ sẽ khó hiểu hơn về XOR vì không giống như & và | nó khó thực hiện hơn rất nhiều trong đầu bạn. Số học XOR chỉ dẫn đến sự nhầm lẫn. Đừng ngại hình dung vấn đề vì trình biên dịch ở đó để thực hiện phép toán, không phải bạn.
Martyn Shutt

36

Đây là một trong những thứ sẽ dễ tìm hiểu hơn một chút:

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

Bây giờ, người ta có thể hiểu thủ thuật XOR dễ dàng hơn một chút bằng cách hiểu ^ có thể được coi là + hoặc - . Cũng như:

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

, vì thế:

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

@Matt J, cảm ơn vì ví dụ về phép trừ. Nó đã giúp tôi tìm kiếm nó.
mmcdole 30/10/08

3
Có thể đáng để nhấn mạnh rằng bạn không thể sử dụng các phương pháp cộng hoặc trừ vì tràn số lớn.
MarkJ

Có phải vậy không? Trong các ví dụ nhỏ mà tôi đã tìm ra, mọi thứ đều diễn ra tốt đẹp bất kể (giả sử kết quả của dòng chảy dưới hoặc tràn là (kết quả% 2 ^ n)). Tôi có thể mã hóa một cái gì đó để kiểm tra nó.
Matt J

Tôi nghĩ rằng, giả sử việc triển khai phần cứng phức tạp nhất của các lệnh ADD và SUB, điều này hoạt động đúng ngay cả khi có tràn hoặc dòng chảy dưới. Tôi vừa mới thử nghiệm nó. Tui bỏ lỡ điều gì vậy?
Matt J

Tôi cho rằng nếu bạn không có ngoại lệ cho tràn & tràn thì chắc chắn nó sẽ hoạt động.
MarkJ

14

Hầu hết mọi người sẽ hoán đổi hai biến x và y bằng cách sử dụng một biến tạm thời, như sau:

tmp = x
x = y
y = tmp

Đây là một thủ thuật lập trình đơn giản để hoán đổi hai giá trị mà không cần tạm thời:

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

Thêm chi tiết trong Hoán đổi hai biến bằng XOR

Trên dòng 1, chúng tôi kết hợp x và y (sử dụng XOR) để có được "kết hợp" này và chúng tôi lưu trữ nó trở lại trong x. XOR là một cách tuyệt vời để lưu thông tin, vì bạn có thể xóa thông tin đó bằng cách thực hiện lại XOR.

Trên dòng 2. Chúng tôi XOR phép lai với y, loại bỏ tất cả thông tin y, chỉ để lại chúng tôi với x. Chúng tôi lưu kết quả này trở lại thành y, vì vậy bây giờ chúng đã đổi chỗ cho nhau.

Trên dòng cuối cùng, x vẫn có giá trị lai. Chúng tôi XOR nó một lần nữa với y (bây giờ với giá trị ban đầu của x) để loại bỏ tất cả các dấu vết của x ra khỏi phép lai. Điều này để lại cho chúng tôi y, và trao đổi hoàn tất!


Máy tính thực sự có một biến "tạm thời" ngầm lưu trữ các kết quả trung gian trước khi ghi chúng trở lại một thanh ghi. Ví dụ: nếu bạn thêm 3 vào một thanh ghi (bằng mã giả ngôn ngữ máy):

ADD 3 A // add 3 to register A

ALU (Đơn vị logic số học) thực sự là thứ thực thi lệnh 3 + A. Nó lấy các đầu vào (3, A) và tạo ra một kết quả (3 + A), sau đó CPU sẽ lưu trữ lại vào thanh ghi ban đầu của A. Vì vậy, chúng tôi đã sử dụng ALU làm khoảng trống tạm thời trước khi có câu trả lời cuối cùng.

Chúng tôi coi dữ liệu tạm thời ngầm định của ALU là điều hiển nhiên, nhưng nó luôn ở đó. Theo cách tương tự, ALU có thể trả về kết quả trung gian của XOR trong trường hợp x = x x hoặc y, lúc này CPU sẽ lưu nó vào thanh ghi ban đầu của x.

Bởi vì chúng ta không quen nghĩ về ALU nghèo, bị bỏ rơi, hoán đổi XOR có vẻ kỳ diệu vì nó không có một biến tạm thời rõ ràng. Một số máy có lệnh XCHG trao đổi 1 bước để hoán đổi hai thanh ghi.


4
Tôi hiểu điều đó, tôi đang hỏi nó hoạt động như thế nào. Làm thế nào để sử dụng độc quyền hoặc trên một giá trị cho phép bạn trao đổi nó mà không có một biến temp
mmcdole

Upvoted vì đây là câu trả lời rõ ràng và chi tiết nhất, nhưng muốn lưu ý rằng hoán đổi với một biến temp là rất nhiều dễ đọc hơn và nhờ đó mang nhiều giá trị hơn trong mã
eyelidlessness

1
Tôi thích câu trả lời ban đầu (có giải thích), nhưng một chút về ALU có vẻ bị hiểu sai. Ngay cả trên các bộ xử lý chu kỳ đơn (không pipelined) mà bạn ám chỉ, khả năng thực hiện "x = (op liên quan đến x)" trong 1 lệnh liên quan nhiều hơn đến thực tế là tệp thanh ghi có các cổng đầu vào đầu ra.
Matt J

14

Lý do TẠI SAO nó hoạt động là vì XOR không làm mất thông tin. Bạn có thể làm điều tương tự với phép cộng và phép trừ thông thường nếu bạn có thể bỏ qua phần tràn. Ví dụ: nếu cặp biến A, B ban đầu chứa các giá trị 1,2, bạn có thể hoán đổi chúng như sau:

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

BTW có một mẹo cũ để mã hóa danh sách liên kết 2 chiều trong một "con trỏ" duy nhất. Giả sử bạn có một danh sách các khối bộ nhớ tại các địa chỉ A, B và C. Từ đầu tiên trong mỗi khối lần lượt là:

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

Nếu bạn có quyền truy cập vào khối A, nó sẽ cung cấp cho bạn địa chỉ của B. Để đến C, bạn lấy "con trỏ" ở B và trừ đi A, v.v. Nó cũng hoạt động ngược lại. Để chạy dọc theo danh sách, bạn cần giữ các con trỏ đến hai khối liên tiếp. Tất nhiên, bạn sẽ sử dụng XOR thay cho phép cộng / trừ, vì vậy bạn sẽ không phải lo lắng về việc tràn.

Bạn có thể mở rộng điều này đến một "web được liên kết" nếu bạn muốn có một số niềm vui.


Thủ thuật con trỏ duy nhất khá tuyệt vời, không biết về điều này! Cảm ơn!
Gab Royer 27/09/09

1
@Gab: Bạn được chào đón và kỹ năng tiếng Anh của bạn tốt hơn tiếng Pháp của tôi rất nhiều!
Mike Dunlavey 28/09/09

1
Đối với +/- cách tiếp cận +1 (Mặc dù inttràn là UB)
Chux - Khôi phục Monica

7

@VonC nói đúng, đó là một thủ thuật toán học gọn gàng. Hãy tưởng tượng 4 từ bit và xem điều này có hữu ích không.

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

Về cơ bản, có 3 bước trong cách tiếp cận XOR:

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

Để hiểu tại sao điều này hoạt động, trước tiên hãy lưu ý rằng:

  1. XOR sẽ chỉ tạo ra 1 nếu chính xác một trong các toán hạng của nó là 1, và toán hạng còn lại là 0;
  2. XOR có tính chất giao hoán nên a XOR b = b XOR a;
  3. XOR là liên kết nên (a XOR b) XOR c = a XOR (b XOR c); và
  4. a XOR a = 0 (điều này phải rõ ràng từ định nghĩa trong 1 ở trên)

Sau Bước (1), biểu diễn nhị phân của a sẽ chỉ có 1-bit ở các vị trí bit mà a và b có các bit đối nhau. Đó là (ak = 1, bk = 0) hoặc (ak = 0, bk = 1). Bây giờ khi chúng ta thực hiện thay thế ở Bước (2), chúng ta nhận được:

b '= (a XOR b) XOR b
= a XOR (b XOR b) vì XOR là liên kết
= a XOR 0 vì [4] ở trên
= a do định nghĩa của XOR (xem 1 ở trên)

Bây giờ chúng ta có thể thay thế thành Bước (3):

a ”= (a XOR b) XOR a
= (b XOR a) XOR a vì XOR là giao hoán
= b XOR (a XOR a) vì XOR là liên kết
= b XOR 0 vì [4] trên
= b do định nghĩa của XOR (xem 1 ở trên)

Thông tin chi tiết hơn tại đây: Cần và Đủ


3

Như một lưu ý nhỏ, tôi đã phát minh lại bánh xe này một cách độc lập cách đây vài năm dưới dạng hoán đổi các số nguyên bằng cách thực hiện:

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

(Điều này được đề cập ở trên một cách khó đọc),

Lập luận tương tự áp dụng cho các hoán đổi xor: a ^ b ^ b = a và a ^ b ^ a = a. Vì xor là giao hoán, x ^ x = 0 và x ^ 0 = x, điều này khá dễ thấy vì

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

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

Hi vọng điêu nay co ich. Lời giải thích này đã được đưa ra ... nhưng không rõ ràng lắm.


Waay muộn ở đây, nhưng tràn số nguyên có dấu là hành vi không xác định trong C và (phiên bản cũ hơn của) C ++. Có thể gọi UB chỉ để "tiết kiệm không gian" khi hoán đổi các biến là một ý tưởng thực sự tồi.
Andrew Henle

3

Tôi chỉ muốn thêm một lời giải thích toán học để làm cho câu trả lời đầy đủ hơn. Theo lý thuyết nhóm , XOR là một nhóm abel , còn được gọi là nhóm giao hoán. Nó có nghĩa là nó thỏa mãn năm yêu cầu: Tính kết hợp, Tính liên kết, Tính đồng nhất, Phần tử nghịch đảo, Tính giao hoán.

Công thức hoán đổi XOR:

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

Mở rộng công thức, thay a, b bằng công thức trước:

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

Tính giao hoán có nghĩa là "a XOR b" bằng "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

Tính liên kết có nghĩa là "(a XOR b) XOR c" bằng "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)

Phần tử nghịch đảo trong XOR là chính nó, có nghĩa là bất kỳ giá trị nào XOR với chính nó đều cho bằng không:

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

Phần tử nhận dạng trong XOR bằng 0, có nghĩa là bất kỳ giá trị nào XOR bằng 0 đều không thay đổi:

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

Và bạn có thể lấy thêm thông tin trong lý thuyết nhóm .


0

Những người khác đã đăng giải thích nhưng tôi nghĩ rằng nó sẽ được hiểu rõ hơn nếu nó đi kèm với một ví dụ tốt.

Bảng sự thật XOR

Nếu chúng ta xem xét bảng sự thật ở trên và lấy các giá trị A = 1100B = 0101chúng ta có thể hoán đổi các giá trị như sau:

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.