🙌

「三項演算子」の名前に値する演算は条件演算子以外にあるか:アセンブリー言語の観点から

に公開

三項演算子 (ternary operator) とは、多くの場合はC系の言語にある <条件> ? <真の場合の式> : <偽の場合の式> の形の式(条件演算子)を指します。3つの式を入力として取る演算子なので三項演算子、というわけです。

しかし、「三項演算子」という普遍的にも思える名前で「条件演算子」という具体的な演算子を指して良いのでしょうか?例えば単に「二項演算子」と言って「減算(マイナス)」を指したら「二項演算子は他に加算、乗算などがあるだろう」と文句を言われそうです。そういう文句が少ないのは、三項演算子と呼べる対象が少ないからなのでしょうか?

この記事では、「三項演算子」という名前に値する演算がどの程度あるかを、アセンブリー言語の観点から考えます。

オペランドの数

よくあるRISCアーキテクチャーでは、二項演算の命令は「結果を格納するレジスター」「入力を格納するレジスター1」「入力を格納するレジスター2」の3つのオペランドを受け取ります。例えば、r2 <- r0 + r1 という足し算の命令だったら

add r2, r0, r1

という感じです。x86の伝統的な(AVX以前の)スタイルだと結果と入力(の片方)が分かれていなくて二項演算が2オペランドだったりしますが。

アセンブリー言語の観点で4オペランドにするのが自然な演算があれば、それは通常のプログラミング言語的には三項演算子に相当するだろう、ということになります。

x86に4オペランドの命令はいくつあるか

筆者が把握している限りで、4オペランドを取りうるx86の命令をいくつか挙げます。

BLENDV系

SSE4.1以降で使えるBLENDVPS/BLENDVPD/PBLENDVBの各命令は、SSE形式で3オペランド、AVX形式で4オペランドを受け取ります。

これは何をする命令かというと、SIMDベクトルの条件選択です。つまり、ベクトルの各レーンに対して条件演算子 a ? b : c を適用します。

x86の命令は可変長とは言え、命令の形式は割とお決まりのパターンがあります(詳しくは「x86-64機械語入門」や「x86-64機械語入門 AVX/AVX-512編」などを参照)。SSE命令は2オペランド形式が基本で、AVX形式(VEXプリフィックス)では3オペランド形式が基本であることを踏まえると、機械語へのエンコード方法も特筆に値します。SSE版はオペランドの一つがXMM0レジスターに固定され、残りの2つのオペランドをビットフィールドで指定する形のようです。AVX版は、通常の方法で指定できるのが3オペランドまでなので、残りの1つのオペランド(レジスターの番号)は1バイトの定数部分(他の命令だと即値を格納する部分)に格納します。

EVEX版はありません。AVX-512VLが使えても、xmm16以降は指定できないということになります。

FMA(融合積和)

FMA (fused multiply-add) は a * b + c を1個の演算として計算します。丸めの回数が変わるので、「積」と「和」を順番に計算するのとは一般に異なる結果が得られます。

関連記事:FMA (fused multiply-add) の話

FMA命令はx86系には2010年代前半から搭載されるようになりましたが、IntelとAMDで互換性のない形で実装されました。x86陣営と言えど当時は連携が取れていなかったんですかね。

IntelはFMAを3オペランドの命令として実装し、入力のいずれかを出力で上書きする形を取りました。

a <- a * b + c
b <- a * b + c
c <- a * b + c

の3通りですね。浮動小数点数の乗算は可換なのに3パターン用意されているのは、NaNの伝播のことを考えたのだと思われます。律儀です。

IntelのFMA命令(後述のAMDのやつと区別する場合はFMA3と呼ばれる)はその後Zen以降のAMDのCPUにも搭載されています。

一方、AMDは当初は4オペランドのFMAを実装しました。これはIntelのやつと区別してFMA4と呼ばれます。Intel SDMには載っていないので、詳細はAMD64 Architecture Programmer's Manualを確認する必要があります。命令の形式は

VFMADDS{S,D} xmm1, xmm2, xmm3/mem{32,64}, xmm4
VFMADDS{S,D} xmm1, xmm2, xmm3, xmm4/mem{32,64}

みたいな感じですね。機械語のエンコーディングは、AVX版のBLENDVと同様に、即値用の1バイトを使うようです。

FMA4はZen以降のCPUからは削除されたようです。

コンパイラーを作る側からしたら4オペランド版が普及してくれたらちょっと楽だったかもしれませんが、普及することになったのはIntelのFMA3でした。まあベクトルの内積みたいな用途だと入力と出力が共通でも困らないとは思いますが。

AVX-512

AVX-512では、多くの命令にマスクを指定することができます。マスクも追加の入力と考えれば、演算が4オペランドになると考えることができるかもしれません。

それから、埋め込み丸め (embedded rounding) もアセンブリー言語では追加のオペランドのように見えます。

VADDSD xmm0, xmm1, xmm2, {ru-sae}

という具合です。C言語だと組み込み関数を使って _mm_add_round_sd(a, b, _MM_FROUND_TO_POS_INF | _MM_FROUND_NO_EXC) となります。これはレジスターを指定するわけではなく、2ビットの即値でしかありませんが。

Armに4オペランドの命令はいくつあるか

ArmのAArch64(A64命令セット)にある4オペランド命令も紹介します。

条件選択(※3オペランド)

x86では4オペランドになっていたSIMDの条件選択は、A64命令セットでは3オペランドの命令3つ BSL, BIT, BIF になります。IntelのFMA命令が3つの命令に分割されたのと同じ理屈です。

BSL Vd, Vn, Vm // Vd <- Vd ? Vn : Vm
BIT Vd, Vn, Vm // Vd <- Vm ? Vn : Vd
BIF Vd, Vn, Vm // Vd <- Vm ? Vd : Vn

なので、4オペランドではないのですが、比較のために紹介しました。

FMA

A64命令セットのFMA命令は4オペランドです!ただしスカラーの場合のみ。

SIMDベクトルのFMAを計算する FMLA 命令は3オペランドで、加算に使うオペランドと出力が共用されます。

FMADD Dd, Dn, Dm, Da // Dd <- Dn * Dm + Da
FMLA Vd, Vn, Vm      // Vd <- Vn * Vm + Vd

命令長が固定(A64の場合は32ビット)だと4オペランドの命令を安易に増やすとエンコーディング空間を圧迫するとかあるんですかね。

加算の代わりに減算を行うパターンや、整数バージョンもあるようです。整数バージョンは2命令に分割できると思いますが。

EOR3

A64には、積和系以外にも4オペランドの命令があります。それが EOR3 命令です。

EORというのはexclusive-ORのことです。XORと略することが多いと思いますが、Arm流ではEORになります。

EOR3 Vd, Vn, Vm, Va // Vd <- Vn ^ Vm ^ Va

EOR3 はFEAT_SHA3拡張の一部で、ハッシュ計算に役立つのかもしれません。

まあ、これは2回のXORに分割できるので、この記事の主題である「三項演算子」にはそぐわないかもしれません。

まとめ

x86とArmのアセンブリー言語で「4オペランドになる可能性がある命令」を探した結果、

  • 条件演算子
  • FMA
  • SIMDのマスクや丸めモードの指定

などが挙がりました。本質的に三項演算になる演算はやはりそこまで多くはないようです。とはいえ三項演算は条件演算子以外にもちゃんとあるので、やはり「三項演算子」で条件演算子を指すのは避けた方が良いとも言えるでしょう。

Discussion