エンディアンについて

  • (by K, 2007.03.04)

(0) 予備知識

(1) エンディアン

  • たとえば 0x12345678 という32bitを8bit(=1バイト)ずつに分けて格納したい場合がある。このときに 12 34 56 78 という順序になるのがビッグエンディアンであって、 78 56 34 12 の順になるのがリトルエンディアンである。
  • 上記のwikipediaでは、ビッグエンディアンは人間の表記順序に近いというメリットが挙げられているし、リトルエンディアンは多倍長計算の際にアドレスを順方向にたどっていけるのがメリットであるとされている(加算、減算、乗算では確かにそのとおりである・・・人間の筆算でも下の桁から計算していきますよね)。
  • もしこれだけの観点しか存在しないのだとしたら、僕はリトルエンディアンのほうが良いと考える。なぜなら人間の慣習なんて合理的な理由ではないから。なんとなれば、人間のほうが数字を逆に書くようになればいいんだ、とさえ思う。

(2) 別の観点

  • ここでは以上の常識的な観点以外の視点から、エンディアンについて考えてみようと思う。
  • 16進数で書くと分かりにくくなるかもしれないので、以後は2進数で考えてみる。abcdefghijklmnop という16bitのビット列があったとしよう。これを8bitずつビッグエンディアンで書くと abcdefgh ijklmnop になる。逆にリトルエンディアンで書くと ijklmnop abcdefgh になる。実際に適当な2進数を当てはめてみれば分かると思う。
  • エンディアンというのは、何も8bitずつ区切ったときにだけ定義されるものではない。4bitずつに切り分ける場合でも適用されるはずだ。ビッグエンディアンなら abcd efgh ijkl mnop になるし、リトルエンディアンなら mnop ijkl efgh abcd になる。
  • これをもっと押し進めると、1bitずつに区切った場合というのも定義できると思う。ビッグエンディアンなら a b c ... n o p だし、リトルエンディアンなら p o n ... c b a だ。
  • したがって、 abcdefgh ijklmnop という2バイトを1bitのシリアル通信で送り出すとしたら、a b c d e f g h i j k l ... か h g f e d c b a p o n m ... という順序こそが自然だということになる。そしてもしあなたがかつての僕のようにリトルエンディアンこそ合理的ですばらしいと信じる人であれば、h g f e d ... という順序でビットストリームを考えるべきといえるだろう。ビットストリームでも数bitのヘッダをつける場合があるが、たとえば、1 1 0 というヘッダは、8bitデータ中では xxxxx011 となって現われるわけである。
    • かつての僕はリトルエンディアンが好きで何でもリトルエンディアンに統一しようとしていたのに、ビットストリーム内の 1 1 0 というヘッダが 110xxxxx としてバイト内に現れるようにプログラムしていたことがあった。つまり当時の僕は全然分かっていなかったということになる。

