小数点を含む数の他に、値の大きな数も整数として取り扱えない場合があります。浮動小数点数という方法を使います。一般的に使われる 6.02×1023 のような指数部分がある表現方法です。6.02の部分を仮数、10を基数、23を指数と呼びます。
浮動小数点数の規格で一番一般的なものは IEEE 754 と呼ばれるものです。このページでは、単精度と呼ばれる32ビットを使う規格を例に仕組みを見ていきます。符号に1ビット、指数に8ビット、仮数に23ビットを割り当てています。
各ビットをクリック操作で変更すると、符号、指数、仮数の値が変わり、最終的に「計算結果」のところに、10進表現で表示されます。
| 符号 | 指数部(8ビット) | 仮数部(23ビット) | |||||||||||||||||||||||||||||
| ± | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 2-1 | 2-2 | 2-3 | 2-4 | 2-5 | 2-6 | 2-7 | 2-8 | 2-9 | 2-10 | 2-11 | 2-12 | 2-13 | 2-14 | 2-15 | 2-16 | 2-17 | 2-18 | 2-19 | 2-20 | 2-21 | 2-22 | 2-23 |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| ± | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | 1 2 | 1 4 |
1 8 | 1 16 |
1 32 | 1 64 |
1 128 | 1 256 |
1 512 | 1 1024 |
1 2048 | 1 4096 |
1 8192 | 1 16384 |
1 32768 | 1 65536 |
1 131072 | 1 262144 |
1 524288 | 1 1048576 |
1 2097152 | 1 4194304 |
1 8388608 |
| 指数部入力値(10進) | 0 | 仮数部入力値(10進) | 0 | ||||||||||||||||||||||||||||
| 状態 | 0 | ||||
| 符号 | 仮数部換算値 | 指数部換算値 | 計算結果 | ||
| + | 0 | ×2 | 0 | = | 0 |
| + | 累乗を使わず、×2,÷2で計算 | = | 0 | ||
状態:ほぼ指数部の値で決まります。0なら「ゼロ」か「非正規化数」、1〜254なら「正規化数」、255なら「無限大」または「NaN」です。NaNは Not a Number で、正しく計算できなかったときに返す値として定められています。
指数部は1〜254の時、127を引いて、-126〜127と換算します。
仮数部は、1.で始まる数になるよう指数部を調整して(これが正規化)1.を省いて23ビットに並べます。仮数部換算値は省いた1を戻したものです。最初の1を省くことで23ビットで24ビット分の精度を確保できます。
非正規化数の場合は特別な扱いになります。精度を犠牲にして小さな値をできる限り表現する手法です。
自由に試すのでいいのですが、いくつか簡単な例をあげます。
| 指数部(8ビット) | 仮数部(23ビット) | 計算結果 | 備考 |
|---|---|---|---|
| 0111 1111 | 全部0 | 1 | 仮数部には一番左に見えない1があります |
| 0111 1111 | 一番左にひとつだけ1 | 1.5 | 固定小数点でやったように右に行くにつれて1/2になります。 |
| 1000 0000 | 一番左にひとつだけ1 | 3 | 1.5の2倍です。 |
| 0111 1100 | 100 1100 1100 1100 1100 1100 | 0.19999998807907104 | 0.2は循環小数になります。 |
| 0111 1100 | 100 1100 1100 1100 1100 1101 | 0.20000000298023224 | ぴったり0.2にはできません。 |
| 1001 0110 | 全部0 | 8388608 | 223です。 |
| 1001 0110 | 一番右にひとつだけ1 | 8388609 | 223+1です。ちょうど+1ですが+0.5はできません |
| 1001 1111 | 全部0 | 4294967296 | 232です。 |
| 1001 1111 | 一番右にひとつだけ1 | 4294967808 | 232+512です。+1にはできません。 |
ビットの扱いは正確に表現できていますが、10進表現に直すときの計算結果は細かな問題があります。
一番右のビットだけ1にすると仮数部入力値は 1.1920928955078125e-7 となりますが、これは 1.1920928955078125×10-7 つまり、0.00000011920928955078125 のことです。仮数なのに e 表示なのはどうかと思いますが、javascriptの仕様で 0.00... の表記を指示できないので放置しています。
もっと問題なのが精度です。一番右のビットだけ1にしたときの計算結果は 1.401298464324817e-45 と表示されますが、この値は2進法で有効数字1桁です。10進法ではそれ以下になりますから、せいぜい 1×10-45 程度しか正確でなく、後ろの .401298464324817 はまったく当てにならないということです。
1 8388608
上の例は非正規化数で、特別な数ですが、普通の値である正規化数でも精度の問題はあります。不正確だと言う意味ではなく、実際よりも正確に見えるように表示されているということです。一番右のビットに加えて左から2番目も1にしてください。これで計算結果は 2.000000238418579 と表示されます。16桁の値ですが、単精度の正確さは7桁程度とされていますから、実際は 2.000000 ぐらい(つまり小数点以下6桁目は確実にゼロですよ)程度の正確さということです。
これらの原因は計算がjavascriptの計算機能を使っていること。そしてjavascriptは倍精度(64ビット)で計算していて10進法換算で16桁程度の精度があります。「計算結果」欄への表示が最大で16桁なのはそのためです。
高校の科目「情報の科学」では浮動小数点数の扱いはせいぜい参考資料程度に出てくる程度で、仕組みに触れることはありません。確かに面倒なことはあるのですが、理解できなくても触れておくことが大切ということもあるのではないかというのが私の考えです。辞書にだって必要のない言葉がたくさん載っているけれども、それを目にすることは大切だし、学童用の辞書の他に大人の使う辞書にも触れておくことが大切だと思います。
解説を随分書いてしまっていますが、操作して結果を眺めるだけでも構わないと思います。
最も大きい数値を出してみるとか、最も小さな数値を出してみるとかといったところから、表現できる範囲がどのようにして決まるのかをさぐったり、12や2.5などの普通の数値を出してみるなどして、指数が1増えると2倍になることを納得したりするのも良いでしょう。
また、扱える数が不連続であること、数の大きさにより隣の数との差も変化することを実感することも大切だと思います。
浮動小数点数にはいろいろな工夫が詰まっています。2進法の仮数は最初の数字がいつも1なのでこれを略することで精度を1桁増やすことができること、指数は正負の数が必要ですが、補数を使わずに127(全ビット1)を足しておく方法を使い仮数の前に配置することで数の大小を比較するのを簡単にしていること、無限大やNaNなど数値以外の取り決めがあることもコンピュータへの理解が深まると思われます。
初期化の部分です。基本的には整数や固定小数でやっているのと同じですが、符号部・指数部・仮数部で背景色を変えたことと、換算値や計算結果を格納するセルをグローバル変数にオブジェクトとして格納する部分が増えています。また、最後に一度計算をして換算値や計算結果を初期化します。
window.onload = function() {
tinit();
}
var tbtns,tflps,texpsum,tfrcsum;
var tsig1,tsig2,texpval,tfrcval,tf2eval;
var flpcolor = new Array("#ccc","#333");
var expmax=8;
function tinit() {
var pwsrow = document.getElementById("tpw");
var tpws = pwsrow.getElementsByTagName("td");
var btnsrow = document.getElementById("tbtn");
tbtns = btnsrow.getElementsByTagName("td");
var flpsrow = document.getElementById("tflp");
tflps = flpsrow.getElementsByTagName("td");
for(var i=0; tbtns.length>i; i++){
tbtns[i].setAttribute("onclick","tflip(this)");
if(i==0){
tpws[i].setAttribute("style","background-color:#eff;");
tbtns[i].setAttribute("style","background-color:#eff;");
tflps[i].setAttribute("style","background-color:#eff;");
}else if(expmax>=i){
tpws[i].setAttribute("style","background-color:#fef;");
tbtns[i].setAttribute("style","background-color:#fef;");
tflps[i].setAttribute("style","background-color:#fef;");
}else{
tpws[i].setAttribute("style","background-color:#efe;");
tbtns[i].setAttribute("style","background-color:#efe;");
tflps[i].setAttribute("style","background-color:#efe;");
}
}
tbtns[8].style.background="#fef url(images/dotL.png) bottom right no-repeat";
tbtns[9].style.background="#efe url(images/dotR.png) bottom left no-repeat";
texpsum = document.getElementById("texpsum");
tfrcsum = document.getElementById("tfrcsum");
tstat = document.getElementById("status");
tsig1 = document.getElementById("tsig1");
tsig2 = document.getElementById("tsig2");
texpval = document.getElementById("texpval");
tfrcval = document.getElementById("tfrcval");
tf2eval = document.getElementById("tf2eval");
tcalc();
}
各ビットをクリックしたときに、0/1を反転させる関数と、指数部・仮数部をまとめて0/1で埋める関数です。
全部まとめて1で埋めたいということがよくあったので、追加しました。
引数のtgtはクリックされた場所がthisで渡されてきます。flgは0/1のどちらかが渡されます。
どちらの場合も最後に計算をして換算値や計算結果をやり直します。
function tflip(tgt){
//alert(tgt); //test
tgt.firstChild.nodeValue = 1-tgt.firstChild.nodeValue;
tcalc();
}
function setexpall(flg){
for(var i=1; expmax>=i; i++){
tbtns[i].firstChild.nodeValue = flg;
}
tcalc();
}
function setfrcall(flg){
for(var i=expmax+1; tbtns.length>i; i++){
tbtns[i].firstChild.nodeValue = flg;
}
tcalc();
}
最後は計算です。前半はビットを読み出して、符号部は+-を、指数部は127加えられた値を、仮数部は最初の1が省かれた値をそれぞれその場所に表示します。仮数部で、frcsumとxfrcsumの2つの変数を使っているのは、あとで1を足す代わりにxfrcsumの方には最初から1があるものとして計算するという工夫です。
8388608は223のことです。
function tcalc(){
//符号
var cpn = "-";
if (tbtns[0].firstChild.nodeValue==0) cpn="+";
tflps[0].firstChild.nodeValue = cpn;
tsig1.firstChild.nodeValue = cpn;
tsig2.firstChild.nodeValue = cpn;
//指数
var expsum = 0;
for(var i=1; expmax>=i; i++){
expsum *=2;
var bit = Number(tbtns[i].firstChild.nodeValue);
expsum += bit;
tflps[i].style.color=flpcolor[bit];
}
texpsum.firstChild.nodeValue = expsum;
//仮数
var frcsum = 0;
var xfrcsum = 1;
for(var i=expmax+1; tbtns.length>i; i++){
frcsum *=2;
xfrcsum *=2;
var bit = Number(tbtns[i].firstChild.nodeValue);
frcsum += bit;
xfrcsum += bit;
tflps[i].style.color=flpcolor[bit];
}
tfrcsum.firstChild.nodeValue = frcsum/8388608;
//換算・計算
if(expsum==0){
if(frcsum==0){
tstat.firstChild.nodeValue = "ゼロ";
texpval.firstChild.nodeValue = "特殊値(0)";
var zerostr = "0";
tfrcval.firstChild.nodeValue = zerostr
if(cpn=="-") zerostr = "-0";
tf2eval.firstChild.nodeValue = zerostr;
document.getElementById("xdecimalv").firstChild.nodeValue = zerostr;
}else {
//非正規化数 subnormal number
tstat.firstChild.nodeValue = "非正規化数";
texpval.firstChild.nodeValue = -126;
tfrcval.firstChild.nodeValue = frcsum/8388608;
//by pow of 2
var dval = frcsum/8388608 * Math.pow(2,-126);
if(cpn=="-") dval *= -1;
tf2eval.firstChild.nodeValue = dval;
//by *2, /2
var xexp = -126-23;
for(var i=xexp;0>i;i++) frcsum /= 2;
if(cpn=="-") frcsum *= -1;
document.getElementById("xdecimalv").firstChild.nodeValue = frcsum;
}
}else if(expsum==255){
texpval.firstChild.nodeValue = "特殊値(255)";
if(frcsum==0){
var infstr = "無限大";
tstat.firstChild.nodeValue = infstr;
tfrcval.firstChild.nodeValue = infstr
if(cpn=="-") infstr = "-無限大";
tf2eval.firstChild.nodeValue = infstr;
document.getElementById("xdecimalv").firstChild.nodeValue = infstr;
}else{
tstat.firstChild.nodeValue = "NaN";
tfrcval.firstChild.nodeValue = "NaN";
tf2eval.firstChild.nodeValue = "NaN";
document.getElementById("xdecimalv").firstChild.nodeValue = "NaN";
}
}else{
//正規化数
tstat.firstChild.nodeValue = "正規化数";
//by pow of 2
var dfrc = xfrcsum/8388608;
var dexp = expsum-127;
texpval.firstChild.nodeValue = dexp;
tfrcval.firstChild.nodeValue = dfrc;
var dval = dfrc * Math.pow(2,dexp);
if(cpn=="-") dval *= -1;
document.getElementById("tf2eval").firstChild.nodeValue = dval;
//by *2, /2
//document.getElementById("test").firstChild.nodeValue = xfrcsum;
var xexp = expsum-127-23;
if (xexp>0) for(var i=0;xexp>i;i++) xfrcsum *= 2;
else for(var i=xexp;0>i;i++) xfrcsum /= 2;
if(cpn=="-") xfrcsum *= -1;
document.getElementById("xdecimalv").firstChild.nodeValue = xfrcsum;
}
}
後半は、指数部が0,255,それ以外に場合分けして、それぞれ計算を進めます。
//by pow of 2 という部分は 仮数×2指数で、Math.pow(2,指数)のメソッドを使う方法。//by *2, /2 と言う部分は、仮数に指数分2をかけたり割ったりして値を出す方法です。精度などに問題が出ないか心配で両方で比較するようにしましたが、javascriptでは倍精度で計算するので異なる値がでるのを見たことはありません。
<div class="table"> <table class="b32"> <tbody> <tr> <td class="k4 sig">符号</td> <td colspan=8 class="exp">指数部(8ビット)</td><td colspan=23 class="frc">仮数部(23ビット)</td> </tr> <tr id="tpw"> <td>±</td><td>2<sup>7</sup></td><td>2<sup>6</sup></td> <td>2<sup>5</sup></td><td>2<sup>4</sup></td> <td>2<sup>3</sup></td><td>2<sup>2</sup></td> <td>2<sup>1</sup></td><td>2<sup>0</sup></td> <td>2<sup>-1</sup></td><td>2<sup>-2</sup></td> <td>2<sup>-3</sup></td><td>2<sup>-4</sup></td> <td>2<sup>-5</sup></td><td>2<sup>-6</sup></td> <td>2<sup>-7</sup></td><td>2<sup>-8</sup></td> <td>2<sup>-9</sup></td><td class="k3">2<sup>-10</sup></td> <td class="k3">2<sup>-11</sup></td><td class="k3">2<sup>-12</sup></td> <td class="k3">2<sup>-13</sup></td><td class="k3">2<sup>-14</sup></td> <td class="k3">2<sup>-15</sup></td><td class="k3">2<sup>-16</sup></td> <td class="k3">2<sup>-17</sup></td><td class="k3">2<sup>-18</sup></td> <td class="k3">2<sup>-19</sup></td><td class="k3">2<sup>-20</sup></td> <td class="k3">2<sup>-21</sup></td><td class="k3">2<sup>-22</sup></td> <td class="k3">2<sup>-23</sup></td> </tr> <tr id="tbtn"> <td>0</td> <td>0</td><td>0</td> <td>0</td><td>0</td> <td>0</td><td>0</td> <td>0</td><td>0</td> <td>0</td><td>0</td><td>0</td><td>0</td> <td>0</td><td>0</td><td>0</td><td>0</td> <td>0</td><td>0</td><td>0</td><td>0</td> <td>0</td><td>0</td><td>0</td><td>0</td> <td>0</td><td>0</td><td>0</td><td>0</td> <td>0</td><td>0</td><td>0</td></tr> <tr id="tflp"> <td>±</td> <td class="k3">128</td><td>64</td> <td>32</td><td>16</td> <td>8</td><td>4</td> <td>2</td><td>1</td> <td>1<br><span>2</span></td><td>1<br><span>4</span></td> <td>1<br><span>8</span></td><td>1<br><span>16</span></td> <td>1<br><span>32</span></td><td>1<br><span>64</span></td> <td class="k3">1<br><span>128</span></td><td class="k3">1<br><span>256</span></td> <td class="k3">1<br><span>512</span></td><td class="k4">1<br><span>1024</span></td> <td class="k4">1<br><span>2048</span></td><td class="k4">1<br><span>4096</span></td> <td class="k4">1<br><span>8192</span></td><td class="k5">1<br><span>16384</span></td> <td class="k5">1<br><span>32768</span></td><td class="k5">1<br><span>65536</span></td> <td class="k5">1<br><span>131072</span></td><td class="k5">1<br><span>262144</span></td> <td class="k5">1<br><span>524288</span></td><td class="k6">1<br><span>1048576</span></td> <td class="k6">1<br><span>2097152</span></td><td class="k6">1<br><span>4194304</span></td> <td class="k6">1<br><span>8388608</span></td> </tr> <tr class="sum"> <td class="sig"></td> <td colspan="5" class="exp">指数部入力値(10進)</td> <td colspan="3" class="exp" id="texpsum">0</td> <td colspan="5" class="frc">仮数部入力値(10進)</td> <td colspan="18" class="frc" id="tfrcsum">0</td> </tr> <tr class="sum"> <td class="sig"></td> <td colspan="8" class="exp"> <button type="button" onclick="setexpall(1)">指数部全1</button> <button type="button" onclick="setexpall(0)">指数部全0</button></td> <td colspan="23" class="frc"> <button type="button" onclick="setfrcall(1)">仮数部全1</button> <button type="button" onclick="setfrcall(0)">仮数部全0</button></td> </tr> </tbody></table> </div> <div class="table"> <table class="todec"> <tbody> <tr><td>状態</td><td id="status" colspan=5>0</td></tr> <tr> <td class="k4 sig">符号</td> <td class="frc">仮数部換算値</td> <td></td> <td class="exp">指数部換算値</td> <td></td> <td>計算結果</td> </tr> <tr> <td class="k4 sig" id="tsig1">+</td> <td class="frc" id="tfrcval">0</td> <td>×2</td> <td class="exp"><sup id="texpval">0</sup></td> <td>=</td> <td id="tf2eval">0</td> </tr> <td class="k4 sig" id="tsig2">+</td> <td id="test" colspan=3>累乗を使わず、×2,÷2で計算</td> <td>=</td> <td id="xdecimalv">0</td> </tr> </tbody></table> </div>
.table { overflow: auto; }
#tflp td{
padding:2px 0;
line-height:120%;
text-align:center;
}
#tflp span{
text-decoration:overline;
padding:0 4px;
}
.b32 {
margin:1%;
border:solid 1px #ccc;
min-width:48em;
}
.b32 td{
text-align:center;
border:none;
padding:2px 0;
}
#tbtn td{
font-size:150%;
width:1.5em;
height:2em;
border:solid 1px #666;
padding:2px 0;
cursor:pointer;
color:#00f;
}
.k6{
font-size:50%;
}
.k5{
font-size:60%;
}
.k4{
font-size:75%;
}
.k3{
font-size:85%;
}
.b32 tr:first-child,
.sum td{
border-top:solid 1px #ccc;
}
.b32 td:first-child{
border-left:solid 1px #ccc;
}
.todec {
margin:1%;
border:none;
}
.sig{background-color:#eff;text-align:center;}
.exp{background-color:#fef;}
.frc{background-color:#efe;}
#tf2eval,#xdecimalv{
background-color:#ffc;
font-size:150%;
}