36進数

昔、16進数知ってるオレ、すげー出来るヒト、と吹いてまわる、「読み書きそろばんが出来ないバカ」な同僚がいた。

それこそ皆があまりのバカさを「巷で話題」にするほどの常識では考えられないほどのバカだったのだが、本人は「皆がオレを出来るヤツだと噂してるんだぜ」と至ってポジティブで、「そんなオレを評価しない会社はオカシイ」といって退職していった。良かった良かった。

て話はどうでも良くて。

実は結構前に既に気付いて直したんだけど、このシリーズで作ってるヤツ(そして今もこっそり進化させ続けてるヤツ)、ID の表現を切り詰めるのに「radix≠10 進数」を今使っている。

この「radix≠10 進数」に関して、「運搬に支障を来たさないように」とか、「人間が読むのに誤解を生じさせないように」ということまで考え出すと、前者なら「base64 系の凝ったヤツ」、後者なら「手作りでもいいから「i、I、l、L、o、O など数字と混同しない進数」を検討することになる。前者は機械が混同することを嫌い、後者が人間が誤解することを嫌うわけだが、後者は一般には「人間が入力する」可能性がある場合にこれをする。それこそ「ユーザID」がそうね。(今は大抵は eメールで運搬するのでコピペで済むことが多いので意識しないのかもしれないが、紙媒体の印刷物を郵送してお知らせするような手続きがある場合、とりわけこれは重要。)

ただ、そこまで考えなくていい場合は、どの言語でも大抵は「parse integer as radix=n」「integer to string as radix=n」の相互変換が組み込みで備えられていて、「C言語ですら」標準ライブラリで簡単に(パースの方は)出来る。javascript の場合は toString の引数に radix を与えることが出来て、parseInt の第二引数に radix を与えることが出来る。

「javascript に不慣れなので」だったので、最初は穏健に「radix=16」から始めた。17以降はどういう振る舞いをするのか、仕様を調べるか試してみるしかわからないから。

で、ワタシの場合の適用対象は「人間が入力したりすることはない」なのでもう「16より大進数でええか」と思い、なおかつ「base64 はパディングの扱いのせいでそのままでは却って肥大する」のも面倒なので、「えいや、a-z まで全部使って 26 進数だ!」。


どういう計算?

なんだか一週間以上気付かなかったのよね。「a-z まで使い切ってやる」つもりなら 36進数。ワタシは「a-z なので 26」としてしまった。アホな子や…。

ちなみに javascript では(確かほかもそうだが) parseInt、toString に渡せる最大は 36。なので、「a-zA-Z 全て使い切ってやる」としたければ独自にやるしかない。これを自作するのは別に難しくはないが、ビルトインでないので性能はダメだろう。


頭で書いた「バカ」の話、ちょっとだけ戻る。

例にしたその「バカ」は、多分ワタシが身の回りで目撃した中でもワーストなバカだったので、今回の話に関してだけ言うと本質が埋もれてしまうんだけれど、そこまでのバカでなくても、「データとその表現」の区別が出来ていないエンジニアは結構多いので、一応注意しておきたい。

ほんとに全然話が通じなくて、説得出来なくて困ったんだけれど、この C の構造体:

1 struct MyStruct {
2     int x;
3     int y;
4 };

が「文字列でないので」という説明が通じないことがあって、滅茶苦茶困ったのよね。どうにも「ソフトウェアのプログラムでの見え方」と実際のデータ、そしてその表現、が全く区別出来ていないみたいで、つまり:

何か問題でも? てわけだ
1 MySctruct ms;
2 ms.x = 10;  // 「10ってわかるじゃん」
3 ms.y = 20;  // 「20って書けるじゃん」

例えばこの構造を「そのまま」運搬する場合にどうなるのか、全然理解出来ていないみたいで。これは一人じゃなかったよ、軒並み話が通じなかった。つまり javascript でこう書けるのとおんなじなんだから何が問題なんだてわけだ:

C と全然同じじゃん、このおいちゃんは何をバカなことを、てわけだ
1 var obj = {};
2 obj.x = "10";
3 obj.y = "20";

それこそ「16進数わかるオレ、カシコイ」にも通じるが、すなわち上の C の例を本当にそのままファイルに書き出して、hex dump してみればいいのである。要するに「人間が読めなくたって知らんよ、オレサマであるところのましーんが解読できればいいのだ」という形に「パックされている」ということが、多分常識的なエンジニアなら(それまで理解出来ていなかったとしても)すぐに理解出来る。(今の例の場合はすぐに「バイトオーダ」の問題に気付くし、もう一つは「アラインメント」の問題にも、「気付く人なら気付く」。)

実際にやってみる? 久しぶりに剥き身の C 書いたので fwrite のシグニチャを思い出せなくてなんと10分もかかってしまったが例えばこんな:

 1 #include <stdio.h>
 2 
 3 struct MyStruct
 4 {
 5   int x;
 6   int y;
 7 };
 8 
 9 int main()
10 {
11   struct MyStruct ms = {4096, 8192};
12   FILE* fp = fopen("dump.dat", "wb");
13   fwrite(&ms, sizeof(struct MyStruct), 1, fp);
14   fclose(fp);
15   return 0;
16 }

これで書き出した dump.dat は、例えば:

1 [me@host: ~]$ od -xa dump.dat
2 0000000 1000 0000 2000 0000
3         nul dle nul nul nul  sp nul nul
4 0000010

nul はわかるであろう。dle は 10進で10、つまりともにどちらもグラフィック文字ではない。そして wintel はリトルエンディアンなので、「0000 1000」ではなく「1000 0000」、さらに「32 bit 境界に揃えられている」。最初にご紹介のおバカさんほどの「そこまでのバカ」でないのに、「C 言語で書くのに「4096 というリテラルを書けているじゃん」から思考を抜け出せない人が結構多い、ということ。

すなわち、「データ」そのものと「その表現」は全くの別問題。運搬先によって、内部表現も外部表現も「全く異なる」。

これがちゃんと理解出来ていない人に「性能問題」を説明するのは非常に厄介。「性能問題」は CPU 効率、メモリ効率、外部記憶格納時の効率、最低この3つをバランスよく考えなければならないが、少なくとも「ソースコードに可読な形で 4096 と書けてますが何か?」で思考が止まってしまう人相手には、「文字列表現で運搬するとかさばる可能性がある」とか「数値のままの方がこの場合はかさばる」という説明全てが「通じない」。

具体的にこの問題を何をしようとしたときに一番実感したかというと、C++ なプロジェクトで「MessagePack を適用するとしたらどうなるか」の検証についてプレゼンしたとき。非常に困った、全然話通じなくて。