Airing

Airing

哲学系学生 / 小学教师 / 程序员,个人网站: ursb.me
github
email
zhihu
medium
tg_channel
twitter_id

エンジン解析:JS における文字列の数値への変換

JS の中で、文字列を数値に変換する方法は以下の 9 種類です:

  1. parseInt()
  2. parseFloat()
  3. Number()
  4. ダブルチルダ(~~)演算子
  5. 単項演算子(+)
  6. Math.floor()
  7. 数値との乗算
  8. 符号付き右シフト演算子(>>)
  9. 符号なし右シフト演算子(>>>)

これらの方法による実行結果の違いは、以下の表に示されています:

文字列を数値に変換する方法の比較

比較表のソースコードは https://airing.ursb.me/web/int.html に公開されていますので、必要に応じてご利用ください。

実行結果の違いに加えて、これらの方法は性能にも違いがあります。NodeJS V8 環境下でのこれらの方法の微ベンチマークテストの結果は以下の通りです:

parseInt() x 19,140,190 ops/sec ±0.45% (92 runs sampled)
parseFloat() x 28,203,053 ops/sec ±0.25% (95 runs sampled)
Number() x 1,041,209,524 ops/sec ±0.20% (90 runs sampled)
ダブルチルダ(~~)演算子 x 1,035,220,963 ops/sec ±1.65% (97 runs sampled)
Math.floor() x 28,224,678 ops/sec ±0.23% (96 runs sampled)
単項演算子(+) x 1,045,129,381 ops/sec ±0.17% (95 runs sampled)
数値との乗算 x 1,044,176,084 ops/sec ±0.15% (93 runs sampled)
符号付き右シフト演算子(>>) x 1,046,016,782 ops/sec ±0.11% (96 runs sampled)
符号なし右シフト演算子(>>>) x 1,045,384,959 ops/sec ±0.08% (96 runs sampled)

ご覧の通り、parseInt()parseFloat()Math.floor()の効率は最も低く、他の演算の約 2% 程度の効率しかありません。その中でもparseInt()が最も遅く、わずか 1% です。

なぜこれらの方法にこのような違いがあるのでしょうか?これらの演算はエンジン層でどのように解釈され実行されるのでしょうか?次に、V8、JavaScriptCore、QuickJS などの主要な JS エンジンの視点から、これらの方法の具体的な実装を探求します。

まずはparseInt()を見てみましょう。

1. parseInt()#

ECMAScript (ECMA-262) parseInt
image

1.1 V8 における parseInt ()#

V8 の [→ src/init/bootstrapper.cc] では、JS 言語の組み込み標準オブジェクトが定義されており、parseIntの定義を見つけることができます:

Handle<JSFunction> number_fun = InstallFunction(isolate_, global, "Number", JS_PRIMITIVE_WRAPPER_TYPE, JSPrimitiveWrapper::kHeaderSize, 0, isolate_->initial_object_prototype(), Builtin::kNumberConstructor);

// Number.parseIntとGlobal.parseIntをインストール
Handle<JSFunction> parse_int_fun = SimpleInstallFunction(isolate_, number_fun, "parseInt", Builtin::kNumberParseInt, 2, true);

