
2026/04/26 19:56
# NAN(Not a Number)の秘めたる生活 NAN(Not a Number)は、コンピュータシステムにおいて数値演算の結果が未定義であるか、表現不可能であることを示す特別な浮動小数点数です。名前には「単なる『数ではない』もの」という印象を与えるかもしれませんが、実際には Python や JavaScript、C++ といったプログラミング言語における数学的な整合性を維持する上で、極めて重要な役割を果たしています。 ## NAN とは何か? NAN は **Not a Number**(数ではない)の略称であり、浮動小数点算術のための IEEE 754 標準において導入されました。他の数値とは異なり、NAN は以下のような独特の性質を備えています: - `NaN == NaN` は常に `False` です(自身と等しいことはありません)。 - NAN を含むあらゆる比較演算の結果は `False` となります。 - 明示的に処理されない限り、計算結果に伝播します。 ### NAN が生じる主な原因 | 操作 | 例 | 結果 | |------|-----|------| | ゼロでの除法(場合による) | `0 / 0` | NAN | | 負数の平方根 | `sqrt(-1)` | NAN | | 非正数の対数 | `log(-5)` | NAN | | 無効な型変換 | `"abc" * 2`(一部の言語) | NAN | ## コード内での NAN の処理方法 ### Python の例 ```python import math # NAN の作成 result = 0 / 0 # nan が返される print(result) # 出力: nan # NAN の検出 if math.isnan(result): print("これは有効な数値ではありません") # try-except やチェックを用いた安全な計算処理 def safe_divide(a, b): if b == 0: return None # または必要に応じて別の方法で対応 return a / b ``` ### JavaScript の例 ```javascript const result = NaN; console.log(typeof result); // "number" if (Number.isNaN(result)) { console.log("結果は数値ではありません"); } // 安全な除法関数 function safeDivide(a, b) { if (b === 0) return null; return a / b; } ``` ## なぜ NAN が存在するのか? NAN は、以下の区別を明確にするために存在します: 1. **エラー**(例:ゼロでの除法) 2. **未定義の結果**(例:`0/0`) 3. **表現不可能な値**(例:実数系における `sqrt(-1)`) この区別により、プログラムは問題のある計算を後で確認できるようにしながらも、健全に動作し続けることができます。 ## 推奨されるベストプラクティス - 演算を実行する前に、必ず入力値を検証してください。 - `x == NaN` ではなく、`math.isnan()`(Python)または `Number.isNaN()`(JavaScript)などのライブラリ関数を使用してください。 - データパイプラインの早期段階で NAN を処理し、沈黙型の失敗を未然に防ぎましょう。 - 文脈に応じて、NAN を意味のあるデフォルト値(例:0、null、特定のフラグ)に置き換えることも検討してください。 --- NAN の秘めたる生活を理解することは、あらゆるプログラミング環境において、より堅牢で予測可能な数値コードを記述するための開発者の助けとなります。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
現代の JavaScript エンジン(例:Safari の JavaScriptCore(JSC))は、メモリ使用量とパフォーマンスを最適化する技術として「NaN boxing」と呼ばれる手法を採用しています。この手法では、IEEE 754 標準に基づき、符号ビット、指数部、そして診断情報やタイプタグのための 51 ビットのペイロードを含む 52 ビットのマンティッサを持つ二重精度浮動小数点数形式を再利用し、IEEE 754 の NaN(数値なし)の未使用ビット内で、すべてのデータ型——数字、オブジェクト、ブール値、null、undefined——を格納します。そのためには、有効な数値範囲に対してオフセット $2^{48}$(0x1000000000000ll)を追加し、低位のビットをポインタの格納および ECMAScript のリテラル用として特別に割り当てたビットパターンを確保します。具体的には、False(0x06)、True(0x07)、Undefined(0x0a)、Null(0x02)、ValueEmpty(0x00)、ValueDeleted(0x04)といった値が対応します。この手法は効率性を著しく向上させた一方、厳格な検証プロトコルが適用される前には、これらの特別に設計された NaN ペイロードを操作することで CVE-2010-1807 といった脆弱性を引き起こすという歴史的なセキュリティリスクをもたらしました。動的型付け言語において計算速度とシステムセキュリティのバランスを取るためには、quiet NaN(qNaN)と signaling NaN(sNaN)の役割を理解することが不可欠です。
本文
IEEE 浮動小数点規格は、数値ではない値を表現するために用いられる特殊な値「Not-a-Number(NaN)」を定義しています。倍精度の NaN では、自由に用途が決められるペイロードとして 51 ビットの空間が用意されており、特に動的型付け言語において実行時における全ての非浮動小数点値とその型をこのペイロードに格納するという、非常に面白い応用例があります。
%%%%%% 更新(2019 年 4 月) %%%%
私が !!con West 2019 で開催した「NaN の秘めたる生活」に関するライトニングトークの内容です。今回は詳細は簡略化されていますが、より多くのジョークを盛り込んでいます。録画映像はこちらからご覧いただけます。
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
私がここで言う「NaN」と「浮動小数点」とは、主に 1985 年に(多くの変遷を伴って)誕生した汎用性の高い IEEE 754-2008 規格で定義された表現方法を指します。この規格は、異なるプロセッサが使っていた多様な不整合な浮動小数点表現によって引き起こされる「無法状態」を鎮め、コードの移植性を確保するための標準的な表現手法としての必要性から生まれました。
浮動小数点値は実数に対する離散的で対数的な近似であり、以下に示す 3 ビットの指数部、3 ビットの有効数字(仮数のこと)を持つ玩具的な浮動小数点表現により定義される点を可視化した図です(画像出典:論文「区間の中央値をどのように計算するのか?」より。この論文は中央値の計算でよく現れる算術的な偽像について指摘しています)。
私がここで論じる NaN は IEEE 754-2008 の外には存在しないため、以下では規格を簡単に解説します。
IEEE 754-2008 の極めて簡潔な概要
この規格は、基底 2 および基底 10 を用いた対数的近似値の分布を定義しています。基底 2 にては、16 ビットから 256 ビットまでの 2 のべき乗幅のすべての表現を定義し(正確にはそうではありません。詳細な規格については仕様書の 13 ページを参照)、基底 10 では 32 ビットから 128 ビットまでの 2 のべき乗幅の表現を定義しています。これらが唯一の標準化されたビット幅です。つまり、プロセッサが 32 ビットの浮動小数点値をサポートしている場合、その実装が規格準拠の表現で行われている可能性は極めて高いと言えます。
さて、規格に準拠した表現を見てみましょう。まずは基底 2 の 16 ビット幅である「binary16」フォーマットです:
1 符号ビット | 5 指数ビット | 10 有効数字ビット S E E E E E M M M M M M M M M M
これらのビットが数値を表現する方法については別の機会に譲りますが、説明を求める場合はこちらのわかりやすい解説記事もおすすめです。
簡潔に触れると、例えば以下のように様々な値をこれら 16 ビットで符号化できます。要点は、この 16 ビットを使って多様な値を表現できることです。
0 01111 0000000000 = 1 0 00000 0000000000 = +0 1 00000 0000000000 = -0 1 01101 0101010101 = -0.333251953125
以上のように、私たちは有限で離散的な実数の集合を表現することができるようになります。数値表現としてこれが望ましい状況です。
さらに興味深くは、規格はまた特殊な値である「±無限大」と、「静かな(quiet)」ならびに「シグナリングする(signaling)」NaN を定義しています。「±無限大」は自明なオーバーフロー挙動を示します:上記の可視化では ±15 が正確に表現可能な最大の絶対値であり、それ以上の絶対値を持つ値との計算によってオーバーフローが生じると、±無限大へと遷移します。規格書には、異なる丸めモードに基づいて操作の結果として±無限大を返すべきかどうかの指針が記されています。
IEEE 754-2008 が NaN について述べていること
まず NaN がどのように表現されるか見てから、「静かな」と「シグナリングする」の違いを整理しましょう。
規格には以下のように記載されています(仕様書 p.35, §6.2.1):
すべてのバイナリ形式の NaN ビット列では、偏った指数部 E のすべてのビットが 1 に設定されている (参照 3.4)。静かな NaN のビット列は、桁落ち仮数字段 T の最初のビット(d1)を 1 にすることで符号化されるべきである。シグナリングする NaN のビット列は、桁落ち仮数字段の最初のビットを 0 にすることで符号化されるべきである。
例えば、binary16 フォーマットにおいて、NaN は以下のビットパターンで指定されます:
s 11111 1xxxxxxxxxx = 静かな (qNaN) s 11111 0xxxxxxxxxx = シグナリングする (sNaN) **
これは非常に多くのビットパターンの集合です。符号ビットを無視しても、桁落ち仮数字段の位数から減じた数のビット分(2 のべき回)のパターンが存在し、これらすべてのパターンは NaN を表現しています。私たちはこれらの残りビットを「ペイロード」と呼びましょう。注釈:sNaN の場合、少なくとも桁落ち仮数字段の 1 ビットは 1 に設定する必要があり、全ゼロのペイロードを持つことはできません。なぜなら、指数部が全 1 で仮数部分が全 0 のビットパターンは無限大を表現するからであり、その状態にありえないためです。
私には、NaN がシグナリングするか否かを表すフラグとして仮数字段の上位ビットが使われていることが奇妙に思えます。おそらく浮動小数点パイプラインの実装方法に関連して、符号ビットを使って例外の有無を判定するのが不自然だからではないでしょうか。
現代のコムモディティ(汎用)ハードウェアではよく 64 ビットの浮動小数点(倍精度)が使われており、倍精度フォーマットでは仮数字段が 52 ビットあるため、ペイロードとして利用可能なビットは 51 ビットとなります。
さて、ここで「静かな」と「シグナリングする」NaN の違いを見てみましょう(仕様書 p.34, §6.2):
シグナリングする NaN は、初期化されていない変数や標準の範囲を超える算術的な拡張表現(例えば複素線形無限大や極めて広い範囲など)、といったこの規格の範囲外にあるものを表現することを可能にします。一方、静かな NaN は、実行者の裁量によって、無効なデータまたは利用できないデータから引き継がれた診断情報および結果を後から検証するための情報を提供すべきです。NaN に含まれる診断情報の伝播を促進するため、可能な限り多くの情報を NaN の演算結果に保持するべきです。
通常の例外処理下では、無効な操作(invalid operation)例外を発し、かつ浮動小数点の結果が返されなければならないようなすべての操作は、静かな NaN を返すものとする。
つまり、「シグナリングする」NaN は例外を発生させることがあり、規格自体が浮動小数点がハードウェアかソフトウェアで実装されているかを無視しており、具体的にどのような例外かが明確にはされていません。ハードウェアの場合、これは浮動小数点ユニット(FPU)が例外フラグを設定することに対応し、C の標準では浮動小数点の計算例外を表すために SIGFPE シグナルを定義・要求しています。
この最後の引用文は、「シグナリングする」NaN を受ける操作が警報を発した上で、その NaN を静かにして伝播させることを示唆しています。なぜそのような状況が生じるのでしょうか?それは最初の引用文で説明されています:初期化されていない変数を「シグナリングする」NaN で表現することで、もし誰かがその値を操作しようとした場合(最初に初期化する前に)、それが意図した行動ではなかった可能性が高いことを告げることが可能となるのです。
逆に、「静かな」NaN は一般的な NaN です。qNaN(quiet NaN)は、真に数ではない結果が得られる際に生成されます(例えば負の数の平方根を取ろうとした場合など)。ここで特に注意すべきなのは以下の一文です:
NaN に含まれる診断情報の伝播を促進するため、可能な限り多くの情報を NaN の演算結果に保持するべきです。
つまり、IEEE 浮動小数点規格による公式な推奨は、ある qNaN をその状態のまま変更せずにそのまま使用し、上記で見たようなペイロードを使って「診断情報」を伝播させることに使うという方向性です。「これは NaN に追加情報を無理やり詰め込むための手招きでしょうか?」という問いに対し、答えは YES です!
ペイロードをどう活用できるのか?
私が本当に興味を持っているのはこの点、あるいはさらに具体的に言えば、「人々はペイロードをどのように利用しているのか」という問いです。
この質問に対する最も満足できる回答は、動的型付け言語においてデータを型情報とともに伝播させるために、NaN のペイロードを利用しているという事実でした。具体的な実装例としては Lua や JavaScript などが挙げられます。なぜ動的型付け言語なのか?それは、変数の型が実行時に変化するため、型の情報を必ずとも伝えなければならないからです。ここで NaN のペイロードは、その型情報に加えて実際の値を両方とも格納する機会となります。以下では、その実装例の一つについて詳しく見ていきましょう。
他の用途を探したところあまり見つけられませんでした。以下に載っている教科書(p.86)にはいくつか提案が挙げられています:
一つの可能性としては、記号式解析器の中で NaN を記号として用いることです。別の案としては、NaN を欠測データ値として使い、ペイロードを欠測データの源泉またはクラスを示すために利用することです。
著者が何か具体的なことを持っていた可能性はありますが、記号としての利用や欠測データの源泉示唆のための実装は見つけることができませんでした。もし NaN ペイロードの他の実用的な利用法を知っている方がいらっしゃいましたら、ぜひお聞かせください!
では、JavaScriptCore がペイロードを使って型情報を格納する方法を見てみましょう:
実践におけるペイロード!JavaScriptCore を探る
ここでは「NaN-boxing」と呼ばれる技術の実装を調べます。NaN-boxing では、言語内のすべての値とその型タグを 64 ビットで表現します。有効な倍精度浮動小数点値は IEEE 754 表現のまま残されますが、NaN のペイロードの余分なスペースには、言語内の他のすべての値と、そのペイロードの型を示すタグが格納されます。「数ではない」という代わりに、「倍精度浮動小数点ではないが、『何か別の型』である」と言い換えるようなイメージです。
ここでは JavaScriptCore(JSC)の NaN-boxing の実装を見ていますが、JSC だけが他型の値を NaN に格納する産業用実装ではありません。例えば Mozilla の SpiderMonkey JavaScript 実装も「nun-boxing」および「pun-boxing」と呼ばれるこの手法を採用しており、LuaJIT はそれを「NaN タギング(NaN-tagging)」と呼んでいます。私が JSC を取り上げる理由は、そのコードに実装を説明する非常に優れたコメントがあるからです。
JSC は WebKit 上で動作する Safari および Adobe クリエイティブスイートなどを実行するための JavaScript 実装です。確認できる限り、我々が検討するコードは現在なお Safari に使用されているものです(2018 年 3 月時点でのファイルの最終更新日は 18 日前でした)。
以下のファイルを調べます。NaN-boxing の仕組みは、非浮動小数点型(ポインタ、整数、ブール値など)を持つ場合はそのペイロードに格納し、上位ビットを使ってペイロードの型を符号化するところにあります。倍精度浮動小数点の場合、ペイロードとして 51 ビット利用できるので、そこに収まるあらゆる値を格納できます。特に注目すべきは、32 ビットの整数と、現在の x86-64 アーキテクチャにおけるポインタ幅である 48 ビットのポインタを格納できることです。つまり、言語内のすべての値を 64 ビットで表現することができるようになります。
付記:ECMAScript 標準によると、JavaScript には原始的な整数データ型が存在せず、すべてが倍精度浮動小数点です。なぜ JS 実装が整数を表現したいのでしょうか?一つの見事な理由は、整数演算がハードウェア上で非常に高速に行えること、また多くの数値値が実際には整数として使われることです。顕著な例は、配列全体を反復する for ループ内のインデックス変数です。また ECMAScript 規格によれば、配列の要素数は 2^32 に制限されているため、配列のインデックス変数を NaN ペイロードとして 32 ビット整数で格納することも安全です。
使用されている符号化スキームは以下の通りです:
- 上位 16 ビットは符号化した JSValue の型を示す:
- Pointer { 0000:PPPP:PPPP:PPPP / 0001:::**** Double { ... \ FFFE:::**** Integer { FFFF:0000:IIII:IIII
私たちが実装したスキームでは、倍精度値を符号化する際に 64 ビット整数加算により 2^48 を加算します。この操作後、符号化した任意の倍精度値は 0x0000 もしくは 0xFFFF で始まることはありません。 後続の浮動小数点演算を行う前に、必ずこの逆操作を適用して値を復元する必要があります。
このコメントによれば、異なる値の範囲が異なるオブジェクトタイプを表すために使用されていますが、注目すべきはこのビット範囲は IEEE-754 で定義されたものと一致していません。例えば倍精度浮動小数点の有効な qNaN については:
1 符号ビット | 11 指数ビット | 52 有効数字ビット 1 | 1 1 1 1 1 1 1 1 1 1 1 | 1 + {51 ビットのペイロード}
バイトごとにグループ化すると: 1 1 1 1 | 1 1 1 1 | 1 1 1 1 | 1 + {51 ビットのペイロード}
これは、以下の範囲内のすべてのビットパターンを表します: 0x F F F F ... から 0x F F F 8 ...
つまり規格によると、有効な倍精度値と qNaN で表現される usual なビット範囲は:
/ 0000:****:****:****
Double { ... \ FFF7:::**** / FFF8:::**** qNaN { ... \ FFFF:::****
となります。コード内のコメントは、表現範囲が標準で定義されているものとずれていることを示しています。これを行う理由はポインタを優先するためです:ポインタは上位 2 バイトがゼロの範囲に占めるため、マスクを適用せずにポインタを操作できます。その結果、ポインタのみは「ボックス化(boxing)」されず、他のすべての値だけがそれになります。このポインタを優先する選択は必ずしも直感的ではありません;SpiderMonkey の実装では範囲をシフトせず、倍精度値を優先しています。
さて、この範囲シフトについて最も簡単に理解できるようにするために、ファイル下部のマスクを見てみましょう:
// This value is 2^48, used to encode doubles such that the encoded value will begin // with a 16-bit pattern within the range 0x0001..0xFFFE. #define DoubleEncodeOffset 0x1000000000000ll
このオフセットは asDouble() 関数で使用されます:
inline double JSValue::asDouble() const { ASSERT(isDouble()); return reinterpretInt64ToDouble(u.asInt64 - DoubleEncodeOffset); }
これにより符号化した倍精度値を規格で定義された通常のビットパターン範囲へシフトします。逆に、asCell() 関数(JSC では「セル」と「ポインタ」はほぼ同義)では、このオフセット適用なしでポインタを直接取得できます:
ALWAYS_INLINE JSCell* JSValue::asCell() const { ASSERT(isCell()); return u.ptr; }
Cool. 実際にはここでの本質はこれだけです。以下に JSC 実装からのいくつかの興味深いエピソードに触れますが、これが NaN-boxing の核心です。
他の値についてはどうでしょうか?
コメントで「上位 2 バイトがゼロならペイロードはポインタである」という部分は嘘でした。あるいは、言い換えると、過度に単純化されたものです。JSC は特定の無効なポインタ値を予約して、ECMAScript 標準で要求される即時値(immediates)を表しています:ブール値、未定義、null です:
False: 0x06
-
True: 0x07 -
Undefined: 0x0a -
Null: 0x02
これらすべては 2 番目のビットが設定されており、これらの即時値の一つであるかを簡単に判定できるようにしています。
また標準では要求されていない 2 つの即時値も表現します:配列の穴(holes)を表すために使用される ValueEmpty(0x00)、削除された値をマークするために使用される ValueDeleted(0x04)。
最後に、Wasm のポインタへの参照も 0x03 で表現しています。
つまり、これらをすべて集めると、JSCにおけるビットパターン符号化の完全な概要は以下のようになります:
ValEmpty { 0000:0000:0000:0000
-
Null { 0000:0000:0000:0002 -
Wasm { 0000:0000:0000:0003 -
ValDeltd { 0000:0000:0000:0004 -
False { 0000:0000:0000:0006 -
True { 0000:0000:0000:0007 -
Undefined { 0000:0000:0000:000a -
Pointer { 0000:PPPP:PPPP:PPPP -
/ 0001:****:****:**** -
Double { ... -
\ FFFE:****:****:**** -
Integer { FFFF:0000:IIII:IIII
要点
- IEEE 浮動小数点規格は NaN のペイロードに多くの余白を残しています。これは意図的な設計です。
- 実際の用途では、これらのペイロードがどう使われているかについては、私が正直言うとあまり分かりません。他の実用例をご存じの方がいらっしゃいましたら、ぜひお聞かせください。
- その一つの用途が「NaN-boxing」であり、これは言語内の全ての非浮動小数点値とその型情報を NaN のペイロードに格納するという手法です。非常に美しいハックと言えます。
付録:NaN をボックス化するか、しないか
この実装を見るだけで、NaN-boxing が良いアイデアなのか、あるいは「bizzo(奇妙な)」なハックなのかという問いが生まれます。私は動的型付け言語を実装・保守していない者として、この質問に適切に答えられる立場にはありません。しかし、多くのアプローチがあり、それぞれ独自の細かなトレードオフがあることは間違いありません。その前提として、いくつかの長所と短所の概略を示します。
長所: メモリ節約、すべての値がレジスタに収まる、ビットマスク適用が高速であること。 短所: ほぼすべての値をボックス化・アンボックス化する必要があるため実装が難しくなる、検証バグが深刻なセキュリティ脆弱性に転じうる。
NaN-boxing のトレードオフについて、動的型付け言語を実装・保守する者の視点をまとめたより良い議論については、以下の記事をご覧ください。
パフォーマンスの問題 aparte として、JSC で発見された脆弱性についてのこれらのレポートもご覧ください(以下)。これらすべての脆弱性が、もし JSC が他のアプローチを採用していれば防止できたかという点については議論の余地がありますが、少なくとも一つは別の手法を使っていれば防ぐことができていたと考えられます:
この方式では構造体の 8 バイトすべてを制御できますが、他の制限もあります(いくつかの浮動小数点正規化操作は真正のアビトラリーな値を書き込むことを許さず。そうでないと CellTag を作り上げてポインタを任意の値に設定でき、それは恐ろしいことになります。面白いことに、これまではそれが許可されており、最初の Vita WebKit エクスプロイト(CVE-2010-1807)がまさにこれを利用していました!)
JSC のメモリモデルについてさらに詳しく知りたい場合は、以下の非常に詳細な記事もおすすめです。