(3) リトルインディアンの美しさ

  • ある整数をリトルエンディアンによって1bitずつ区切って並べたビットストリーム 1 0 1 0 0 0 1 1 を考えよう。これは8bitにまとめると、11000101 である。10進数に直せばこれは197である。このビットストリームと数値197との関係は
    197 = 2^0 * 1 + 2^1 * 0 + 2^2 * 1 + 2^3 * 0 + 2^4 * 0 + 2^5 * 0 + 2^6 * 1 + 2^7 * 1
  • となる。これでは論点が分かりにくいかもしれないので、プログラムで表してみる。ビットストリームの各桁が bs[i] によって表現可能であるとすると、
    s = 0;
    for (i = 0; i < 8; i++)
        s += 2^i * bs[i];
  • という計算でビットストリームをデコードできるというわけである。
  • 一方、ビッグエンディアンで 11000101 をビットストリーム化すると、 1 1 0 0 0 1 0 1 になる。これをデコードするプログラムを考えれば、
    s = 0;
    for (i = 0; i < 8; i++)
        s += 2^(7-i) * bs[i];
  • ということになる。
  • エンコードされた数値をデコードするという作業はきわめて基本的なものであって、そのプログラムをリトルエンディアンならすっきり書けるというわけである。2のベキの指数部分の数値とバイトストリームのインデックスが同じだというのはシンプルでとても気持ちがいい。しかもビッグエンディアンのほうに現れた 2^(7-i) なんていう式は、全体が8bitだと分かっていなければ書けない式である。リトルエンディアンならそんな情報は要らなくて、ただビットストリームの終わりまで計算すればそれでOKなのだ。なんて美しいんだろう。
  • 自分はビットストリームなんて扱わない、バイト列で十分だ、というかもしれない。しかしそれでも、バイト列は256進法だとみなすことができるから、
    s = 0;
    for (i = 0; i < 4; i++)
        s += 256^i * bs[i]; /* ここではバイトストリーム */
  • と書ける。だからエンディアンの美しさはバイト列しか扱わなくても同じように現れる。
  • もちろん僕は以下のようなビッグエンディアン向きのデコード方法も知っている。
    s = 0;
    for (i = 0; i < 8; i++)
        s = s * 2 + bs[i];
  • しかしなんというか、これはプログラミングテクニック的な問題である気がする。しかもこれは必ず前からデコードしなければいけない。ビットストリームが乱れてbs[0]から順番には届かず、先にbs[4]がきて、次にbs[1]が来て、みたいなことがもしあっても、とりあえずbs[0]が到着するまではこの方法ではデコード計算を開始できない(ビットストリームが乱れても、それがビットストリームのどの部分であるかの情報は、いっしょになって到着するとする・・・乱れるかもしれない状況ではそうやってパケットを作るのが基本である)。先に示した 2^(7-i) 方式であれば、とりあえず届いたパケットから処理してsに加算していくことが可能である。そういう意味でより基本的なデコード方法であると僕は信じる。

  • リトルエンディアンによるビットストリームは、僕たちの数値表現とは逆方向なので読みにくい。でもそれは僕たちのほうが間違っているのではないかと思う。123という数値を"123"と書くが、なぜこれを"321"と書かないのか。「百・二十・三」と読まずに「三・二十・百」と読めばいいではないか。これなら、大きな数字を渡されても、読む前に「ええと、一・十・百・千・万・十万・百万・・・」みたいに桁を数える必要がない。気にせず左から読めばいいのだ。足し算をやる場合も「左から右へ」処理していくことができる。これは左から右へ文章を書くという方式と非常に合っている。
  • アラビア語は右から左へ書いていくそうだけど、それなら数値も"123"でいいだろう(アラビア語で数値を"123"と書いているのかそれとも"321"と書いているかは知らないけど)。というか123みたいな記法はアラビア数字とかいうから、もしかしたらアラビアでは「正しい」方向に書いていたのかもしれない。これがヨーロッパに伝わるときに、自分たちの記述方向に合わせて向きを反転しなかったのかもしれない・・・この推測が合っているのかどうかはわからないけど。

  • とまあ以上のような考えを僕は以前から持っていて、だからリトルエンディアン派だったのでした。