JSObject::AddProperty(isolate_, global_object, "parseInt", parse_int_fun,
 native_context()->set_global_parse_int_fun(*parse_int_fun);

ここで、Number.parseInt とグローバルオブジェクトの parseInt はどちらもSimpleInstallFunctionに基づいて登録されており、API を isolate にインストールし、このメソッドを Builtin にバインドします。JS 側からparseIntを呼び出すと、エンジン側ではBuiltin::kNumberParseIntが呼び出されます。

Builtin(組み込み関数)は、V8 内で VM の実行時に実行可能なコードブロックであり、VM に対する実行時の変更を表現するために使用されます。現在の V8 バージョンでは、Builtin には以下の 5 つの実装方法があります:

  • プラットフォーム依存のアセンブリ言語:非常に効率的ですが、すべてのプラットフォームに手動で適合させる必要があり、メンテナンスが難しい。
  • C++:スタイルは runtime functions と非常に似ており、V8 の強力なランタイム機能にアクセスできますが、通常は性能に敏感な領域には適していません。
  • JavaScript:遅いランタイム呼び出し、型汚染による予測不可能な性能影響、および複雑な JS セマンティクスの問題。現在、V8 は JavaScript の組み込み関数を使用していません。
  • CodeStubAssembler:効率的な低レベル機能を提供し、アセンブリ言語に非常に近いですが、プラットフォーム依存性と可読性を維持します。
  • Torque:CodeStubAssembler の改良版であり、その構文は TypeScript のいくつかの特徴を組み合わせており、非常にシンプルで読みやすいです。性能を損なうことなく使用の難易度をできるだけ下げることを強調し、Builtin の開発をより容易にします。現在、多くの組み込み関数は Torque で実装されています。

前述のBuiltin::kNumberParseInt関数は、[→ src/builtins/builtins.h] でその定義を見ることができます:

// すべてのbuiltinのために名前付きアクセサを生成しないための便利なマクロ
#define BUILTIN_CODE(isolate, name) \
  (isolate)->builtins()->code_handle(i::Builtin::k##name)

したがって、この関数の登録された元の名前はNumberParseIntであり、[→ src/builtins/number.tq] で実装されています。これは Torque に基づく Builtin 実装です。

// ES6 #sec-number.parseint
transitioning javascript builtin NumberParseInt(
    js-implicit context: NativeContext)(value: JSAny, radix: JSAny): Number {
  return ParseInt(value, radix);
}


transitioning builtin ParseInt(implicit context: Context)(
    input: JSAny, radix: JSAny): Number {
  try {
    // radixが10(つまり、undefined、0または10)であるべきかどうかを確認します。
    if (radix != Undefined && !TaggedEqual(radix, SmiConstant(10)) &&
        !TaggedEqual(radix, SmiConstant(0))) {
      goto CallRuntime;
    }

    typeswitch (input) {
      case (s: Smi): {
        return s;
      }
      case (h: HeapNumber): {
        // 入力値がSigned32範囲内であるかどうかを確認します。
        const asFloat64: float64 = Convert<float64>(h);
        const asInt32: int32 = Signed(TruncateFloat64ToWord32(asFloat64));
        // NaNの場合の比較の感覚が重要です。
        if (asFloat64 == ChangeInt32ToFloat64(asInt32)) goto Int32(asInt32);

        // 入力の絶対値が[1,1<<31[範囲内にあるかどうかを確認します。結果が-0になる可能性があるため、[0,1[範囲のためにランタイムを呼び出します。
        const kMaxAbsValue: float64 = 2147483648.0;
        const absInput: float64 = math::Float64Abs(asFloat64);
        if (absInput < kMaxAbsValue && absInput >= 1.0) goto Int32(asInt32);
        goto CallRuntime;
      }
      case (s: String): {
        goto String(s);
      }
      case (HeapObject): {
        goto CallRuntime;
      }
    }
  } label Int32(i: int32) {
    return ChangeInt32ToTagged(i);
  } label String(s: String) {
    // 文字列がキャッシュされた配列インデックスであるかどうかを確認します。
    const hash: NameHash = s.raw_hash_field;
    if (IsIntegerIndex(hash) &&
        hash.array_index_length < kMaxCachedArrayIndexLength) {
      const arrayIndex: uint32 = hash.array_index_value;
      return SmiFromUint32(arrayIndex);
    }
    // ランタイムにフォールバックします。
    goto CallRuntime;
  } label CallRuntime {
    tail runtime::StringParseInt(input, radix);
  }
}

このコードを見ていく前に、V8 のいくつかのデータ構造について説明します(V8 のすべてのデータ構造の定義は [→ src/objects/objects.h] で見ることができます):

  • Smi:Object から継承される、即時の小さな整数、31 ビットのみ
  • HeapObject:Object から継承される、ヒープに割り当てられたすべてのもののスーパークラス
  • PrimitiveHeapObject:HeapObject から継承される
  • HeapNumber:PrimitiveHeapObject から継承される、数値のヒープオブジェクトで、大きな整数のオブジェクトを保存するために使用されます。

parseIntは 2 つの引数を受け取ることが知られています、すなわちparseInt(string, radix)です。実装の流れは以下の通りです:

  • まず、radixが渡されていないか、0 または 10 が渡されたかを確認します。そうでない場合、10 進数の変換ではないため、ランタイムで提供されるStringParseInt関数runtime::StringParseIntに進みます。
  • 10 進数の変換であれば、最初の引数のデータ型を確認します。
    • Smi またはオーバーフローしていない(31 ビットを超えない)HeapNumber であれば、引数をそのまま返します。つまり、変換は行われません。それ以外の場合も同様にruntime::StringParseIntに進みます。ここでオーバーフローが発生した場合は、ChangeInt32ToTaggedに進みます。これは CodeStubAssembler によって実装された関数で、Int32 を強制的に変換します。現在の実行環境が 32 ビットのオーバーフローを許可しない場合、変換後の数値は期待通りではなくなります。
    • 文字列の場合は、ハッシュがキャッシュされた配列インデックスであるかどうかを確認します。そうであれば、対応する整数値を見つけて返します。それ以外の場合もruntime::StringParseIntに進みます。

次に焦点が当たるのはruntime::StringParseIntです。[→ src/runtime/runtime-numbers.cc]

// ES6 18.2.5 parseInt(string, radix) スローパス
RUNTIME_FUNCTION(Runtime_StringParseInt) {
  HandleScope handle_scope(isolate);
  DCHECK_EQ(2, args.length());
  Handle<Object> string = args.at(0);
  Handle<Object> radix = args.at(1);

  // まず{string}を文字列に変換し、フラット化します。
  Handle<String> subject;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, subject,
                                     Object::ToString(isolate, string));
  subject = String::Flatten(isolate, subject);

  // {radix}をInt32に変換します。
  if (!radix->IsNumber()) {
    ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, radix,
                                       Object::ToNumber(isolate, radix));
  }
  int radix32 = DoubleToInt32(radix->Number());
  if (radix32 != 0 && (radix32 < 2 || radix32 > 36)) {
    return ReadOnlyRoots(isolate).nan_value();
  }

  double result = StringToInt(isolate, subject, radix32);
  return *isolate->factory()->NewNumber(result);
}

このロジックは比較的シンプルなので、詳細に解説する必要はありません。標準に従い、radixが 2〜36 の範囲外であれば、NaN を返すことに注意してください。

1.2 JavaScriptCore における parseInt ()#

次に、JavaScriptCore におけるparseInt()を見てみましょう。

JavaScriptCore では、JS 言語の組み込みオブジェクトの登録は [→ runtime/JSGlobalObjectFuntions.cpp] ファイル内で行われています:

JSC_DEFINE_HOST_FUNCTION(globalFuncParseInt, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
    JSValue value = callFrame->argument(0);
    JSValue radixValue = callFrame->argument(1);

    // 数値の最適化された処理:
    // 引数が0または範囲内の数値(10^-6 <= n < INT_MAX+1)の場合、parseIntは整数への切り捨てを行います。-0の場合は0に変換されます。
    //
    // INT_MAX+1 <= n < 10^21の範囲内の値についても切り捨てが行われますが、これらの値は10^21がint64_tの範囲を超えるため、単純に切り捨てることはできません。負の数は少し複雑で、-10^21 < n <= -1の範囲の値は整数と同様ですが、-1 < n <= -10^-6の範囲の値は-0に切り捨てる必要があります。
    static const double tenToTheMinus6 = 0.000001;
    static const double intMaxPlusOne = 2147483648.0;
    if (value.isNumber()) {
        double n = value.asNumber();
        if (((n < intMaxPlusOne && n >= tenToTheMinus6) || !n) && radixValue.isUndefinedOrNull())
            return JSValue::encode(jsNumber(static_cast<int32_t>(n)));
    }

    // ToStringがスローされた場合、ToInt32を呼び出すべきではありません。
    return toStringView(globalObject, value, [&] (StringView view) {
        return JSValue::encode(jsNumber(parseInt(view, radixValue.toInt32(globalObject))));
    });
}

