BigInt: JavaScriptにおける任意精度整数

公開 · タグ ECMAScript ES2020

BigInt は、JavaScript の新しい数値プリミティブであり、任意精度で整数を表すことができます。 BigInt を使用すると、Number の安全な整数制限を超えても、大きな整数を安全に格納および操作できます。この記事では、いくつかのユースケースを説明し、JavaScript の NumberBigInt を比較することで、Chrome 67 の新機能を解説します。

ユースケース #

任意精度整数は、JavaScript に多くの新しいユースケースをもたらします。

BigInt を使用すると、オーバーフローすることなく、整数演算を正確に実行できます。それ自体で、数え切れないほどの新しい可能性が広がります。たとえば、金融テクノロジーでは、大きな数値に対する数学的な演算が一般的に使用されます。

大きな整数ID高精度のタイムスタンプ は、JavaScript では Number として安全に表現できません。これにより、しばしば 実際のバグ につながり、JavaScript開発者は代わりに文字列として表現せざるを得ませんでした。 BigInt を使用すると、このデータを数値として表現できるようになりました。

BigInt は、最終的な BigDecimal 実装の基礎を形成する可能性があります。これは、10進数の精度で金額を表したり、それらを正確に操作したりするのに役立ちます(いわゆる 0.10 + 0.20 !== 0.30 の問題)。

これまで、これらのユースケースを持つJavaScriptアプリケーションは、BigIntのような機能をエミュレートするユーザランドライブラリに頼らざるを得ませんでした。 BigInt が広く利用可能になると、そのようなアプリケーションは、ネイティブの BigInt を優先して、これらのランタイム依存関係を削除できます。これにより、ロード時間、解析時間、コンパイル時間が短縮され、さらにランタイムパフォーマンスが大幅に向上します。

Chromeのネイティブな BigInt 実装は、一般的なユーザランドライブラリよりも優れたパフォーマンスを発揮します。

現状: Number #

JavaScript の Number は、倍精度浮動小数点数として表現されます。これは、精度が制限されていることを意味します。 Number.MAX_SAFE_INTEGER 定数は、安全にインクリメントできる最大の整数を示します。その値は 2**53-1 です。

const max = Number.MAX_SAFE_INTEGER;
// → 9_007_199_254_740_991

注: 読みやすくするために、この大きな数の桁を、区切り文字としてアンダースコアを使用して、千単位でグループ化しています。数値リテラル区切り文字の提案は、一般的なJavaScriptの数値リテラルでまさにそれを実現します。

一度インクリメントすると、期待どおりの結果が得られます。

max + 1;
// → 9_007_199_254_740_992 ✅

ただし、2 回目にインクリメントすると、結果は JavaScript の Number として正確に表現できなくなります。

max + 2;
// → 9_007_199_254_740_992 ❌

max + 1max + 2 と同じ結果を生成することに注意してください。JavaScript でこの特定の値を取得したときは、それが正確かどうかを判断する方法はありません。安全な整数範囲(つまり、Number.MIN_SAFE_INTEGER から Number.MAX_SAFE_INTEGER まで)外の整数に対する計算は、精度が失われる可能性があります。このため、安全な範囲内の数値整数値のみを信頼できます。

新しいホットな機能: BigInt #

BigInt は、JavaScript の新しい数値プリミティブであり、任意精度で整数を表すことができます。 BigInt を使用すると、Number の安全な整数制限を超えても、大きな整数を安全に格納および操作できます。

BigInt を作成するには、任意の整数リテラルに n サフィックスを追加します。たとえば、123123n になります。グローバルな BigInt(number) 関数を使用して、NumberBigInt に変換できます。言い換えれば、BigInt(123) === 123n です。これらの 2 つの手法を使用して、以前に抱えていた問題を解決してみましょう。

BigInt(Number.MAX_SAFE_INTEGER) + 2n;
// → 9_007_199_254_740_993n ✅

次に別の例を示します。ここでは、2 つの Number を乗算しています。

1234567890123456789 * 123;
// → 151851850485185200000 ❌

最下位桁の 93 を見ると、乗算の結果は 7 で終わるはずであることがわかります(9 * 3 === 27 のため)。ただし、結果は一連のゼロで終わっています。それは正しいはずがありません! 代わりに BigInt でもう一度試してみましょう。