(4) ビッグエンディアンの合理性

  • そんな僕だったのですが、先日ビッグエンディアンもいいなと思うことがあって、それならいっそビッグエンディアンのほうが本質的だとこじつけることはできないだろうか、なんて空想をしてみました。初めは飽くまでもリトルエンディアン派が「かわいそうだからビッグエンディアンを慰める」みたいな感じだったのですが、だんだんビッグエンディアンこそ本質的かもしれないと思えるようになり、今ではすっかりビッグエンディアン派です。そのことを以下に書きます。

  • ヘッダのあるビットストリームをバイト列に格納して、このヘッダを判定したいとする。たとえば 0 x x x x ... か 1 0 0 x x x x ... か 1 0 1 x x x x ... か 1 1 0 x x x x ... か 1 1 1 x x x x ... かのどれであるかを判定したいとしよう。こんなとき、もちろんANDして比較するというのが普通の書き方だ。そういう書き方をするなら、リトルエンディアンではこうなる(bs[]はバイトストリームのことで、unsigned char)。
    if ((bs[0] & 1) == 0) { ... } /* ...xxxx0 */
    else if ((bs[0] & 7) == 1) { ... } /* ...xxxx001 */
    else if ((bs[0] & 7) == 5) { ... } /* ...xxxx101 */
    else if ((bs[0] & 7) == 3) { ... } /* ...xxxx011 */
    else { ... } /* ...xxxx111 */
  • これはこれで妥当なのだ。ビッグエンディアン用に書き直しても同じようにANDして比較するように書ける。しかしビッグエンディアンなら次のようなテクニックも使える。
    if (bs[0] < 0x80) { ... } /* 0xxxx... */
    else if (bs[0] < 0xa0) { ... } /* 100xxxx... */
    else if (bs[0] < 0xc0) { ... } /* 101xxxx... */
    else if (bs[0] < 0xe0) { ... } /* 110xxxx... */
    else { ... } /* 111xxxx... */
  • つまりANDがいらない。その分だけ判定が速くできるというわけだ。もちろんリトルエンディアンのほうだって、bs[0] & 7 の値をレジスタに保持しておけば一度しか計算しなくて済む。しかし一度もANDしないで済むビッグエンディアンの方法はもっと有利に見える。
  • こんなビット列判定なんて回路的には簡単だから、専用の命令をCPUに持たせればいいという考え方もできる。今までは(これからも?)ニーズがなかったからそのような命令をCPUがもっていないだけなのだと。しかしビッグエンディアンであれば、そのような専用命令を新設する必要はなく、普通の符号なし比較命令で代用できるのである。
  • もちろんこれは逆にもいえる。つまりリトルエンディアンではビットストリームにおけるフッタを比較命令だけで判定可能である。しかしそれは果たしてどれだけ実用的だろうか。やはりビットストリームは後ろから読む場合よりも前から読む必要のほうが断然に多いのではないだろうか。

  • 前から感じていたことに、日本と欧米の住所の書き方の違いがある。日本では県名・市名・町名・番地の順で書く。しかし欧米では番地・町名・市名・県名の順で書く。大きな単位から書くか、小さな単位から書くかの違いだと思う。年を含めた場合の日付の書き方も違う。日本では○年○月○日だけど、欧米では○月○日○年になっている。月と日を不可分のひとかたまりだと考えるなら、やっぱり欧米式は小さな単位から書いていることになる。日本式はビッグエンディアンで、欧米式はリトルエンディアンであるように僕には思える。
  • 欧米式の良いところは、たとえば世界にひとつしかない珍しい町名であれば、そして読み手がそれを知っていたら、その先を読まなくてもどこのことなのか分かるということだろう。分からなければ先を読みつづければいい。だからリトルエンディアンのほうがいい・・・といいたくなるが、ちょっと待て。そもそも町名だとか市名だとかを使うことそのものが無駄だと思う。郵便番号みたいに数値化するべきだ。そのほうがずっと短く書ける。いやそれをいうのならそもそも緯度や経度を使った座標のほうがもっと合理的だ。地名のないところでも表現できるし、二点間の距離も簡単に計算できる。地名ではそういうことはできない。
  • では座標の場合リトルエンディアンは有利なのか?座標の下の桁が分かったとしたら、それはとりあえず現在地からの近辺だと仮定することくらいしかできない。もしくはかつてのパターンから言って下3桁が578ってことはいつも郵便物の多い山田さんの家のことだろうな、とか。
  • 日付や時刻を言うときもリトルエンディアンは便利かもしれない。たとえば「帰ってくるのはいつですか」といわれて、「○年○月○日○時○分」なんていうのは大変だ。しかし、ビッグエンディアンであっても「今日の○時○分」みたいに言うことはできる。だから実はビッグエンディアンだといつも一番大きな単位から長々といわなければいけないというわけじゃない。それにビッグエンディアンでは、秒やミリ秒など細かい時刻に興味がなければ、最後まで聞かない(読まない)ことも可能である。リトルエンディアンでは一番小さい単位から言うから面倒でも聞き流さなければいけない。

  • ビッグエンディアンの場合、途中までしか読んでいない状態でも、とりあえず行動するのがやりやすいというメリットがあると思う。たとえばディスクの○○セクタを読んでくれ、なんていう命令を出す場合、○○の部分がビックエンディアンであれば、上の桁から言ってくれるので、最後まで分からなくてもとりあえずシークをはじめることだってできると思う(見切り発車みたいなこと)。リトルエンディアンだとこれはできない。いやしたの桁だけ見てそれまでの推測で適当に動き始めることはできはなくはないが、それは外れるかもしれない。そして外れだったらかえって時間がかかることになりかねない。それにビッグエンディアンでも初めの数桁だけで予測を立てることはできるから、予測することがリトルエンディアンの長所というわけでもない(まあ予測用のハッシュとしては下の桁のほうがうまくバラけて有用であるとは思うけど)。

  • ようするにビッグエンディアンのほうが利用しやすいエンコード方法なのではないかと思うのだ。