WebKit のコードのコメントは非常に詳細で読みやすく、ここでも詳細に解説する必要はありません。最終的には、parseIntは JavaScriptCore のparseIntの実装全体が [→ runtime/ParseInt.h] にあり、核心的なコードは以下の通りです:

ALWAYS_INLINE static bool isStrWhiteSpace(UChar c)
{
    // https://tc39.github.io/ecma262/#sec-tonumber-applied-to-the-string-type
    return Lexer<UChar>::isWhiteSpace(c) || Lexer<UChar>::isLineTerminator(c);
}

// ES5.1 15.1.2.2
template <typename CharType>
ALWAYS_INLINE
static double parseInt(StringView s, const CharType* data, int radix)
{
    // 1. Let inputString be ToString(string).
    // 2. Let S be a newly created substring of inputString consisting of the first character that is not a
    //    StrWhiteSpaceChar and all characters following that character. (In other words, remove leading white
    //    space.) If inputString does not contain any such characters, let S be the empty string.
    int length = s.length();
    int p = 0;
    while (p < length && isStrWhiteSpace(data[p]))
        ++p;

    // 3. Let sign be 1.
    // 4. If S is not empty and the first character of S is a minus sign -, let sign be -1.
    // 5. If S is not empty and the first character of S is a plus sign + or a minus sign -, then remove the first character from S.
    double sign = 1;
    if (p < length) {
        if (data[p] == '+')
            ++p;
        else if (data[p] == '-') {
            sign = -1;
            ++p;
        }
    }

    // 6. Let R = ToInt32(radix).
    // 7. Let stripPrefix be true.
    // 8. If R != 0,then
    //   b. If R != 16, let stripPrefix be false.
    // 9. Else, R == 0
    //   a. LetR = 10.
    // 10. If stripPrefix is true, then
    //   a. If the length of S is at least 2 and the first two characters of S are either ―0x or ―0X,
    //      then remove the first two characters from S and let R = 16.
    // 11. If S contains any character that is not a radix-R digit, then let Z be the substring of S
    //     consisting of all characters before the first such character; otherwise, let Z be S.
    if ((radix == 0 || radix == 16) && length - p >= 2 && data[p] == '0' && (data[p + 1] == 'x' || data[p + 1] == 'X')) {
        radix = 16;
        p += 2;
    } else if (radix == 0)
        radix = 10;

    // 8.a If R < 2 or R > 36, then return NaN.
    if (radix < 2 || radix > 36)
        return PNaN;

    // 13. Let mathInt be the mathematical integer value that is represented by Z in radix-R notation, using the letters
    //     A-Z and a-z for digits with values 10 through 35. (However, if R is 10 and Z contains more than 20 significant
    //     digits, every significant digit after the 20th may be replaced by a 0 digit, at the option of the implementation;
    //     and if R is not 2, 4, 8, 10, 16, or 32, then mathInt may be an implementation-dependent approximation to the
    //     mathematical integer value that is represented by Z in radix-R notation.)
    // 14. Let number be the Number value for mathInt.
    int firstDigitPosition = p;
    bool sawDigit = false;
    double number = 0;
    while (p < length) {
        int digit = parseDigit(data[p], radix);
        if (digit == -1)
            break;
        sawDigit = true;
        number *= radix;
        number += digit;
        ++p;
    }

    // 12. If Z is empty, return NaN.
    if (!sawDigit)
        return PNaN;

    // 特定の大きな数値のための代替コードパス。
    if (number >= mantissaOverflowLowerBound) {
        if (radix == 10) {
            size_t parsedLength;
            number = parseDouble(s.substring(firstDigitPosition, p - firstDigitPosition), parsedLength);
        } else if (radix == 2 || radix == 4 || radix == 8 || radix == 16 || radix == 32)
            number = parseIntOverflow(s.substring(firstDigitPosition, p - firstDigitPosition), radix);
    }

    // 15. Return sign x number.
    return sign * number;
}

ALWAYS_INLINE static double parseInt(StringView s, int radix)
{
    if (s.is8Bit())
        return parseInt(s, s.characters8(), radix);
    return parseInt(s, s.characters16(), radix);
}

template<typename CallbackWhenNoException>
static ALWAYS_INLINE typename std::invoke_result<CallbackWhenNoException, StringView>::type toStringView(JSGlobalObject* globalObject, JSValue value, CallbackWhenNoException callback)
{
    VM& vm = getVM(globalObject);
    auto scope = DECLARE_THROW_SCOPE(vm);
    JSString* string = value.toStringOrNull(globalObject);
    EXCEPTION_ASSERT(!!scope.exception() == !string);
    if (UNLIKELY(!string))
        return { };
    auto viewWithString = string->viewWithUnderlyingString(globalObject);
    RETURN_IF_EXCEPTION(scope, { });
    RELEASE_AND_RETURN(scope, callback(viewWithString.view));
}

// 整数0..35をこの値を識別する数字にマッピングします。基数2..36用。
const char radixDigits[] = "0123456789abcdefghijklmnopqrstuvwxyz";

コードをそのまま貼り付けました。JavaScriptCore の API はすべてECMAScript (ECMA-262) parseInt標準に従って段階的に実装されており、可読性とコメントも非常に良いので、読者は自分で読んでみることを強くお勧めします。ここでは詳細に解説しません。

1.3 QuickJS における parseInt ()#

QuickJS の核心コードは [→ quickjs.c] にあり、まずはparseIntの登録コードを見てみましょう:

