はじめに
Rubyの数値クラス(Numeric)には、大きく分けてInteger(整数)とFloat(小数)があります。
足し算や掛け算といったお馴染みに計算はどちらでも共通して使えますが、実はIntegerにはできてFloatには「絶対にできない」演算があります。
それが、シフト演算(<<,>>)やビット演算(&,|,^,~)などのビットレベルの操作です。
10 << 1 #=> (Integerは成功する)
10.0 << 1 #=> NoMethodError: undefined method `<<' for 10.0:Float (Floatはエラーになる!)
「同じ数字を扱う仲間なのに、なぜFloatにはビット演算のメソッドが用意されていないのか?」
この記事では、RubyでFloatにビット演算を実装されていない理由を、「コンピューターが小数をどうやって保存しているのか(IEEE 754)」という内部構造の視点から紐解いていきます。
直感的なイメージ : 「純粋な数値」と「精密機械」
難しい話に入る前に、直感的なイメージで捉えてみましょう。 ビットの並びを「Excel(スプレッドシート)の列(A列、B列、C列...)」だと想像してください。
-
Integer(整数)のビットは「ただの数値が並んだセル」 整数のビットは、すべてのセル(列)が「ただの数値」という均一な意味を持っています。セルを範囲選択して、ドラッグ&ドロップで強引に1マス左にズラして(シフトして)も、全体の桁が上がって数値が大きくなるだけで、データとして壊れることはありません。 -
Float(小数)のビットは「役割が決まっているフォーマット表」 小数のビットは、A列が「会員フラグ(符号)」、B列が「生年月日(指数)」、C列以降が「氏名(仮数)」というように、列ごとに全く違う役割が割り当てられたフォーマット表(構造体)です。 これを範囲選択して、強引に1マス左にズラす(シフト演算する)とどうなるでしょうか? 「氏名」の文字が「生年月日」の列に侵入し、「生年月日」の数字が「会員フラグ」の列に押し出されてしまいます。参照していた関数もバグり、データとして完全に意味不明なゴミになりますよね。
だからこそ、Rubyは「Float(フォーマット表)に対して、セルを強引にズラす操作(シフト演算)を使うのは危険すぎる!」として、最初からメソッドを用意していないのです。
Floatの内部構造(IEEE 754)
では、具体的にFloatビットはどうなっているのでしょうか。
RubyのFloatクラスは、実際の内部処理では「64ビット(倍精度浮動小数点数)」採用しています。しかし、64個の「0と1」を並べて解説すると目がチカチカしてしまうため、ここでは基本原理が全く同じである「32ビット(単精度)」のサイズを例に構造を見ていきます。(※実際の64ビットの構成については記事の最後に補足します)
小数は、以下の3つの専用の「部屋」に分けて保存されます。
【32ビットモデルの構造】
-
S(符号部 : 1bit) :
0ならプラス、1ならマイナス - E(指数部 : 8bit) : 小数点の位置(ケタの大きさ)。マイナス表現を避けるため、バイアス(+127のゲタ) を履かせて、すべて正の整数として保存します。
-
M (仮数部: 23bit) : 有効数字。正規化(
1.◯◯の形)したのち、「先頭は絶対に1になるから」という理由で先頭の1.を省略(ケチ表現 / Hidden bit)して保存します。
このように、Floatのデータには「ゲタを履いた数字」や「省略された暗黙の数字」という特殊な暗号ルールが使われています。
もしFloatビット演算をしてしまったら?(崩壊シュミレーション)
もし、RubyがFloatへのビット演算を許可していたらどうなるか、シュミレーションしてみましょう。
1 : 左シフト(<< 1)の崩壊
1.5を左に1つシフトして3.0(2倍)にしたいと思います。
1.5のビット配列(32bit)は以下の通りです。
0 01111111 10000000000000000000000
これを全体的に左に1つズラす(<< 1)とこうなります。
0 11111111 00000000000000000000000
この新しいビット列をコンピュータが読み解くとどうなるでしょうか? 「指数部がすべて1(255)」で「仮数部がすべて0」というデータは、IEEE 754のルール上**「Infinity(無限大)」を意味します。 1.5を2倍にしたかっただけなのに、仮数のビットが指数の部屋に侵入(オーバーフロー)した結果、一瞬で宇宙の果て(無限大)まで飛んでいってしまいました。
2 : ビット論理積(&)の崩壊
10.5と5.5で&(AND)演算をしたとします。
それぞれのビット列を縦に並べて掛け合わせることになりますが、ここで大問題が発生します。
-
ゲタ(バイアス)同士の掛け合わせ
指数部には「本当の指数 + 127」というゲタを履いた数字が入っています。ゲタ込みの数字同士をビット演算しても、数学的な意味は全くありません。 -
一番大きな数字(省略された1)の無視
仮数部には、ケチ表現によって「一番大きな位である先頭の1.」が存在しません。存在しない数字を無視して、尻尾の細かい数字だけで計算することになります。
結果として生成されるのは、元の数字とは全く無関係な「無意味なゴミデータ(バグ数値)」です。
補足 : Rubyの本当の姿(64ビット)
今回は分かりやすさのために32ビットで解説しましたが、RubyのFloatクラスが実際に使っている「64ビット(倍数度浮動小数点)」の構造は以下のようになっています。
- S(符号部) : 1bit
- E(指数部) : 1bit (バイアス値は +1023 )
-
M (仮数部) : 52bit
箱のサイズ(ビット数)が大きくなり、ゲタの数字(バイアス)が変わっただけで「符号・指数・仮数に分ける」「ゲタをはかせる」「先頭に1を省略する」というIEEE 754の根本的なルールは32ビットと完全に同じです
仮数部が52ビットもあるおかげで、Rubyは非常に細かく正確な小数計算を行うことできます。
まとめ
RubyのFloatクラスでシフト演算やビット演算できない理由は以下の通りです。
- Floatのビット列は、純粋な数値ではなく「符号・指数・仮数」に分かれたデータ構造(パッケージ)であるため。
- IEEE 754特有の「バイアス(ゲタ)」や「ケチ表現(先頭の1省略)」が含まれており、ビット同士を直接計算しても数学的に破壊するため。
- 部屋の境界線を超えてビットをズラすと、全く意味不明な数値やinfinityに化けてしまうため。
計算結果にシフト演算を使いたい場合は、必ずto_iやroundなどを使ってIntegerに変換してから行うようにしましょう。
# 小数が混ざる場合は、整数に変換してからシフトする
result = (10 + 0.5).to_i << 1
#=> 20