(5) ビッグエンディアンのほうが美しい

  • 結局リトルエンディアンのメリットは、 s += 2^i * bs[i]; が数学的に美しいということだけになってしまったように思う。多倍長計算の際に bs[0] から処理できるというのは確かにメリットだが、実はこれは加減算と乗算に限定されるわけで、たとえば除算であればリトルエンディアンの場合 bs[n-1] から処理していかなければならない。つまり多倍長計算で有利か不利かというのは演算の処理内容に依存するのである。だから僕はこれを長所にはカウントしない。
  • ビッグエンディアンのほうが良いと思うようになってしまった以上、ビッグエンディアンこそ数学的に美しくあってほしい。そう願えば美しくなるというわけではないのだが、数学っていうのは正しくて有用なものほど美しいことが多いから、そう思ってもっとよく考えてみた。
  • s += 2^(7-i) * bs[i]; のどこが汚いのかといえば、もちろん7の部分だ。ということでこの7をなくしてみた。 s += 2^(-i) * bs[i]; 。もちろんこんな式にすれば値は2^7倍狂ってしまう。しかしこれを見て思う、「なるほど、sが小数ならいいのか」。
  • 今までレジスタやメモリの中身は0以上の整数(かもしくは符号付き整数)が入っているということになっていたが、これを0以上1未満の小数が入っていると考えたらどうだろうか。今までだと「8bitでは0〜255が扱える、16bitなら0〜65535が扱える」であり、bit数が増えることは「より大きな数値が扱える」ということを意味していた。しかしそうではなく、「8bitでも16bitでも32bitでも4bitでも1bitでも、扱える数値は0以上1未満、違うのは精度だけ。bit数が上がると精度が良くなる。たとえば1bitの場合、0.0と0.5の2^(-1)刻み。4bitなら、2^(-4)刻みで0.0000〜0.9375。」と考えるのである。この考えを受け入れれば、 s += 2^(-i) * bs[i]; と書ける。これを s += (1/2)^i * bs[i]; と書いてもいいかもしれない。つまりリトルエンディアンは2進法、ビッグエンディアンは(1/2)進法なのである。
  • なるほど1未満の小数なら、日常使う数値に関しても左から読めている。一・十・百・千・万・・・などとやることはない。1未満の小数ではなくもっと大きい数字を書きたいのであれば、適当な指数を乗じればいいだろう。 0.625*2^5 のように。しかしビッグエンディアンのルールに徹するなら大きい単位は前に書くべきなので、 2^5 * 0.625 のほうがよりよいだろう。(1/2)進法であることを意識するなら、(1/2)^(-5) * 0.625 のほうがさらにいいかもしれない。-5乗というのは、小数点を負の方向に5つずらすという意味になる。負の方向というのは、数字を書いていく向きと反対ということになるので、左へ5つずらすということだ。
  • さまざまな量を整数で表そうという考え方は、これ以上小さくできない最小の単位みたいなものがあって(原子とか)、それの整数倍で表そうという考え方につながっていると思う。bit数が無制限なら基本的に上限みたいなものはない。bit数を増やせばどこまでもいける。これがリトルエンディアンの精神なんだと思う。逆にビッグエンディアンでは、まず限界というか上限みたいなものがあって、それにどれほど近づいているかの割合で表現しようというものだと思う。bit数が無制限なら基本的に最小単位みたいなものはなくて、bit数を増やせば精度はどこまでも上げられる。
  • 今の僕の感覚だと数値の扱いとしては、ビッグエンディアンでの0以上1未満で大きな単位からという考え方のほうが美しく感じる。