/* グローバルオブジェクト */
static const JSCFunctionListEntry js_global_funcs[] = {
    JS_CFUNC_DEF("parseInt", 2, js_parseInt ),
	//...
}

js_parseIntの実装ロジックは以下の通りです:

static JSValue js_parseInt(JSContext *ctx, JSValueConst this_val,
                           int argc, JSValueConst *argv)
{
    const char *str, *p;
    int radix, flags;
    JSValue ret;

    str = JS_ToCString(ctx, argv[0]);
    if (!str)
        return JS_EXCEPTION;
    if (JS_ToInt32(ctx, &radix, argv[1])) {
        JS_FreeCString(ctx, str);
        return JS_EXCEPTION;
    }
    if (radix != 0 && (radix < 2 || radix > 36)) {
        ret = JS_NAN;
    } else {
        p = str;
        p += skip_spaces(p);
        flags = ATOD_INT_ONLY | ATOD_ACCEPT_PREFIX_AFTER_SIGN;
        ret = js_atof(ctx, p, NULL, radix, flags);
    }
    JS_FreeCString(ctx, str);
    return ret;
}

Bellard 大神のコードにはコメントが少ないですが、非常に簡潔です。

これで、V8、JavaScriptCore、QuickJS の各エンジンにおけるparseIntの実装を紹介しました。3 つとも標準に基づいた実装ですが、コードスタイルが異なるため、異なる文体の散文を読むような感覚になります。

ただし、標準と実装を考慮すると、parseIntは文字列を数値に変換する操作を実行する際に、引数の妥当性チェック、引数のデフォルト値、文字列の形式チェックと正規化、オーバーフローのチェックなど、多くの前処理を行っています。したがって、その効率がやや低い理由を推測することができます。

次に、parseFloatを簡単に見てみましょう。

2. parseFloat()#

ECMAScript (ECMA-262) parseFloat
image

標準によれば、parseFloat は parseInt と明らかに異なる点が 2 つあります:

  1. 引数は 1 つのみで、進数変換をサポートしていません。
  2. 戻り値は浮動小数点型をサポートしています。

2.1 V8 における parseFloat ()#

V8 におけるparseFloatの関連ロジックはparseIntのすぐ隣にあり、ここでは重要な実装を直接貼り出します:

[→ src/builtins/number.tq]

// ES6 #sec-number.parsefloat
transitioning javascript builtin NumberParseFloat(
    js-implicit context: NativeContext)(value: JSAny): Number {
  try {
    typeswitch (value) {
      case (s: Smi): {
        return s;
      }
      case (h: HeapNumber): {
        // 入力はすでに数値です。-0に注意してください。
        // NaNの場合の比較の感覚が重要です。
        return (Convert<float64>(h) == 0) ? SmiConstant(0) : h;
      }
      case (s: String): {
        goto String(s);
      }
      case (HeapObject): {
        goto String(string::ToString(context, value));
      }
    }
  } label String(s: String) {
    // 文字列がキャッシュされた配列インデックスであるかどうかを確認します。
    const hash: NameHash = s.raw_hash_field;
    if (IsIntegerIndex(hash) &&
        hash.array_index_length < kMaxCachedArrayIndexLength) {
      const arrayIndex: uint32 = hash.array_index_value;
      return SmiFromUint32(arrayIndex);
    }
    // ランタイムにフォールバックします。
    return runtime::StringParseFloat(s);
  }
}

[→ src/runtime/runtime-numbers.cc]

// ES6 18.2.4 parseFloat(string)
RUNTIME_FUNCTION(Runtime_StringParseFloat) {
  HandleScope shs(isolate);
  DCHECK_EQ(1, args.length());
  Handle<String> subject = args.at<String>(0);

  double value = StringToDouble(isolate, subject, ALLOW_TRAILING_JUNK,
                                std::numeric_limits<double>::quiet_NaN());

  return *isolate->factory()->NewNumber(value);
}

標準のプロセスがより簡易であるため、parseIntに比べてparseFloatはよりシンプルで読みやすいです。

2.2 JavaScriptCore における parseFloat ()#

JavaScriptCore におけるparseFloatのロジックはさらに簡潔明瞭です:

static double parseFloat(StringView s)
{
    unsigned size = s.length();

    if (size == 1) {
        UChar c = s[0];
        if (isASCIIDigit(c))
            return c - '0';
        return PNaN;
    }

    if (s.is8Bit()) {
        const LChar* data = s.characters8();
        const LChar* end = data + size;

        // 先頭の空白をスキップします。
        for (; data < end; ++data) {
            if (!isStrWhiteSpace(*data))
                break;
        }

        // 空の文字列。
        if (data == end)
            return PNaN;

        return jsStrDecimalLiteral(data, end);
    }

    const UChar* data = s.characters16();
    const UChar* end = data + size;

    // 先頭の空白をスキップします。
    for (; data < end; ++data) {
        if (!isStrWhiteSpace(*data))
            break;
    }

    // 空の文字列。
    if (data == end)
        return PNaN;

    return jsStrDecimalLiteral(data, end);
}

2.3 QuickJS における parseFloat ()#

QuickJS では、JavaScriptCore と比較して非常に短いコードです:

[→ quickjs.c]

static JSValue js_parseFloat(JSContext *ctx, JSValueConst this_val,
                             int argc, JSValueConst *argv)
{
    const char *str, *p;
    JSValue ret;

    str = JS_ToCString(ctx, argv[0]);
    if (!str)
        return JS_EXCEPTION;
    p = str;
    p += skip_spaces(p);
    ret = js_atof(ctx, p, NULL, 10, 0);
    JS_FreeCString(ctx, str);
    return ret;
}

ただし、QuickJS が短い理由は ASCII と 8 ビットの互換性を持たないためです。ECMAScript (ECMA-262) parseFloatを読むと、QuickJS の処理には問題がないことがわかります。最新の標準では、インタプリタがそのような互換性を持つ必要はありません。

