【问题标题】:How does XOR variable swapping work?XOR 变量交换如何工作?
【发布时间】:2010-09-19 22:47:45
【问题描述】:

有人可以向我解释一下没有临时变量的两个变量的异或交换是如何工作的吗?

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

我了解它的作用,但有人可以告诉我它的工作原理吗?

【问题讨论】:

  • 我认为 xor 变量交换对乱序执行核心很不利。每个后续的 xor 都有一个 read-after-write 依赖,并且需要等待 answer 完成。对于 x86,你最好像往常一样编码。编译器应该发出一些像样的东西。

标签: language-agnostic bit-manipulation xor


【解决方案1】:

你可以通过替换来看看它是如何工作的:

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 对任何 x,

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

因为x xor 0 == x 对任何 x,

y2 = x0
x2 = y0

交换完成。

【讨论】:

  • 我不知道你是否会在 11 年后看到这条评论,但这是有史以来最好的解释,谢谢!
  • 接近 12 年后:如何处理字符串(如字符串反转)?是不是因为您不是对 ASCII 值进行操作,而是对包含字符串各个部分的内存地址的二进制表示进行操作?
  • 我几乎无法抗拒将y2 更改为y1 的冲动。它触发我你有x0x1,但随后使用y0y2。 :-]
【解决方案2】:

其他人已经解释过了,现在我想解释一下为什么这是个好主意,但现在不是。

在我们拥有简单的单周期或多周期 CPU 的那一天,使用这个技巧来避免代价高昂的内存取消引用或将寄存器溢出到堆栈中会更便宜。但是,我们现在拥有具有大量流水线的 CPU。 P4 的流水线在其流水线中有 20 到 31 个(左右)级不等,其中读取和写入寄存器之间的任何依赖都可能导致整个事情停止。 xor 交换在 A 和 B 之间有一些非常重的依赖关系,这些依赖关系实际上根本不重要,但在实践中会停止流水线。停滞的管道会导致代码路径变慢,如果此交换位于您的内部循环中,您的移动速度将非常缓慢。

在一般实践中,当您使用临时变量进行交换时,您的编译器可以确定您真正想要做什么,并且可以将其编译为单个 XCHG 指令。使用异或交换使编译器更难猜测您的意图,因此更不可能正确优化它。更不用说代码维护等了。

【讨论】:

  • 是的 - 就像所有节省内存的技巧一样,这在内存便宜的时代并不是那么有用。
  • 同理,嵌入式系统cpus还是受益良多。
  • @Paul,这取决于您的工具链。我会先对其进行测试,以确保您的嵌入式编译器尚未执行该优化。
  • (还值得注意的是,从大小的角度来看,三个 XOR 可能大于一个 XCHG,具体取决于架构。不使用 xor 技巧可能会节省更多空间。)
【解决方案3】:

我喜欢用图形而不是数字来思考它。

假设您从 x = 11 和 y = 5 开始 在二进制中(我将使用假设的 4 位机器),这里是 x 和 y

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

现在对我来说,异或是一个反转操作,做两次就是一面镜子:

     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 算法只会导致混乱。不要害怕将问题可视化。编译器是用来做数学的,而不是你。
【解决方案4】:

这是一个应该稍微容易理解的:

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,感谢您提供减法示例。它确实帮助我理解它。
  • 可能值得强调的是,由于大数溢出,您不能使用加法或减法方法。
  • 是这样吗?在我制定的小例子中,无论如何(假设下溢或溢出的结果是(结果%2^n)),事情都解决了。我可能会编写一些代码来测试它。
  • 我认为,假设 ADD 和 SUB 指令的硬件实现最简洁,即使存在上溢或下溢,它也能正常工作。我刚刚测试过了。我错过了什么吗?
  • 我想如果你没有溢出和下溢的例外情况,它肯定会起作用。
【解决方案5】:

大多数人会使用临时变量交换两个变量 x 和 y,如下所示:

tmp = x
x = y
y = tmp

这是一个无需临时值即可交换两个值的巧妙编程技巧:

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

更多详情Swap two variables using XOR

在第 1 行,我们结合 x 和 y(使用 XOR)来获得这个“混合”,并将其存储回 x。 XOR 是一种保存信息的好方法,因为您可以通过再次执行 XOR 来删除它。