(6) 余談

  • 精度を意識して数値を表現する場合、0.12345e+6と書いても1.2345e+5と書いても同じだが、人間社会での一般では1.2345e+5の表記のほうをよく見かける。つまり仮数部は1以上10未満となる。しかし先の2進数によるビッグエンディアンの考え方では仮数部は1/2以上1未満となる。これは10進数で対応させると0.1以上1未満だ。ここでは、実は0.12345e+6という風な書き方のほうが合理的であるということを示したい。
  • 仮に今ある数値を扱うとして、指数部だけしか覚えていなくて仮数部をすっかり忘れてしまったとしよう。これはある種の近似でもある。仮数部がいくつか分からない以上、とりあえず1〜10の間を取って5だとしよう。ということで 5e+2 * 5e-1 みたいな計算をすると、答えは 2.5e+2 となる。いっぽうこれは、 0.5e+3 * 0.5e0 = 0.25e+3 とも書ける。
  • ここで指数部に注目してほしい。仮数部が0.1以上1未満になるように調節して書いた式では、指数部が単純に和になっている。これはよいことだ。・・・どうよいのかもっとわかりやすくしよう。もし仮数部の計算を省略し指数部だけでやって仮数部を5または0.5に近似してしまえば、 5e+2 * 5e-1 = 5e+1 と 0.5e+3 * 0.5e0 = 0.5e+3 ということになる。どちらが良い近似だろう。 5e+1 だと 2.5e+2 と比べて5倍も狂っているのに対し、 0.5e+3 なら 0.25e+3 と比べて2倍しか狂っていない。したがって、仮数部をあえて0.1以上1未満にするのは近似計算時の精度が良くなりやすいのである。
  • この議論には文句があるかもしれない。つまり、仮数部を5に仮定したところである。今は乗算を例に取っているのだから、相加平均の5ではなく、相乗平均の3を使うべきではないかと。なるほど、それはそうかもしれない。ということで3を使ってみる。3をそのまま使うとまだ不満を持つ人がいるかもしれないので、ここではもう少し正確に3.162を使おう。
  • そうするとそれぞれ 3.162e+2 * 3.162e-1 = 1.000e+2、 0.3162e+3 * 0.3162e0 = 0.1e+3 となる。これらを 3.162e+1 や 0.3162e+3 と比較すると、狂いはどちらも 3.162倍になる。だからこの方法でなら優劣はない。

こめんと欄

  • ビットオーダーの話とバイトオーダーの話がゴッチャになってる様な気がします。 -- 通りすがり 2016-12-05 (月) 07:00:27

コメントお名前NameLink

リロード   新規 編集 差分 添付   トップ 一覧 検索 最終更新 バックアップ   ヘルプ   最終更新のRSS
Last-modified: 2016-12-05 (月) 07:00:27 (933d)