3. Number()#

ECMAScript (ECMA-262) Number ( value )

image

3.1 V8 における Number ()#

Number はグローバルオブジェクトとして、[→ src/init/bootstrapper.cc] で定義されており、前述のNumber.parseIntの登録時にすでに紹介しました。以下のように振り返ります:

Handle<JSFunction> number_fun = InstallFunction(
        isolate_, global, "Number", JS_PRIMITIVE_WRAPPER_TYPE,
        JSPrimitiveWrapper::kHeaderSize, 0,
        isolate_->initial_object_prototype(), Builtin::kNumberConstructor);
number_fun->shared().DontAdaptArguments();
number_fun->shared().set_length(1);
InstallWithIntrinsicDefaultProto(isolate_, number_fun,
                                     Context::NUMBER_FUNCTION_INDEX);

// %NumberPrototype%を作成
Handle<JSPrimitiveWrapper> prototype = Handle<JSPrimitiveWrapper>::cast(
        factory->NewJSObject(number_fun, AllocationType::kOld));
prototype->set_value(Smi::zero());
JSFunction::SetPrototype(number_fun, prototype);

// {prototype}に「constructor」プロパティをインストールします。
JSObject::AddProperty(isolate_, prototype, factory->constructor_string(),
                          number_fun, DONT_ENUM);

このコードはNumberオブジェクトを登録するだけでなく、そのプロトタイプチェーンを初期化し、コンストラクタをそのプロトタイプチェーンに追加します。コンストラクタBuiltin::kNumberConstructorは Torque で実装されており、[→ src/builtins/constructor.tq] に具体的な実装があります:

// ES #sec-number-constructor
transitioning javascript builtin
NumberConstructor(
    js-implicit context: NativeContext, receiver: JSAny, newTarget: JSAny,
    target: JSFunction)(...arguments): JSAny {
  // 1. 引数がこの関数呼び出しに渡されなかった場合、nを+0とします。
  let n: Number = 0;
  if (arguments.length > 0) {
    // 2. そうでなければ、
    //    a. primを? ToNumeric(value)とします。
    //    b. Type(prim)がBigIntであれば、nをprimのNumber値とします。
    //    c. そうでなければ、nをprimとします。
    const value = arguments[0];
    n = ToNumber(value, BigIntHandling::kConvertToNumber);
  }

  // 3. NewTargetがundefinedであれば、nを返します。
  if (newTarget == Undefined) return n;

  // 4. Oを? OrdinaryCreateFromConstructor(NewTarget,
  //    "%NumberPrototype%", « [[NumberData]] »)とします。
  // 5. O.[[NumberData]]をnに設定します。
  // 6. Oを返します。

  // 通常のターゲットパラメータを無視し、ここで現在のフレームから値をロードして、レジスタの圧力を軽減します。
  const target: JSFunction = LoadTargetFromFrame();
  const result = UnsafeCast<JSPrimitiveWrapper>(
      FastNewObject(context, target, UnsafeCast<JSReceiver>(newTarget)));
  result.value = n;
  return result;
}

コメントの 1-6 は [ECMAScript (ECMA-262) Number ( value )] 標準のプロセス 1-6 に対応しているため、ここでの実装を詳述する必要はありません。標準では、Number が BigInt をサポートすることが明示されており、各エンジンの実装もこの点に注意を払っています。これにより、前述の運算対照表の結果が証明されます。

3.2 JavaScriptCore における Number ()#

JavaScriptCore におけるこのコードはコメントが不足していますが、V8 と同じロジックで標準に従っています:

[→ runtime/NumberConstructor.cpp]

// ECMA 15.7.1
JSC_DEFINE_HOST_FUNCTION(constructNumberConstructor, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);
    double n = 0;
    if (callFrame->argumentCount()) {
        JSValue numeric = callFrame->uncheckedArgument(0).toNumeric(globalObject);
        RETURN_IF_EXCEPTION(scope, { });
        if (numeric.isNumber())
            n = numeric.asNumber();
        else {
            ASSERT(numeric.isBigInt());
            numeric = JSBigInt::toNumber(numeric);
            ASSERT(numeric.isNumber());
            n = numeric.asNumber();
        }
    }

    JSObject* newTarget = asObject(callFrame->newTarget());
    Structure* structure = JSC_GET_DERIVED_STRUCTURE(vm, numberObjectStructure, newTarget, callFrame->jsCallee());
    RETURN_IF_EXCEPTION(scope, { });

    NumberObject* object = NumberObject::create(vm, structure);
    object->setInternalValue(vm, jsNumber(n));
    return JSValue::encode(object);
}

3.3 QuickJS における Number ()#

Number オブジェクトとそのプロトタイプチェーンの登録コードは以下の通りです:

[→ quickjs.c]

void JS_AddIntrinsicBaseObjects(JSContext *ctx)
{
	//...

	/* Number */
    ctx->class_proto[JS_CLASS_NUMBER] = JS_NewObjectProtoClass(ctx, ctx->class_proto[JS_CLASS_OBJECT], JS_CLASS_NUMBER);
    
    JS_SetObjectData(ctx, ctx->class_proto[JS_CLASS_NUMBER], JS_NewInt32(ctx, 0));
    JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_NUMBER], js_number_proto_funcs, countof(js_number_proto_funcs));
    
    number_obj = JS_NewGlobalCConstructor(ctx, "Number", js_number_constructor, 1, ctx->class_proto[JS_CLASS_NUMBER]);
    
    JS_SetPropertyFunctionList(ctx, number_obj, js_number_funcs, countof(js_number_funcs));
}

同様に、プロトタイプチェーンを登録する際にコンストラクタjs_number_constructorをバインドしています:

