世界を動かす技術を、日本語で。

レジスタを自分自身とXORすることは、それをゼロにするための慣用句です。なぜ減算ではないのでしょうか?

概要

x86アーキテクチャ でレジスタをゼロクリアする代表的な方法について解説。 xor eax, eax 命令がなぜ主流になったのか、その理由と歴史的経緯を整理。 sub eax, eax との比較や、CPU設計上の最適化事情も紹介。 Intelなどの CPUメーカーの対応 や、他アーキテクチャ(Itanium)との違いも触れる。 実際のアセンブリコードの現場でのエピソードも交えて解説。

x86コンパイラがxor eax, eaxを好む理由

  • xor eax, eax は、x86でレジスタをゼロにする最も コンパクトな方法
  • mov eax, 0 よりも 命令サイズが小さい (movは即値エンコードで4バイト必要)
  • x86には ゼロレジスタが存在しない ため、都度ゼロ化が必要
  • xor 命令のほかにも sub eax, eax などゼロ化できる命令は存在
  • ただし、xorが主流になった理由は明確ではなく、 慣習や偶然 が影響した可能性

xor eax, eaxとsub eax, eaxの比較

  • バイト数実行サイクル数 ともに同じ
  • フラグレジスタ(EFLAGS)の影響
    • xor eax, eax:AF(補助キャリーフラグ)は 未定義
    • sub eax, eax:AFは クリア される
  • その他のフラグ(OF, SF, ZF, PF, CF)はどちらもゼロ化に適切な状態

なぜxorが主流になったのか

  • 初期のコンパイラが xorを選択 したことで、そのまま 慣習化
  • 「コンパイラがそうしているから正しいはず」という 集団心理
  • Intelは両命令に 特別な最適化(命令デコーダでの検出・依存性解消) を実装
    • 入力に依存せず、 ゼロレジスタへのリネーム で実質「0サイクル」動作
  • ただし他のCPUベンダーは xorのみ特別扱い のケースがあるため、xorの優位性が確定

余談・実際の現場エピソード

  • sub r, r を使う開発者も存在し、アセンブリコードで 個性が出る
  • Itaniumアーキテクチャ ではxorトリックは NaTビットがリセットされない ため無効
    • Itaniumには 専用ゼロレジスタ が存在し、直接ゼロを代入可能

著者・背景情報

  • 著者 Raymond はWindows開発に30年以上携わるエンジニア
  • The Old New Thing というWebサイト・書籍の作者
  • Windows Dev Docs Twitterアカウントでも活動
  • 技術的な話題から雑談まで幅広く発信

Hackerたちの意見

レイモンドのコンピュータの些細なことについての文章が、こんなに面白いなんて驚きだよね。

マイクロソフトが今どれだけ叩かれてるかは別として、彼らには低レベルコンピューティングについて書くのが上手い人たちがいるよ。ジェームズ・ミケンズの文章は、これについて読んでて本当に笑っちゃった。チェンが彼を「マイクロソフトリサーチで一番面白い男」と表現したのが一番的確だね。

明らかな答えは、XORの方が速いってこと。減算をするには、最下位ビットから最上位ビットにキャリービットを伝播させなきゃいけないけど、XORだとそれをしなくていいんだ。各ビットの出力が隣接するビットに依存しないからね。たぶん、明示的なペナルティがないALUパイプラインの設計もあるけど、全てではないから、XORの方が速いんだよね。レイモンド・チェンみたいなすごい人はそれを知ってるはず。こんなに明白で基本的なことなのに、何か見落としてるのかな?

TFAが言ってる通り、x86では sub eax, eax は同じバイト数でエンコードされ、同じサイクル数で実行されるよ。

「答えは明らかだ」 ちょっと脱線するけど、「明らか」ってのは自分が知ってることによるんだよね。専門家は自分が「明らか」だと思ってることを説明しないことが多いけど、それは彼らにとってだけ「明らか」なことだから。みんな親切にして、知らない人にも「明らか」なことを説明すべきだよね。

彼の言いたいことは、x86ではパフォーマンスの違いはないけど、同僚や友人以外はみんなXORを使ってるってこと。実際、subはクリーンなフラグを残すから、彼はこれが何かの社会的慣習で、ランダムに選ばれて、支持するための根拠のない議論で広まったんじゃないかと思ってる(あるいは「見た目がクール」っていうアート用語の一部として)。また、アセンブリで働くほとんどの人が論理ゲートの特性を知っているから、内部的には何かしら良いかもしれないという理解を持っているのかもしれない。

実際、XORがSUBより速いCPUは知らないな。もっと重要なのは、8086ではタイミングが同じだってこと。これがこのパターンの出所なんだよね。