1234567890123456789n * 123n;
// → 151851850485185185047n ✅

今回は正しい結果が得られます。

Number の安全な整数制限は BigInt には適用されません。したがって、BigInt を使用すると、精度が失われることを心配することなく、正確な整数演算を実行できます。

新しいプリミティブ #

BigInt は、JavaScript 言語の新しいプリミティブです。そのため、typeof 演算子を使用して検出できる独自の型を取得します。

typeof 123;
// → 'number'
typeof 123n;
// → 'bigint'

BigInt は別の型であるため、BigIntNumber と厳密に等しくなることはありません。たとえば、42n !== 42 です。 BigIntNumber と比較するには、比較を行う前に、それらのいずれかを他方の型に変換するか、抽象的な等価性(==)を使用します。

42n === BigInt(42);
// → true
42n == 42;
// → true

ブール値に強制変換される場合(たとえば、if&&||、または Boolean(int) を使用する場合)、BigIntNumber と同じロジックに従います。

if (0n) {
console.log('if');
} else {
console.log('else');
}
// → logs 'else', because `0n` is falsy.

演算子 #

BigInt は、最も一般的な演算子をサポートします。バイナリの +-*、および ** は、すべて期待どおりに機能します。 /% は機能し、必要に応じてゼロに向かって丸めます。ビット演算 |&<<>>、および ^ は、負の値に対して 2 の補数表現 を仮定してビット単位の演算を実行します。これは、Number の場合と同じです。

(7 + 6 - 5) * 4 ** 3 / 2 % 3;
// → 1
(7n + 6n - 5n) * 4n ** 3n / 2n % 3n;
// → 1n

単項 - を使用して、負の BigInt 値を示すことができます。たとえば、-42n です。 単項 + は、+x が常に Number または例外を生成することを期待する asm.js コードを壊すため、サポートされていません。

注意すべき点の 1 つは、BigIntNumber の間で演算を混在させることが許可されていないことです。これは、暗黙的な強制変換によって情報が失われる可能性があるため、良いことです。次の例を考えてみましょう。

BigInt(Number.MAX_SAFE_INTEGER) + 2.5;
// → ?? 🤔

結果はどうなるべきでしょうか? ここに良い答えはありません。 BigInt は分数を表現できず、Number は安全な整数制限を超える BigInt を表現できません。そのため、BigIntNumber の間で演算を混在させると、TypeError 例外が発生します。

このルールの唯一の例外は、===(前述)などの比較演算子、<、および >= です。これらはブール値を返すため、精度の損失のリスクはありません。

1 + 1n;
// → TypeError
123 < 124n;
// → true

BigIntNumber は一般的に混在しないため、既存のコードをオーバーロードしたり、Number の代わりに BigInt を使用するように魔法のように「アップグレード」したりしないでください。操作する 2 つのドメインのどちらかを決定し、それに固執してください。潜在的に大きな整数を操作する新しいAPIの場合、BigInt が最適な選択肢です。 Number は、安全な整数範囲内にあることがわかっている整数値に対しては依然として理にかなっています。

もう 1 つ注意すべき点は、符号なし右シフトを実行する>>> 演算子は、常に符号付きであるため、BigInt では意味がないということです。このため、>>>BigInt では機能しません。

API #

いくつかの新しい BigInt 固有の API が利用可能です。

グローバルな BigInt コンストラクターは、Number コンストラクターに似ています。(前述のとおり) 引数を BigInt に変換します。変換が失敗すると、SyntaxError または RangeError 例外がスローされます。

BigInt(123);
// → 123n
BigInt(1.5);
// → RangeError
BigInt('1.5');
// → SyntaxError

これらの最初の例では、数値リテラルを BigInt() に渡しています。これは、Number が精度損失の影響を受けるため、BigInt 変換が発生する前に精度が失われる可能性があるため、悪い習慣です。

BigInt(123456789123456789);
// → 123456789123456784n ❌

このため、BigInt リテラル表記(n サフィックス付き)、または文字列(Number ではない!)を BigInt() に渡すことをお勧めします。

123456789123456789n;
// → 123456789123456789n ✅
BigInt('123456789123456789');
// → 123456789123456789n ✅