static JSValue js_number_constructor(JSContext *ctx, JSValueConst new_target,
                                     int argc, JSValueConst *argv)
{
    JSValue val, obj;
    if (argc == 0) {
        val = JS_NewInt32(ctx, 0);
    } else {
        val = JS_ToNumeric(ctx, argv[0]);
        if (JS_IsException(val))
            return val;
        switch(JS_VALUE_GET_TAG(val)) {
#ifdef CONFIG_BIGNUM
        case JS_TAG_BIG_INT:
        case JS_TAG_BIG_FLOAT:
            {
                JSBigFloat *p = JS_VALUE_GET_PTR(val);
                double d;
                bf_get_float64(&p->num, &d, BF_RNDN);
                JS_FreeValue(ctx, val);
                val = __JS_NewFloat64(ctx, d);
            }
            break;
        case JS_TAG_BIG_DECIMAL:
            val = JS_ToStringFree(ctx, val);
            if (JS_IsException(val))
                return val;
            val = JS_ToNumberFree(ctx, val);
            if (JS_IsException(val))
                return val;
            break;
#endif
        default:
            break;
        }
    }
    if (!JS_IsUndefined(new_target)) {
        obj = js_create_from_ctor(ctx, new_target, JS_CLASS_NUMBER);
        if (!JS_IsException(obj))
            JS_SetObjectData(ctx, obj, val);
        return obj;
    } else {
        return val;
    }
}

QuickJS はコンパクトさを追求しているため、BigInt のサポートを自分で設定できるようになっていますが、他のロジックは依然として標準に従っています。

4. ダブルチルダ(~~)演算子#

ECMAScript (ECMA-262) ビット単位の NOT 演算子

image

~演算子を使用すると、標準の第 2 ステップを利用して、計算される値の型変換を行い、文字列を数値に変換します。ここでは、このプロセスがエンジン内のどのステップで完了するのかに注目します。

4.1 V8 におけるビット単位の NOT#

まず、V8 における単項演算子の判断を見てみましょう:

[→ src/parsing/token.h]

static bool IsUnaryOp(Value op) { return base::IsInRange(op, ADD, VOID); }

ADD と VOID の範囲内で定義された op はすべて単項演算子であり、具体的には([→ src/parsing/token.h] を参照)以下のようになります。SUB と ADD は二項演算子リストの末尾に定義されており、IsUnaryOp の中でも単項演算子の判断にヒットします:

E(T, ADD, "+", 12)
E(T, SUB, "-", 12)
T(NOT, "!", 0)
T(BIT_NOT, "~", 0)
K(DELETE, "delete", 0)
K(TYPEOF, "typeof", 0)
K(VOID, "void", 0)

その後、構文解析段階に進み、AST ツリーを解析する過程で、単項演算子に遭遇すると、相応の処理が行われ、まずParseUnaryOrPrefixExpressionを呼び出し、その後単項演算子式BuildUnaryExpressionを構築します:

[→ src/parsing/parser-base.h]

template <typename Impl>
typename ParserBase<Impl>::ExpressionT
ParserBase<Impl>::ParseUnaryExpression() {
  // UnaryExpression ::
  //   PostfixExpression
  //   'delete' UnaryExpression
  //   'void' UnaryExpression
  //   'typeof' UnaryExpression
  //   '++' UnaryExpression
  //   '--' UnaryExpression
  //   '+' UnaryExpression
  //   '-' UnaryExpression
  //   '~' UnaryExpression
  //   '!' UnaryExpression
  //   [+Await] AwaitExpression[?Yield]

  Token::Value op = peek();
  // 単項演算子の処理
  if (Token::IsUnaryOrCountOp(op)) return ParseUnaryOrPrefixExpression();
  if (is_await_allowed() && op == Token::AWAIT) {
	// awaitの処理
    return ParseAwaitExpression();
  }
  return ParsePostfixExpression();
}
template <typename Impl>
typename ParserBase<Impl>::ExpressionT
ParserBase<Impl>::ParseUnaryOrPrefixExpression() {
	//...

	//...
 	// Allow the parser's implementation to rewrite the expression.
   	return impl()->BuildUnaryExpression(expression, op, pos);
}

[→ src/parsing/parser.cc]

Expression* Parser::BuildUnaryExpression(Expression* expression,
                                         Token::Value op, int pos) {
  DCHECK_NOT_NULL(expression);
  const Literal* literal = expression->AsLiteral();
  if (literal != nullptr) {
	// !
    if (op == Token::NOT) {
      // リテラルをブール条件に変換し、否定します。
      return factory()->NewBooleanLiteral(literal->ToBooleanIsFalse(), pos);
    } else if (literal->IsNumberLiteral()) {
      // 数値リテラルのみを含むいくつかの式を計算します。
      double value = literal->AsNumber();
      switch (op) {
	    // +
        case Token::ADD:
          return expression;
        // -
        case Token::SUB:
          return factory()->NewNumberLiteral(-value, pos);
        // ~
        case Token::BIT_NOT:
          return factory()->NewNumberLiteral(~DoubleToInt32(value), pos);
        default:
          break;
      }
    }
  }
  return factory()->NewUnaryOperation(op, expression, pos);
}

もしリテラルが数値型であり、単項演算子が NOT(!)でない場合、Value は Number に変換され、BIT_NOT であれば INT32 に変換されて反転演算が行われます。

4.2 JavaScriptCore におけるビット単位の NOT#

同様に、構文解析段階で TILDE(~)トークンに遭遇した際、式を作成する際に型変換の作業が行われます:

[→ Parser/Parser.cpp]

template <typename LexerType>
template <class TreeBuilder> TreeExpression Parser<LexerType>::parseUnaryExpression(TreeBuilder& context)
{
	//... 省略無関係なコード
	 while (tokenStackDepth) {
 		switch (tokenType) {
		//... 省略無関係なコード
		// ~
		case TILDE:
     			expr = context.makeBitwiseNotNode(location, expr);
     			break;
	     // +
		case PLUS:
      			expr = context.createUnaryPlus(location, expr);
     			break;
		//... 省略無関係なコード
		}
	}
}