そのコメントは、SUBがXORより高コストな実際のCPUを指摘しないとあまり役に立たないよね ;) 例えば、Z80と6502では両方とも同じサイクル数だし。

8086アセンブリを学んでるとき、if x==yを正しくやるにはCMP命令を使うって知ったとき、似たような反応をしたよ。CMPは引き算をしてフラグだけを設定するんだよね。(その本には、さまざまな比較演算子に使うブランチ命令のセクションがあった。)XORを使って、引き算を避けるような二つの値を比較してブランチするマクロを作れるか試してみたのに、数分かかったな。

FPGAやASICで単独でXORをやると速いよ。でも、ALU(算術論理ユニット)で他の多くの操作と一緒にXORをやると、スピードは一番遅い操作によって決まるから、速い操作のスピードは関係なくなるんだ。つまり、ほとんどのCPUではXORと加算、引き算は同じスピードなんだよ。XORは速くできるのにね。現代のパイプラインCPUでは、クロック周波数は通常、64ビットの加算が1クロックサイクルでできるように選ばれるんだ。レジスタや多重化器、ALUステージの外の回路によるオーバーヘッドを含めてね。64ビットの加算/引き算よりも複雑な操作は、1クロックサイクル以上のレイテンシがあるけど、実行パイプラインの1クロックサイクルごとに1つの操作を開始できるんだ。64ビットの加算/引き算よりも単純な操作、例えばXORは1クロックサイクルで実行されるから、スピードのアドバンテージはないんだ。いわゆるスーパーパイプラインCPUもあって、クロック周波数が上がると、加算/引き算でも2サイクル以上のレイテンシが出ることがある。スーパーパイプラインCPUでは、引き算よりも速いXOR命令が可能かもしれないけど、実際に実装されたかはわからない。性能向上が微々たるものだから、実行パイプラインが複雑になるかもしれないしね。最初はDECがスーパースカラプロセッサに対するより良い代替品としてスーパーパイプラインを推進してたけど、後にスーパーパイプラインは放棄されたんだ。スーパースカラ方式の方が同じ性能でより良いエネルギー効率を提供するからね。(つまり、数年間はスピードデーモンがブレイニアックに勝つと思われてたけど、最終的にはブレイニアックがスピードデーモンに勝つことが証明されたんだ。AppleのCPUのようにね。)主流のCPUはスーパーパイプラインを使ってないけど、比較的最近のIBM POWER CPUにはスーパーパイプラインがあったんだ。ただし、元々提案された理由とは違う理由でね。そのPOWER CPUは、SMTを使ったマルチスレッドワークロードでのみ良い性能を発揮するように設計されていて、シングルスレッドアプリケーションではそうじゃなかったんだ。だから、同じALUで同時にスレッドを実行することで、加算/引き算のマルチサイクルレイテンシが隠されてた。この技術により、IBMは5GHz以上で動作するCPUを簡単に実装できたんだ。シングルスレッド性能だけを劣化させて、SMT性能には影響を与えないようにね。SMTを使うときに何の利点もなかっただろうから、そのPOWER CPUではXORが引き算より速く作られることはなかったと思う。理論的には可能だったかもしれないけど。

TFAによると、これらのイディオムがレジスタをゼロにする方法として広く使われているため、Intelは命令デコーディングのフロントエンドに特別なxor r、r検出とsub r、r検出を追加し、宛先を内部ゼロレジスタに名前変更して、命令の実行を完全にバイパスすることにしたんだ。

引き算をするには、最下位ビットから最上位ビットにキャリービットを伝播させる必要がある。そうだけど、それはビット数に対して線形にスケールする必要はないよ。https://en.wikipedia.org/wiki/Carry-lookahead_adder: 「キャリー先読み加算器(CLA)またはファストアダーは、デジタルロジックで使用される電子加算器の一種です。キャリー先読み加算器は、より単純で通常は遅いリップルキャリー加算器(RCA)と対比されます。RCAでは、キャリービットは合計ビットと一緒に計算され、各ステージは前のキャリービットが計算されるまで自分の合計ビットとキャリービットの計算を開始できません。キャリー先読み加算器は、合計の前に1つ以上のキャリービットを計算するため、加算器の大きな値のビットの結果を計算する待機時間が短縮されます。 […] すでに1800年代半ばに、チャールズ・バベッジは彼の差分機関で使用されていたリップルキャリーによる性能ペナルティを認識し、彼の未完成の解析機関のためにキャリーを予測するメカニズムを設計しました。[1][2] コンラート・ツーゼは、1930年代の彼のバイナリ機械コンピュータZuse Z1で最初のキャリー先読み加算器を実装したと考えられています。」今のALUはほとんど、いや全てがこういう加算器を実装してると思うよ。

Hacker Newsで議論の続きを見る