2 つのライブラリ関数を使用すると、BigInt 値を符号付きまたは符号なしの整数としてラップでき、特定のビット数に制限されます。 BigInt.asIntN(width, value) は、BigInt 値を width 桁の 2 進数符号付き整数にラップし、BigInt.asUintN(width, value) は、BigInt 値を width 桁の 2 進数符号なし整数にラップします。たとえば、64 ビット演算を実行している場合は、これらの API を使用して適切な範囲内に収めることができます。

// Highest possible BigInt value that can be represented as a
// signed 64-bit integer.
const max = 2n ** (64n - 1n) - 1n;
BigInt.asIntN(64, max);
9223372036854775807n
BigInt.asIntN(64, max + 1n);
// → -9223372036854775808n
// ^ negative because of overflow

64 ビット整数範囲(つまり、絶対数値の 63 ビット + 符号用の 1 ビット)を超える BigInt 値を渡すとすぐにオーバーフローが発生することに注意してください。

BigInt を使用すると、他のプログラミング言語で一般的に使用される 64 ビット符号付き整数と符号なし整数を正確に表現できます。 2 つの新しい型付き配列フレーバーである BigInt64ArrayBigUint64Array を使用すると、このような値のリストを効率的に表現および操作することが容易になります。

const view = new BigInt64Array(4);
// → [0n, 0n, 0n, 0n]
view.length;
// → 4
view[0];
// → 0n
view[0] = 42n;
view[0];
// → 42n

BigInt64Array フレーバーは、その値が符号付き 64 ビット制限内に収まるようにします。

// Highest possible BigInt value that can be represented as a
// signed 64-bit integer.
const max = 2n ** (64n - 1n) - 1n;
view[0] = max;
view[0];
// → 9_223_372_036_854_775_807n
view[0] = max + 1n;
view[0];
// → -9_223_372_036_854_775_808n
// ^ negative because of overflow

BigUint64Array フレーバーは、代わりに符号なし 64 ビット制限を使用して同じことを行います。

BigInt のポリフィルとトランスパイル #

執筆時点では、BigInt は Chrome でのみサポートされています。他のブラウザは、それらを実装するために積極的に取り組んでいます。しかし、ブラウザの互換性を犠牲にすることなく、今日 BigInt 機能を使用したい場合はどうすればよいでしょうか? 聞いてくれて嬉しいです!答えは…控えめに言って興味深いものです。

他のほとんどの最新の JavaScript 機能とは異なり、BigInt は ES5 に合理的にトランスパイルできません。

BigInt の提案は、BigInt で動作するように(+>= などの)演算子の動作を変更します。これらの変更は直接ポリフィルすることが不可能であり、また、Babel または同様のツールを使用して BigInt コードをフォールバックコードにトランスパイルすることを(ほとんどの場合)不可能にしています。その理由は、このようなトランスパイルでは、プログラム内のすべての単一の演算子を、入力で型チェックを実行する関数の呼び出しに置き換える必要があり、容認できないランタイムパフォーマンスのペナルティが発生するためです。さらに、トランスパイルされたバンドルのファイルサイズが大幅に増加し、ダウンロード、解析、コンパイル時間に悪影響を及ぼします。

より実現可能で将来性のある解決策は、当面の間、JSBIライブラリを使用してコードを記述することです。 JSBI は、V8 および Chrome の BigInt 実装の JavaScript ポートです。設計上、ネイティブの BigInt 機能とまったく同じように動作します。違いは、構文に依存するのではなく、APIを公開することです。

import JSBI from './jsbi.mjs';

const max = JSBI.BigInt(Number.MAX_SAFE_INTEGER);
const two = JSBI.BigInt('2');
const result = JSBI.add(max, two);
console.log(result.toString());
// → '9007199254740993'

すべてのブラウザーで BigInt がネイティブにサポートされるようになったら、babel-plugin-transform-jsbi-to-bigint を使用してコードをネイティブの BigInt コードにトランスパイルし、JSBI 依存関係を削除できます。たとえば、上記の例は以下のようにトランスパイルされます。

const max = BigInt(Number.MAX_SAFE_INTEGER);
const two = 2n;
const result = max + two;
console.log(result);
// → '9007199254740993'

さらに詳しい情報 #

もしあなたがBigIntが舞台裏でどのように動作するのか(例えば、メモリ内でどのように表現され、それらに対する演算がどのように実行されるのか)に興味があるなら、実装の詳細を解説したV8のブログ記事をご覧ください。

BigIntのサポート #