Armv8.3の FJCVTZS
命令を解説します。
浮動小数点数から整数へのキャスト
予備知識として、いくつかのプログラミング言語における浮動小数点数から整数へのキャストの仕様を解説します。
C言語
基本的に0方向への丸め(切り捨て)で計算されます。
(int32_t)(-2.8); // -> -2
(int32_t)3.9999; // -> 3
元の値の絶対値が大きすぎる場合や、無限大、NaNの場合は、
- 6.3.1.4: 表現できない場合はundefined behavior。
- Annex F.4: 表現できない場合はinvalid例外が発生して、値はunspecified。
とされています。
これ以外の浮動小数点数→整数型の変換方法には (l)lrint
や (l)lround
関数などがあります。
Java
基本的に0方向への丸め(切り捨て)で計算されますが、コーナーケースについても言語仕様で定めています。
- NaN:0を返す
- 結果が表現できないもしくは無限大の場合:符号に応じて最大値または最小値が返る。
参照:
JavaScript
JavaScriptではビット演算やいくつかのMath関数で「浮動小数点数から32ビット整数への変換」が行われます。ECMAScriptの規格ではこの操作にはToInt32/ToUint32という名前がついています。
- ToInt32 - ECMAScript® 2020 Language Specification
- ToUint32 - ECMAScript® 2020 Language Specification
手順としては次の通りです。
- NaN, 無限大の場合:0を返す。
- そうでない場合:0方向への切り捨てを行って整数にし、それを$2^{32}$で割ったあまりを計算する。
- ToUint32の場合はそれがそのまま返して、ToInt32の場合はさらに符号の調整が入る。
C言語やJavaと違い、範囲外の値に対してはmodを取ることが特徴的です。擬似コードで書けばこんな感じになるでしょう:
function ToInt32(x: number): int32
{
if (!Number.isFinite(x) || x === 0) {
return 0;
}
const t = Math.trunc(x); // 0方向に丸める
const r = ((t % 2**32) + 2**32) % 2**32; // 2**32で割った余り(非負)
if (r >= 2**31) {
return r - 2**32;
} else {
return r;
}
}
命令セットでの事情
大抵の浮動小数点数演算ではデフォルトで最近接丸めが使われますが、C言語の浮動小数点数から整数型へのキャストでは常に(0方向への)切り捨てが行われます。
C言語でキャストする度に丸めモードを変更していると大変なので、アーキテクチャーによっては「その時の丸めモードにかかわらず切り捨てを行う」命令を持っています。
x86系を例に説明すると、x87 FPUでは FIST
/FISTP
命令がその時点での丸めモードを使用するのに対して、 FISTTP
命令は常に0方向への丸め(切り捨て)を使用します。SSE2では CVTS[SD]2SI
が現在の丸めモードを使用するのに対して、 CVTTS[SD]2SI
は切り捨てを行います。
Power ISAもチラ見した感じでは浮動小数点数から整数の変換の際に「切り捨て」専用の命令があるようです。
というわけで、そういうアーキテクチャーではC言語の浮動小数点数から整数型へのキャストは1命令で実行できます。
(ちなみにAArch64は「切り捨て」のみを特別扱いせず、IEEEに規定された5つの丸め方法のそれぞれに対応する変換命令を持っています。)
JavaScript専用命令
C言語は昔から重要な言語でしたが、最近重要性を増しているのがJavaScriptです。JavaScriptではビット演算を行う度にToInt32/ToUint32が実行されます。(入力が最初から32ビット整数であることが分かっていれば変換の必要はないかと思いますが)
Armの中の人(もしくはArm準拠のチップを開発している誰か)は「JavaScriptのToInt32/ToUint32専用の命令を用意すると良さそう」と考えたのか、Armv8.3に FJCVTZS
という命令を導入しました。命令名の J
はJavaScriptのJです。まさしく「JavaScript専用命令」です。
(この命令の説明的な名前は Floating-point Javascript Convert to Signed fixed-point, rounding toward Zero です。)
この命令の細かい動作は今更説明するまでもないでしょう。ToInt32と同じです。
なお、この命令はArmのC拡張 (ACLE) で定義された組み込み関数 __jcvt
として、C言語からも利用できます。試しに、C言語のキャストと __jcvt
の実行結果を比較してみましょう:
// gcc-10 -march=armv8.3-a arm-jcvt.c という風にコンパイル
#include <stdio.h>
#include <math.h>
#include <inttypes.h>
#include <arm_acle.h>
// Prototype:
// int32_t __jcvt(double);
#if defined(__GNUC__)
__attribute__((noinline))
#endif
int32_t cast_double_to_i32(double x)
{
return (int32_t)x;
}
int main(void)
{
printf("(int32_t)(-2.8) = %" PRId32 "\n", cast_double_to_i32(-2.8));
printf("(int32_t)1.99 = %" PRId32 "\n", cast_double_to_i32(1.99));
printf("(int32_t)(-Infinity) = %" PRId32 "\n", cast_double_to_i32(-INFINITY));
printf("(int32_t)Infinity = %" PRId32 "\n", cast_double_to_i32(INFINITY));
printf("(int32_t)NaN = %" PRId32 "\n", cast_double_to_i32(NAN));
printf("(int32_t)(0x1p50 + 123.0) = %" PRId32 "\n", cast_double_to_i32(0x1p50+123.0));
#if defined(__ARM_FEATURE_JCVT)
printf("__jcvt(-2.8) = %" PRId32 "\n", __jcvt(-2.8)); // -> -2
printf("__jcvt(1.99) = %" PRId32 "\n", __jcvt(1.99)); // -> 1
printf("__jcvt(-Infinity) = %" PRId32 "\n", __jcvt(-INFINITY)); // -> 0
printf("__jcvt(Infinity) = %" PRId32 "\n", __jcvt(INFINITY)); // -> 0
printf("__jcvt(NaN) = %" PRId32 "\n", __jcvt(NAN)); // -> 0
printf("__jcvt(0x1p50 + 123.0) = %" PRId32 "\n", __jcvt(0x1p50+123.0)); // -> 123
#else
puts("__jcvt is not available");
#endif
}
実行例:
(int32_t)(-2.8) = -2
(int32_t)1.99 = 1
(int32_t)(-Infinity) = -2147483648
(int32_t)Infinity = 2147483647
(int32_t)NaN = 0
(int32_t)(0x1p50 + 123.0) = 2147483647
__jcvt(-2.8) = -2
__jcvt(1.99) = 1
__jcvt(-Infinity) = 0
__jcvt(Infinity) = 0
__jcvt(NaN) = 0
__jcvt(0x1p50 + 123.0) = 123
GCC/Clangで試した感じでは、AArch64ではC言語のキャストには FCVTZS 命令が使用されました。こちらは範囲外の値には32ビット整数の最大値・最小値を返します(Javaっぽいですね)。一方で、 __jcvt
関数(FJCVTZS 命令)の方はJavaScriptのセマンティクスに沿っています。
筆者のラズパイ4(Cortex-A72搭載)は FJCVTZS 命令に対応していなかったので、上記のコードはQEMUで動作確認しました。QEMUは偉大です。
今話題のApple M1は少なくともArmv8.3以降に対応しているようなので、この「JavaScript専用命令」の恩恵を受けることができる、というわけです。実際にどのぐらい高速化につながるのかは筆者は知りません。
追記:JavaScriptエンジンによる利用状況
せっかくJavaScript専用命令があっても、実際の処理系で使われないと意味がありません。というわけで、各種JavaScriptエンジンが FJCVTZS 命令を使用するのか(ソースを見て)確認してみました。いずれも執筆時点(2020年11月30日)の状況です。
- V8:
コードのチェックアウトが面倒なのでちゃんと確認してません。悪しからず。GitHubのミラーで検索した感じではヒットしませんでした。- 対応しているようです:[arm64] Add FJCVTZS support (I6047fa16) · Gerrit Code Review
- ソースの該当部分
- JavaScriptCore: 184023 – Emit fjcvtzs on ARM64E on Darwin
- DarwinとLinux上で有効なようです。
- SpiderMonkey (Mozilla): 1556571 - Use FJCVTZS for convertDoubleToInt32
- ソースを見た感じ、Linux(Android含む)では有効なようですが、Darwin上ではどうなんですかね…。