在第 2 行。我们将混合与 y 异或,这抵消了所有 y 信息,只剩下 x。我们将这个结果保存回 y,所以现在它们已经交换了。

在最后一行,x 仍然具有混合值。我们再次与 y (现在与 x 的原始值)进行异或,以从混合中删除 x 的所有痕迹。这给我们留下了y,交换完成!


计算机实际上有一个隐含的“temp”变量,用于在将中间结果写回寄存器之前存储它们。例如,如果将 3 加到寄存器中(机器语言伪代码):

ADD 3 A // add 3 to register A

ALU(算术逻辑单元)实际上是执行指令 3+A 的部分。它接受输入 (3,A) 并创建一个结果 (3 + A),然后 CPU 将其存储回 A 的原始寄存器。因此,在得到最终答案之前,我们将 ALU 用作临时暂存空间。

我们认为 ALU 的隐式临时数据是理所当然的,但它始终存在。类似地,ALU可以在x = x xor y的情况下返回异或的中间结果,此时CPU将其存储到x的原始寄存器中。

因为我们不习惯考虑可怜的、被忽视的 ALU,XOR 交换似乎很神奇,因为它没有显式的临时变量。有些机器有一个 1 步交换 XCHG 指令来交换两个寄存器。

【讨论】:

  • 我明白,我在问它是如何工作的。在值上使用独占或如何让您在没有临时变量的情况下交换它
  • 赞成,因为这是最清晰和最详细的答案,但要注意的是,与临时变量的交换更具可读性,因此在代码中具有更多价值
  • 我喜欢原来的答案(有解释),但关于 ALU 的部分似乎被误导了。即使在您提到的单周期(非流水线)处理器上,在 1 条指令中执行“x =(涉及 x 的操作)”的能力也与寄存器文件具有输入 和 输出端口。
【解决方案6】:

之所以起作用,是因为 XOR 不会丢失信息。如果你可以忽略溢出,你可以用普通的加法和减法做同样的事情。例如,如果变量对 A,B 最初包含值 1,2,您可以像这样交换它们:

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

顺便说一句,在单个“指针”中编码 2 路链表有一个老技巧。 假设您在地址 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:不客气,你的英语水平比我的法语好多了!
  • 对于+/-方法+1(虽然int溢出是UB)
【解决方案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

【讨论】:

    【解决方案8】:

    XOR 方法基本上有 3 个步骤:

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

    要了解为什么这样做,首先要注意:

    1. 只有当其中一个操作数恰好为 1 且另一个为 0 时,XOR 才会产生 1;
    2. XOR 是可交换的,所以 a 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 的二进制表示将仅在 a 和 b 具有相反位的位位置具有 1 位。即 (ak=1, bk=0) 或 (ak=0, bk=1)。现在,当我们在步骤(2)中进行替换时,我们得到:

    b' = (a XOR b) XOR b
    = a XOR (b XOR b) 因为 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

    这里有更多详细信息: Necessary and Sufficient

    【讨论】:

      【解决方案9】:

      作为旁注,几年前我以交换整数的形式独立地重新发明了这个轮子:

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

      (上面以一种难以阅读的方式提到了这一点),

      同样的推理也适用于异或交换: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。

      【讨论】:

      • 来晚了,但有符号整数溢出在 C 和(旧版本)C++ 中是未定义的行为。在交换变量时可能调用 UB 只是为了“节省一些空间”是一个非常糟糕的主意。
      【解决方案10】:

      我只是想添加一个数学解释,以使答案更完整。在group theory 中,XOR 是一个abelian group,也称为交换群。这意味着它满足五个要求:闭包、关联性、标识元素、逆元素、交换性。

      异或交换公式:

      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)
      

      异或中的逆元是自身,这意味着任何值与自身异或都为零:

      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
      

      异或中的标识元素为零,这意味着任何与零异或的值都保持不变:

      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
      

      您可以在group theory获取更多信息。

      【讨论】:

        【解决方案11】:

        其他人已经发布了解释,但我认为如果它带有一个很好的例子会更好地理解。

        XOR Truth Table

        如果我们考虑上面的真值表并取值A = 1100B = 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
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-01-04
          • 2012-07-04
          • 1970-01-01
          相关资源
          最近更新 更多