Help us understand the problem. What is going on with this article?

ArmにあるというJavaScript専用命令とは何か、あるいは浮動小数点数を整数に変換する方法について

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という名前がついています。

手順としては次の通りです。

  1. NaN, 無限大の場合:0を返す。
  2. そうでない場合:0方向への切り捨てを行って整数にし、それを$2^{32}$で割ったあまりを計算する。
  3. 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 の実行結果を比較してみましょう:

arm-jcvt.c
// 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日)の状況です。

mod_poppo
最近は浮動小数点数オタクをやっています。
https://blog.miz-ar.info/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away