[→ parser/ASTBuilder.h]

ExpressionNode* ASTBuilder::makeBitwiseNotNode(const JSTokenLocation& location, ExpressionNode* expr)
{
	if (expr->isNumber())
        return createIntegerLikeNumber(location, ~toInt32(static_cast<NumberNode*>(expr)->value()));
    return new (m_parserArena) BitwiseNotNode(location, expr);
}

[→ parser/NodeConstructors.h]

inline BitwiseNotNode::BitwiseNotNode(const JSTokenLocation& location, ExpressionNode* expr)
        : UnaryOpNode(location, ResultType::forBitOp(), expr, op_bitnot)
{
}

4.3 QuickJS におけるビット単位の NOT#

QuickJS では、構文解析段階で~トークンに遭遇すると、emit_op(s, OP_not)が呼び出されます:

[→ quickjs.c]

/* allowed parse_flags: PF_ARROW_FUNC, PF_POW_ALLOWED, PF_POW_FORBIDDEN */
static __exception int js_parse_unary(JSParseState *s, int parse_flags)
{
    int op;

    switch(s->token.val) {
    case '+':
    case '-':
    case '!':
    case '~':
    case TOK_VOID:
        op = s->token.val;
        if (next_token(s))
            return -1;
        if (js_parse_unary(s, PF_POW_FORBIDDEN))
            return -1;
        switch(op) {
        case '-':
            emit_op(s, OP_neg);
            break;
        case '+':
            emit_op(s, OP_plus);
            break;
        case '!':
            emit_op(s, OP_lnot);
            break;
        case '~':
            emit_op(s, OP_not);
            break;
        case TOK_VOID:
            emit_op(s, OP_drop);
            emit_op(s, OP_undefined);
            break;
        default:
            abort();
        }
        parse_flags = 0;
        break;
	//...
	}
    //...
}

emit_opは OP_not バイトコードオペレーターを生成し、ソースコードを fd->byte_code に保存します。

static void emit_op(JSParseState *s, uint8_t val)
{
    JSFunctionDef *fd = s->cur_func;
    DynBuf *bc = &fd->byte_code;

    /* Use the line number of the last token used, not the next token,
       nor the current offset in the source file.
     */
    if (unlikely(fd->last_opcode_line_num != s->last_line_num)) {
        dbuf_putc(bc, OP_line_num);
        dbuf_put_u32(bc, s->last_line_num);
        fd->last_opcode_line_num = s->last_line_num;
    }
    fd->last_opcode_pos = bc->size;
    dbuf_putc(bc, val);
}

int dbuf_putc(DynBuf *s, uint8_t c)
{
	return dbuf_put(s, &c, 1);
}

int dbuf_put(DynBuf *s, const uint8_t *data, size_t len)
{
    if (unlikely((s->size + len) > s->allocated_size)) {
        if (dbuf_realloc(s, s->size + len))
            return -1;
    }
    memcpy(s->buf + s->size, data, len);
    s->size += len;
    return 0;
}

QuickJS の解釈実行関数はJS_EvalFunctionInternalで、JS_CallFreeを呼び出してバイトコードの解釈実行を行います。その核心ロジックはJS_CallInternal関数を呼び出すことです。

