skip to content

Why do reviewers reject the temp-free XOR swap of two array slots?

level: middleimportance: should knowfreq 44%

answer

  1. ask what the two operands refer to
  2. consider both indices being equal
  3. self-inverse fires on the first step
  4. the value is gone before step two
  5. three dependent steps versus three independent moves

basics

~20 s

Because it silently zeroes the value when both operands are the same storage location — a partition routine that swaps an element with itself destroys it. It is also not faster than a swap through a temporary on modern hardware.

solid answer

~50 s

It has an aliasing bug and it is not the optimisation people think it is. The three-step sequence works only when the two slots are distinct storage; if both indices are the same, the first step computes `a[i] xor a[i]`, which is zero, and the remaining two steps only propagate that zero — the element is gone. Partition and selection routines routinely call swap with equal indices when an element is already in place, so this is a live bug rather than a theoretical one. On the performance side, the three XOR steps form a strictly serial dependency chain, each waiting on the previous result, whereas a swap through a temporary is three independent moves that a wide out-of-order core absorbs almost for free — and an optimising compiler will often eliminate the moves entirely by renaming. The temp-free version costs correctness and readability to buy nothing.

code

pseudocode · 9 lines
pseudocode
// intended: exchange a[i] and a[j] with no temporary
a[i] = a[i] xor a[j]
a[j] = a[i] xor a[j]
a[i] = a[i] xor a[j]

// trace with i == j and a[i] == 42:
//   a[i] = 42 xor 42 = 0
//   a[i] = 0  xor 0  = 0
//   a[i] = 0  xor 0  = 0

go deeper

for a junior

Know that exchanging two values without a temporary is possible with three XOR steps, and that it breaks when both sides are the same storage. Be able to trace those three steps on paper.

for a middle

Explain both objections precisely: the aliasing case that zeroes the element, and why three serially dependent operations are not cheaper than three independent moves a compiler may erase entirely.

for a senior

Frame it as a review call. Describe how the corruption surfaces far from its cause, why tests rarely hit the equal-index path, and what you say to an author who insists it is faster.

for a principal

Own the general rule behind the example: clever in-place updates that read and write overlapping storage need an explicit aliasing precondition, and a team standard that rejects unmeasured micro-optimisation is cheaper than debugging one.

## What the sequence does when the slots are distinct Call the two stored values `A` and `B`, held in distinct slots `a[i]` and `a[j]`. 1. `a[i] = a[i] xor a[j]` leaves `A xor B` in the first slot. 2. `a[j] = a[i] xor a[j]` computes `(A xor B) xor B`, which is `A` by self-inverse and identity, and stores it in the second slot. 3. `a[i] = a[i] xor a[j]` computes `(A xor B) xor A`, which is `B`, and stores it in the first slot. The exchange is real, and it uses no third storage location. That is the whole appeal, and it is why the trick keeps reappearing in code review. ## The aliasing failure Everything above assumed the two slots are different pieces of storage. Suppose they are the same — `i == j`, or two references that happen to point at the same element. Then step 1 is `a[i] = a[i] xor a[i]`, which is zero by self-inverse. The value is destroyed immediately, and steps 2 and 3 have nothing left to reconstruct it from: they compute `0 xor 0` twice. The slot ends holding zero. This is not a contrived case. Any partition or selection routine that walks two indices towards each other will, sooner or later, call its swap with both indices equal — that is what happens when the element under consideration is already where it belongs. A swap through a temporary handles that call as a harmless no-op. The temp-free version turns it into silent data loss, and the corruption surfaces far from its cause: a value quietly becomes zero, the surrounding algorithm keeps running, and the wrong output shows up in some downstream aggregate. If the same value happens never to be equal-indexed in your tests, the bug ships. The general lesson generalises past this one trick: any "clever" in-place update that reads and writes overlapping storage must state explicitly whether its operands may alias, and be guarded or documented accordingly. ## Why it is not faster either The usual justification is that the temp-free version saves a register or a store. Both halves of that claim are weak on any modern general-purpose processor: - **The dependency chain.** The three XOR steps are strictly serial: step 2 needs step 1's result, step 3 needs step 2's. A swap through a temporary is three data movements with no such chain, so a wide out-of-order core can issue them in parallel. Latency, not instruction count, is what you are paying here, and the trick makes latency worse. - **The compiler is already ahead of you.** Optimising compilers routinely implement a temporary swap by renaming which register holds which value, emitting no movement at all. You cannot beat zero instructions with three. - **The register is not scarce.** The scarcity argument belongs to hand-written code for very small processors with a handful of registers and no compiler worth the name. In that setting the trick is a legitimate tool — which is exactly why it exists — but it is not the setting most reviewers are working in. There is a further constraint worth naming: the trick applies only to values on which XOR is defined bit-for-bit. Anything with a representation that is not a plain bit pattern, or where distinct bit patterns must compare equal, is not a candidate at all. ## What to say in review The reviewable statement is not "never use XOR swap", it is "this buys nothing here and introduces an aliasing precondition nobody will remember". If the code needs a swap, use a temporary; if profiling ever shows that the swap itself is the bottleneck — which it essentially never is, because the surrounding memory traffic dominates — the fix is to move less data, not to move it more cleverly. ## The one thing the trick genuinely teaches It is a good interview question precisely because it is a small, complete test of whether the candidate reasons about preconditions instead of pattern-matching on cleverness. The candidate who says "it swaps without a temporary" has read it somewhere; the candidate who asks "what if both indices are the same?" has thought about it.

  • Where in a real algorithm does a swap get called with both indices equal?
    Partition and selection routines that advance two indices towards each other reach that call whenever the element under consideration already sits in its final slot. A swap through a temporary makes that a harmless no-op, so most implementations never bother to guard against it — which is exactly why substituting the temp-free version is dangerous.
  • Is there any setting where the temp-free swap is the right call?
    Hand-written code for very constrained processors where registers are genuinely scarce and no optimising compiler is involved. Even there it needs an explicit guard or a documented precondition that the operands never alias. In ordinary application code the register pressure argument simply does not hold.
  • Would adding a guard for equal indices make it acceptable?
    It would make it correct, but the guard is a branch that costs more than the temporary it was meant to save, and the code is now three XOR steps plus a comparison instead of one obvious exchange. Correct and pointless is still worth rejecting in review.

saying these in an interview costs you the question

  • Calls it a free micro-optimization with no downside
  • Cannot say what happens when both indices are equal
  • Claims fewer instructions always means faster
  • Assumes the compiler cannot eliminate a temporary swap
  • Thinks the bug is caught by ordinary unit tests
  • Believes the trick works on any kind of value

context