/* argv[] is modified if (flags & JS_CALL_FLAG_COPY_ARGV) = 0. */
static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
                               JSValueConst this_obj, JSValueConst new_target,
                               int argc, JSValue *argv, int flags)
{
    JSRuntime *rt = caller_ctx->rt;
    JSContext *ctx;
    JSObject *p;
    JSFunctionBytecode *b;
    JSStackFrame sf_s, *sf = &sf_s;
    const uint8_t *pc;
	// ...省略無関係なコード
	
	for(;;) {
		int call_argc;
		JSValue *call_argv;
		SWITCH(pc) {
		// ...
		CASE(OP_not):
		{
			JSValue op1;
			op1 = sp[-1];
			// 整数の場合
			if (JS_VALUE_GET_TAG(op1) == JS_TAG_INT) {
				sp[-1] = JS_NewInt32(ctx, ~JS_VALUE_GET_INT(op1));
			// 整数でない場合
			} else {
				if (js_not_slow(ctx, sp))
					goto exception;
			}
		}
		BREAK;
		// ...
	}
	// ...
}

ここで、OP_not に遭遇した場合、もし整数であればそのまま反転し、そうでなければjs_not_slowを呼び出します:

static no_inline int js_not_slow(JSContext *ctx, JSValue *sp)
{
    int32_t v1;

    if (unlikely(JS_ToInt32Free(ctx, &v1, sp[-1]))) {
        sp[-1] = JS_UNDEFINED;
        return -1;
    }
    sp[-1] = JS_NewInt32(ctx, ~v1);
    return 0;
}

js_not_slowは変換を試み、変換できなければ - 1 を返し、変換できれば整数に変換して反転します。JS_ToInt32Freeの変換ロジックは以下の通りです:

/* return (<0, 0) in case of exception */
static int JS_ToInt32Free(JSContext *ctx, int32_t *pres, JSValue val)
{
 redo:
	tag = JS_VALUE_GET_NORM_TAG(val);
	switch(tag) {
	case JS_TAG_INT:
	case JS_TAG_BOOL:
	case JS_TAG_NULL:
	case JS_TAG_UNDEFINED:
		ret = JS_VALUE_GET_INT(val);
		break;
		// ...
	default:
		val = JS_ToNumberFree(ctx, val);
		if (JS_IsException(val)) {
			*pres = 0;
			return -1;
		}
		goto redo;
	}
    *pres = ret;
    return 0;
}

文字列の場合は、JS_ToNumberFreeに進み、その後JS_ToNumberHintFreeを呼び出します。文字列処理に関する核心的なロジックは以下の通りです:

static JSValue JS_ToNumberHintFree(JSContext *ctx, JSValue val,
                                   JSToNumberHintEnum flag)
{
    uint32_t tag;
    JSValue ret;

 redo:
    tag = JS_VALUE_GET_NORM_TAG(val);
    switch(tag) {
    // ...省略無関係なロジック
	case JS_TAG_STRING:
        {
            const char *str;
            const char *p;
            size_t len;
            
            str = JS_ToCStringLen(ctx, &len, val);
            JS_FreeValue(ctx, val);
            if (!str)
                return JS_EXCEPTION;
            p = str;
            p += skip_spaces(p);
            if ((p - str) == len) {
                ret = JS_NewInt32(ctx, 0);
            } else {
                int flags = ATOD_ACCEPT_BIN_OCT;
                ret = js_atof(ctx, p, &p, 0, flags);
                if (!JS_IsException(ret)) {
                    p += skip_spaces(p);
                    if ((p - str) != len) {
                        JS_FreeValue(ctx, ret);
                        ret = JS_NAN;
                    }
                }
            }
            JS_FreeCString(ctx, str);
        }
        break;
	// ...省略無関係なロジック
	}
	// ...省略無関係なロジック
}

変換可能な場合はJS_NewInt32を使用し、そうでなければ NaN を返します。

5. 単項演算子(+)#

ECMAScript (ECMA-262) 単項プラス演算子

image

単項演算子のプラスは、筆者が最も好んで使用する文字列を数値に変換する方法の一つです。標準には特に派手なことはなく、非常に簡潔で明確で、数値型変換のために使用されます。

5.1 V8 における UnaryPlus#

構文解析段階はダブルチルダ(~~)演算子と同様であり、ここでは詳細に解説しません。

5.2 JavaScriptCore における UnaryPlus#

構文解析段階はダブルチルダ(~~)演算子と同様であり、ここでは詳細に解説しません。

5.3 QuickJS における UnaryPlus#

構文解析段階はダブルチルダ(~~)演算子と同様であり、ここでは詳細に解説しません。最終的にはJS_CallInternalに進みます。

[→ quickjs.c]

/* argv[] is modified if (flags & JS_CALL_FLAG_COPY_ARGV) = 0. */
static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
                               JSValueConst this_obj, JSValueConst new_target,
                               int argc, JSValue *argv, int flags)
{
    JSRuntime *rt = caller_ctx->rt;
    JSContext *ctx;
    JSObject *p;
    JSFunctionBytecode *b;
    JSStackFrame sf_s, *sf = &sf_s;
    const uint8_t *pc;
	// ...省略無関係なコード
	
	for(;;) {
		int call_argc;
		JSValue *call_argv;
		SWITCH(pc) {
		// ...
		CASE(OP_plus):
			{
			    JSValue op1;
				uint32_t tag;
				op1 = sp[-1];
				tag = JS_VALUE_GET_TAG(op1);
				if (tag == JS_TAG_INT || JS_TAG_IS_FLOAT64(tag)) {
				} else {
					if (js_unary_arith_slow(ctx, sp, opcode))
				 		goto exception;
				}
				BREAK;
			}
		// ...省略無関係なコード
		}
	}
	// ...省略無関係なコード
}

操作数が Int または Float の場合は、そのまま処理を行い、標準に従って一致します。他の状況ではjs_unary_arith_slowを呼び出し、呼び出し中に例外が発生した場合は例外処理に進みます:

static no_inline __exception int js_unary_arith_slow(JSContext *ctx, JSValue *sp, OPCodeEnum op)
{
    JSValue op1;
    double d;

    op1 = sp[-1];
    if (unlikely(JS_ToFloat64Free(ctx, &d, op1))) {
        sp[-1] = JS_UNDEFINED;
        return -1;
    }
    switch(op) {
    case OP_inc:
        d++;
        break;
    case OP_dec:
        d--;
        break;
    case OP_plus:
        break;
    case OP_neg:
        d = -d;
        break;
    default:
        abort();
    }
    sp[-1] = JS_NewFloat64(ctx, d);
    return 0;
}

ここでのJS_ToFloat64Freeの内部処理ロジックは、4.3 のJS_ToFloat64Freeと同様であるため、詳細に解説する必要はありません。js_unary_arith_slowが数値変換を処理した後、演算子が単項演算プラスであればそのまま返し、そうでなければ演算子に応じて相応の演算処理を行います。


これで、以下の 5 つの方法がインタプリタ内での具体的な実装について説明されました:

  1. parseInt()
  2. parseFloat()
  3. Number()
  4. ダブルチルダ(~~)演算子
  5. 単項演算子(+)

これらの 5 つの数値変換方法に加えて、以下の 4 つの方法については、篇幅の都合上、ここでは詳述しません:

  • Math.floor()
  • 数値との乗算
  • 符号付き右シフト演算子(>>)
  • 符号なし右シフト演算子(>>>)

文字列を数値に変換する方法にはそれぞれ利点と欠点があり、使用者は自分のニーズに応じて選択できます。以下は私の個人的な経験からのまとめです:

もし戻り値が整数型のみを要求する場合:

  • コードの簡潔さと実行効率を追求し、入力値にある程度の信頼がある場合(防御が不要)、優先的に単項演算子(+)を使用します。
  • 入力値に信頼がない場合、防御的なプログラミングが必要な場合は、parseInt()を使用します。
  • BigInt をサポートする必要がある場合は、優先的にNumber()を検討します。ダブルチルダ(~~)演算子を使用する場合は、31 ビットの問題に注意が必要です。

もし戻り値が浮動小数点型を要求する場合:

  • コードの簡潔さと実行効率を追求し、入力値にある程度の信頼がある場合(防御が不要)、優先的に単項演算子(+)を使用します。
  • 入力値に信頼がない場合、防御的なプログラミングが必要な場合は、parseFloat()を使用します。
  • BigInt をサポートする必要がある場合は、parseFloat()を使用します。
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。