Quantcast
Channel: プログラムモグモグ
Viewing all 233 articles
Browse latest View live

音量を調節できるCLIツールをGo言語で作りました!

$
0
0

volumeコマンドを作りました。

音量の調整ってコマンドからどうやるんだろう、ポータブルな形でコマンドラインツールがあれば便利なのでは… と思ったので作りました。 macOSUbuntuで動作確認をしています。

インストー

go get -u github.com/itchyny/volume-go/cmd/volume

getは今の音量を返します。set (0-100) で音量を設定できます。

 $ volume get
25
 $ volume set 20
 $ volume get
20

up, down, mute, unmuteなど、直感的に使えるサブコマンドを揃えています。

 $ volume down
 $ volume status
volume: 14
muted: false
 $ volume up
 $ volume status
volume: 20
muted: false
 $ volume mute
 $ volume status
volume: 20
muted: true
 $ volume unmute
 $ volume status
volume: 20
muted: false

簡単!OS起動時に音量を下げたり、アプリケーションを切り替えるたびに音量を調節したりするなどのオートメーションに使えて便利ですね。おわり!


ptraceシステムコール入門 ― プロセスの出力を覗き見してみよう!

$
0
0

他のプロセスを中断せずに、その出力をミラーリングして新しくパイプで繋ぐ、そんなことはできるのでしょうか。 straceやgdbといったコマンドは一体どういう仕組みで動いているのでしょうか。 ptraceシステムコールを使い、プロセスが呼ぶシステムコールを調べて出力を覗き見するコマンドを実装してみたいと思います。

ptraceシステムコール

Linuxを触っていると、いかにプロセスを組み合わせるか、組み合わせる方法をどれだけ知っているかが重要になってきます。 パイプやリダイレクトを使ってプロセスの出力結果を制御したり、コードの中からコマンドを実行して、終了ステータスを取得したりします。 プロセスツリーやプロセスグループを理解し、シグナルやnohupコマンドを使ったりします。

プロセスの扱いに慣れると疑問に持つのがstracegdbの仕組みです。 プロセスの実行しているシステムコールを出力したり、メモリーを書き換えたりできるこれらのコマンドは、まるで魔法みたいです。 一体全体どんな仕組みで動いているのでしょうか。 誰に許しを得て他のプロセスのメモリーにアクセスしたりしているのでしょうか。 これらのコマンドは、標準出力を読んだりプロセスにシグナルを送ったりするのとは全く別の次元のことをやっているかのようです。

ptraceシステムコールは、straceやgdbのようなコマンドの実装の中で核となるシステムコールです。 このシステムコールを使うと、実行中の他のプロセスの動作を覗き見したり、メモリーを書き換えたりすることができます。 魔法を使っているんじゃなかったんですね。 ちょっぴり残念な気分です。

procoutコマンド

使っているシステムコールがわかれば、それを使って何かを作りたくなってきます。 ptraceシステムコールを使うと、原理上はstraceやgdbを自前で実装することができます。 しかし、最初から複雑なデバッガを目指そうとすると大変です。

私はまず手始めに、procoutというコマンドを作ってみました。

中断して途中から再開したりできないプロセスの出力が端末に流れている時に、後からgrepしたりteeしたりしたくなることがあると思います。 そんな時にプロセス自体を中断せず、またコードの変更も行わずに、別のターミナルからプロセスの出力を流すことができるコマンドです。 プロセスの出力を覗き見るという感じなので、procoutと名付けてみました。

次のように、引数にpidを渡して実行します。

 $ sudo procout [pid]

そうすると、対象となるプロセスにアタッチし、コマンドの標準出力をそのまま真似して出力してくれます。 エディタのプロセスに対して使うと、まるで端末がミラーリングされているかのような挙動になります。

f:id:itchyny:20170731003438g:plain

procoutコマンドは、コマンド自体の便利さやおもしろさよりも、それ自体を実装することに意義があります。 ptraceシステムコールの基礎に触れることができるからです。 ptraceの最初の練習問題として、そしてLinuxシステムコールがどのように呼ばれているかについて理解するために、このエントリーではprocoutコマンドを一緒に作っていきたいとおもいます。

免責: 私は一週間前にptraceシステムコールについて学びはじめた素人です。素人だからこそ、わからないところを一つずつ潰しながらこの記事を書きました。もし誤っている記述があれば、お気軽にコメントいただければと思います。

この記事のコード及び上記procoutコマンドは、以下の環境で動作確認をしています。macOSでは動きませんが、仮想マシン上でも簡単に試せますので、是非挑戦していただけたらと思います。

vagrant@vagrant-ubuntu-trusty-64:~/$ uname -srmo
Linux 3.13.0-125-generic x86_64 GNU/Linux

プロセスにアタッチしてみよう

ptraceシステムコールは、引数によって様々なことを行うことができます。 man 2 ptraceでマニュアルを引いてみましょう。

NAME
       ptrace - process trace

SYNOPSIS
       #include <sys/ptrace.h>

       long ptrace(enum __ptrace_request request, pid_t pid,
                   void *addr, void *data);

第一引数にはプロセスに対してどういうptraceリクエストを行うか、第二引数にはプロセスのpidを指定します。 リクエストの内容によって、第三・四引数の意味は変わってくるので、これらについてはおいおい見ていきましょう。 他にはsys/ptrace.hをincludeすること、返り値がlongであることがわかりました。

まずは、ptraceの基本であるアタッチ・デタッチから始めましょう。

#include <stdio.h>#include <stdlib.h>#include <sys/ptrace.h>int main(int argc, char *argv[])
{
  long ret;
  if (argc < 2) {
    fprintf(stderr, "specify pid\n");
    exit(1);
  }

  pid_t pid = atoi(argv[1]);
  printf("attach to %d\n", pid);

  ret = ptrace(PTRACE_ATTACH, pid, NULL, NULL);
  if (ret < 0) {
    perror("failed to attach");
    exit(1);
  }
  printf("attached to %d (ret: %ld)\n", pid, ret);
  sleep(5);

  ret = ptrace(PTRACE_DETACH, pid, NULL, NULL);
  if (ret < 0) {
    perror("failed to detach");
    exit(1);
  }
  printf("detached from %d (ret: %ld)\n", pid, ret);

  return0;
}

topコマンドを実行し、そのプロセスに対してアタッチしてみましょう。

f:id:itchyny:20170730182701g:plain

あら、アタッチできませんでした。 Operation not permittedと表示されていることから想像がつきますが、ptraceで他のプロセスにアタッチするには、root権限が必要です。 そうですよね、一般ユーザーで他のプロセスを自由に操作できたら怖いですよね。

f:id:itchyny:20170730182642g:plain

うまく動きました。 プロセスにアタッチしてsleepしている間、左側のtopコマンドが停止しているのがわかります。

なぜtopコマンドは止まってしまったのでしょうか。 man 2 ptraceでは次のように説明されています。

While being traced, the tracee will stop each time a signal is delivered, even if the signal is being ignored. (An exception is SIGKILL, which has its usual effect.) The tracer will be notified at its next call to waitpid(2) (or one of the related “wait” system calls); that call will return a status value containing information that indicates the cause of the stop in the tracee.

tracerがptraceするプロセスで、traceeがptraceされるプロセスです (employerとemployeeと同じ)。 ptraceされるプロセスはシグナル毎にいちいち止まるから、ptraceするプロセスはwaitpidを使ってねと書かれています。

straceのようなptraceの典型的な用途では、システムコールが呼ばれるところで処理を行います。 PTRACE_SYSCALLを使ってプロセスを再開すると、次のシステムコールでプロセスが停止し、ptraceするプロセスはwaitpidを使ってその停止を検知することができます。

#include <stdio.h>#include <stdlib.h>#include <sys/ptrace.h>#include <sys/wait.h>#include <sys/user.h>#include <sys/syscall.h>int main(int argc, char *argv[])
{
  int status;

  if (argc < 2) {
    fprintf(stderr, "specify pid\n");
    exit(1);
  }

  pid_t pid = atoi(argv[1]);
  printf("attach to %d\n", pid);

  if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) {
    perror("failed to attach");
    exit(1);
  }

  while (1) {
    waitpid(pid, &status, 0);
    if (WIFEXITED(status)) {
      break;
    } elseif (WIFSIGNALED(status)) {
      printf("terminated by signal %d\n", WTERMSIG(status));
    } elseif (WIFSTOPPED(status)) {
      printf("stopped by signal %d\n", WSTOPSIG(status));
    }

    ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
  }

  return0;
}

f:id:itchyny:20170728000028g:plain

大量に表示されるsignal 5とはどういう意味でしょうか。 プロセスにシグナルを送るコマンドであるkillに聞いてみましょう。

 $ kill-l1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
...

5番のシグナルがSIGTRAPだということがわかりました。 PTRACE_SYSCALLで再開するとSIGTRAPによって止まるという動作が確認できました。 これは期待されている動作なのでしょうか。 man 2 ptraceを引き、適当に検索しながら該当しそうな記述を探します。

Syscall-enter-stop and syscall-exit-stop are observed by the tracer as waitpid(2) returning with WIFSTOPPED(status) true, and WSTOPSIG(status) giving SIGTRAP.

From the tracer’s perspective, the tracee will appear to have been stopped by receipt of a SIGTRAP.

実際の挙動を確かめながら、manを読み込み少しずつ知識を蓄えていくことは楽しいことですね。

レジスタの状態を取得してみよう

プロセスの出力を覗き見するには、プロセスが呼ぶwriteシステムコールの引数を解析する必要があります。 システムコールが呼ばれるときのレジスタの中身を見てみましょう。 ptraceの引数にPTRACE_GETREGSを使ってみます。

struct user_regs_struct regs;

  while (1) {
    waitpid(pid, &status, 0);

    if (WIFEXITED(status)) {
      break;
    } elseif (WIFSTOPPED(status)) {
      ptrace(PTRACE_GETREGS, pid, NULL, &regs);
      printf("%lld%lld%lld%lld\n", regs.orig_rax, regs.rsi, regs.rdx, regs.rdi);
    }

    ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
  }

f:id:itchyny:20170728000012g:plain

なんだか出力が賑やかになってきました。 他のプロセスにアタッチし、システムコールが呼ばれる時のレジスタを出力しているだけですが、これはすでに簡易straceのようなものです。

一番左に出力したorig_raxは、システムコールの番号を表します。 システムコール番号はsys/syscall.hで定義されています。

#include <stdio.h>#include <sys/syscall.h>int main(int argc, charconst* argv[])
{
  printf("%d\n", SYS_write);
  return0;
}

私の手元ではSYS_write1でした。

上記のコードでは、システムコール番号の他にrsi, rdx, rdiを表示しています。 システムコールが呼ばれる瞬間、各レジスタには何が入っているのでしょうか。 x86_64 syscall registersなどでググって調べてもいいのですが、簡単なコードのアセンブリを見るという方法もあります。

#include <stdio.h>#include <unistd.h>void main() {
  write(STDOUT_FILENO, "Hello, world!", 13);
}
 $ gcc -O0-S test.c
 $ cat test.s
.file"write_regs.c"
    .section.rodata.LC0:
    .string"Hello, world!".text.globlmain.typemain, @functionmain:
.LFB0:
    .cfi_startprocpushq    %rbp.cfi_def_cfa_offset16.cfi_offset6, -16movq %rsp, %rbp.cfi_def_cfa_register6movl $13, %edxmovl $.LC0, %esimovl $1, %edicallwritepopq %rbp.cfi_def_cfa7, 8ret.cfi_endproc.LFE0:
    .sizemain, .-main.ident"GCC: (Ubuntu4.8.4-2ubuntu1~14.04.3) 4.8.4"
    .section.note.GNU-stack,"",@progbits

文字列のアドレスはesiに、長さ (writeの第三引数) がedxに、出力先であるfd (=STDOUT_FILENO=1) はediに書かれていることがわかります。 siレジスタは文字列操作のためのSource index, diレジスタDestination indexであることからその名がついていることを思い出すと、それぞれに文字列のアドレスとfdが入ってるというのは自然な挙動です。

やりたいことは「プロセスの出力を覗き見する」だったので、regs.orig_rax == SYS_writeのときにプロセスのメモリーから文字列を読み取り、出力すれば完成です。

syscall-enter-stopとsyscall-exit-stop

これまで「システムコールが呼ばれる時」とごまかしてきましたが、実はこの言い方は正確ではありません。 レジスタの値を出力して様子を見てみましょう。

 $ sudo ./main 24358
attach to 243581654595243411654595243411654595211401165459521140116545952821116545952821116545952113611654595211361# orig_rax rsi rdx rdi

左からシステムコール番号 (SYS_write), 文字列のアドレス, 書き込んだバイト長, fdです。 同じ値の行が二回ずつ表示されていることがわかります。 これは同じ引数で二回システムコールが呼ばれているのではなく、システムコールが呼ばれる直前と直後の二回表示されているのです。

さらに理解を深めるために、orig_raxレジスタに加えてraxレジスタも表示してみます。

 $ sudo ./main 24358
attach to 243581-38654595212781112786545952127811-38654595212621112626545952126211-38654595282111821654595282111-3865459521122111122654595211221# orig_rax rax rsi rdx rdi

システムコールが呼ばれると、その返り値がraxレジスタに入ります。 writeシステムコールの返り値は書き込んだバイト数ですから、writeの第三引数であるrdxraxが同じ行はシステムコールが呼ばれた後ということになります。 raxレジスタの値が-38となっている行は、システムコールが呼ばれる前の状態ということになります。 この-38が何の値なのかは後で説明します。

PTRACE_SYSCALLによりシステムコールをトラップすると、システムコールに入ったとき (syscall-enter-stop) と終わった時 (syscall-exit-stop) の二回停止するようになっています。 manを見てみましょう。

If the tracee was restarted by PTRACE_SYSCALL or PTRACE_SYSEMU, the tracee enters syscall-enter-stop just prior to entering any system call (中略). No matter which method caused the syscall-entry-stop, if the tracer restarts the tracee with PTRACE_SYSCALL, the tracee enters syscall-exit-stop when the system call is finished, or if it is interrupted by a signal. (That is, signal-delivery-stop never happens between syscall-enter-stop and syscall-exit-stop; it happens after syscall-exit-stop.).

プロセスの出力文字列を覗き見するのは、入ったときでも終わったときでもどっちでも構いません。 ただ二回表示されると困るので、ここではシステムコールに入った時だけ処理を行うようにしましょう。 では、syscall-enter-stopsyscall-exit-stopを区別するにはどうすればいいのでしょうか。 manを順番に読んでいくと、次のような記述に愕然とします。

Syscall-enter-stop and syscall-exit-stop are indistinguishable from each other by the tracer. The tracer needs to keep track of the sequence of ptrace-stops in order to not misinterpret syscall-enter-stop as syscall-exit-stop or vice versa.

関連する記述をmanから抜き出してまとめてみました。

  • syscall-enter-stopの直後はsyscall-exit-stopとは限らない。PTRACE_EVENTによる停止かもしれないし、終了しているかもしれない。
  • PTRACE_O_TRACESYSGOODを使うと、syscall-{enter,exit}-stopかそれ以外かは区別できる。
  • x86において、syscall-enter-stopではraxレジスタ-ENOSYS (この値が-38) になる。しかし、何らかのシステムコールが同じ値を返すこともあり、rax == -ENOSYSだからといってsyscall-exit-stopではないとは言い切れない。
  • syscall-enter-stopとsyscall-exit-stopは単体で見た時に区別することはできない。前の状態を保持しておいて調べるしかない。

以上を踏まえて、syscall-enter-stopでのみレジスタ値を表示するように実装してみました。

  ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESYSGOOD);

  int is_enter_stop = 0;
  long prev_orig_rax = -1;
  while (1) {
    waitpid(pid, &status, 0);

    if (WIFEXITED(status)) {
      break;
    } elseif (WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80)) {
      ptrace(PTRACE_GETREGS, pid, NULL, &regs);
      is_enter_stop = prev_orig_rax == regs.orig_rax ? !is_enter_stop : 1;
      prev_orig_rax = regs.orig_rax;
      if (is_enter_stop && regs.orig_rax == SYS_write) {
        printf("%lld%lld%lld%lld%lld\n", regs.orig_rax, regs.rax, regs.rsi, regs.rdx, regs.rdi);
      }
    }

PTRACE_SETOPTIONSはptraceリクエストの1つで、PTRACE_O_TRACESYSGOODを指定することで、システムコールによる停止かどうかを正確に判定できるようになります。 システムコール番号が前から変化したときにenter-stopだと判定するようにしました。 ずっと同じシステムコールが呼ばれ続けるならば、ずっとexit-stopで出力する可能性は否定できませんが、多くの現実的なコマンドそういうことはなさそうですし、仮にそうなったとしても出力を覗き見するコマンドとしての動作には影響しません。 だんだん精度良くシステムコールをトレースできるようになってきましたね。

文字列をメモリーから読み取ろう

プロセスがwriteシステムコールを呼ぶ時の引数から、出力されているバイト列を読み取ることができます。 PTRACE_PEEKDATAを使ってptraceを呼ぶと、プロセスの管理しているメモリーの値を取得することができます。

        peek_and_output(pid, regs.rsi, regs.rdx, (int)regs.rdi);

/* ... */void peek_and_output(pid_t pid, longlong addr, longlong size, int fd)
{
  if (fd != 1&& fd != 2) {
    return;
  }
  char* bytes = malloc(size + sizeof(long));
  int i;
  for (i = 0; i < size; i += sizeof(long)) {
    long data = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL);
    if (data == -1) {
      printf("failed to peek data\n");
      free(bytes);
      return;
    }
    memcpy(bytes + i, &data, sizeof(long));
  }
  bytes[size] = '\0';
  write(fd == 2 ? 2 : 1, bytes, size);
  fflush(fd == 2 ? stderr : stdout);
  free(bytes);
}

ptrace(PTRACE_PEEKDATA, pid, {addr}, NULL)の返り値が、そのアドレスにある値です。 標準エラー出力ならエラー出力に出すように実装してみました。

さっそく実行してみましょう。

やったー! topコマンドのプロセスにアタッチすると、まるでこちらでもtopコマンドを打ったような動きになりました。 もちろん、topコマンドでなくてどんなコマンドに対しても使えます。 Vimのプロセスにアタッチしてみましょう。 Vimの画面がミラーリングされていておもしろい!

今回実装するprocoutコマンドはここまでとします。 ただ、ここから様々な発展したコマンドが実装できると思います。 上のコードでは諸事情でfd = 1, 2のみ扱っていますが、この処理の必然性はありません (こう制限しないとエディタにアタッチした時にゴミが出力される)。 open, readなど、対応するシステムコールを増やしていくと、より便利なデバッガとなるでしょう。

straceやgdbそのものをそっくり実装しようとする必要はありません。 既にこれらのコマンドはあるじゃないですか、うまく動いているじゃないですか。 それでもなお、これらのコマンドの仕組みを理解することは重要な意義があると考えています。 ptraceシステムコールについて学ぶこと、それを使って実際に動くコマンドを作ってみること、簡単なデバッガを書いてみること。 そして、何をどのように実装すればstraceやgdbなどを作れるかをイメージできるようになること。 コマンドやツールの使い方を学ぶだけで満足するのではなく、それらの仕組みを深く知り、実装方法をイメージできるようになると、技術者としての知識がより幅広くそして深くなり、エンジニアリングを楽しめるようになっていくのだと思います。

Rustで書き直そう

Linuxシステムコールについて学ぶためには、C言語が最適の言語だと思います。 しかし、それらを組み合わせて大きなプログラムを組んだり、複雑なTUIを作ったりする必要が生じたときにふさわしい言語であるかどうかはかなり疑わしいと思います。

Rustはシステムプログラミングを学ぶのに適した言語です。 冒頭でご紹介したprocoutコマンドも、Rustで実装しています。 実は、先にRust版を書いてから、ブログのためにCで書き直しているのが実情です。 ブログを書く時にCを選んだのは、システムコールについて学ぶ時にRustを選ぶことが時に遠回りになりうるとわかったからです。

Rustを書いていると、実行時のメモリー安全性や、型チェックの厳格さから、「コードの正しさ」をコンパイルのチェックに委ねがちになります。 しかし、低レイヤーを触るとこれはかなり様子が変わってくるのがわかります。 当然のことですが、コンパイルが通っても、システムコールの使い方が適切でなければ全く動きません。 システムコールの呼ぶ順番が間違っていたら、コンパイルが通ったとしても、やってることは全くトンチンカンかもしれません。 また、Rustで書くこととコードがportableかどうかもイコールではありません。 結局のところ、誰かがportabilityの高い素晴らしいライブラリ (ただし中身は涙ぐましい努力で書かれている) を用意しないといけないのです。

Rustで書くことは、メモリー管理と型チェックに関してコードの安全性と大きな安心感をもたらしてくれます。 例えば、ptraceの2つの引数を誤って逆に書いてしまい、数分悩むといったことは起こりえないでしょう (記事を書く過程でやらかしました…)。 Rustの洗練されたエラーのハンドリングや、豊富なライブラリなどエコシステムの恩恵も受けられます。 ただ、低レイヤーを触るときは「正しくないコードはコンパイルが通ったとしても正しく動かない」という当たり前のことを教えてくれます。

さて、procoutのRust実装ですが、これは「読者の課題」としたいと思います。 大した行数じゃないので、簡単に移植できると思います。 私もコードをGitHubにあげていますので、実装できたらコードを見比べてみるとおもしろいかもしれません。 ここではRustで書いたことで得られた知見を簡単に書いておきます。

  • rust-lang/libcnix-rust/nixを使えばだいたいのことはできる。
  • nixパッケージにもmacOSで動くptraceは実装されていない。システムプログラミングでportableにすることは難しい。システムコールの番号すらプラットフォームによって異なる。
  • nixパッケージのptraceはまだ機能が揃っていない。getregsなんかは欲しい。まだ未熟なので、プルリクエストを送ったら簡単に取り込まれるかもしれません。

まとめ

ptraceシステムコールを使い、他のプロセスが呼ぶシステムコールを調べたり、メモリーを読み取ることができるのを確認しました。 プロセスの呼ぶwriteシステムコールの引数を使い、プロセスの出力を覗き見するコマンドprocoutをRustで実装しました。

当初は、普段使っているmacOS上で動くものを作ろうとしたのですが、nixライブラリのptraceのコードを見た時に諦めました。 VagrantUbuntuを立てて、その中で動作確認を行なっています。 普段Cを書くことがほとんどないので、自分にとって貴重な経験になりました。 /usr/includeから素早くファイルを開いて実装を確認するのにも慣れました。 portableなstraceを作るにはかなり大変だということもわかりました。

Rustは書いていてとても楽しい言語です。 今回だと、メモリーからバイト列を読み取って結合するコードはきれいに書けたと思います。 低レイヤーを触るときは何よりもまず、システムコール自体を正しく理解していることが大事です。 実際に動作するCのコードを書けることを確認しておくとよいでしょう。

straceやgdbってどうやって動いているのだろう。 この小さな疑問が浮かんだのが、一週間前のことです。 システムコールについて学び、それを使ったコマンドツールを作るのは、とても楽しい経験でした。 既存のツールの仕組みを調べることで「何を使えば何ができる」というレパートリーを増やし、それらがアイディアの源泉となって、便利なコマンドラインツールを作っていけたらいいなと思います。

参考にしたサイトは以下のとおりです。勉強させていただきました、ありがとうございます。

おまけ: ptraceを使いこなせるようになると、GitHub - nelhage/reptyr: Reparent a running program to a new terminalのような発想が出てくるわけです。いやはや、これはすごいですね。

ソースコード

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/ptrace.h>#include <sys/wait.h>#include <sys/user.h>#include <sys/syscall.h>void peek_and_output(pid_t, longlong, longlong, int fd);

int main(int argc, char *argv[])
{
  int status;
  struct user_regs_struct regs;

  if (argc < 2) {
    fprintf(stderr, "specify pid\n");
    exit(1);
  }

  pid_t pid = atoi(argv[1]);
  printf("attach to %i\n", pid);

  if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) {
    fprintf(stderr, "failed to attach\n");
    exit(1);
  }
  ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESYSGOOD);

  int is_enter_stop = 0;
  long prev_orig_rax = -1;
  while (1) {
    waitpid(pid, &status, 0);

    if (WIFEXITED(status)) {
      break;
    } elseif (WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80)) {
      ptrace(PTRACE_GETREGS, pid, NULL, &regs);
      is_enter_stop = prev_orig_rax == regs.orig_rax ? !is_enter_stop : 1;
      prev_orig_rax = regs.orig_rax;
      if (is_enter_stop && regs.orig_rax == SYS_write) {
        peek_and_output(pid, regs.rsi, regs.rdx, (int)regs.rdi);
      }
    }

    ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
  }

  return0;
}

void peek_and_output(pid_t pid, longlong addr, longlong size, int fd)
{
  if (fd != 1&& fd != 2) {
    return;
  }
  char* bytes = malloc(size + sizeof(long));
  int i;
  for (i = 0; i < size; i += sizeof(long)) {
    long data = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL);
    if (data == -1) {
      printf("failed to peek data\n");
      free(bytes);
      return;
    }
    memcpy(bytes + i, &data, sizeof(long));
  }
  bytes[size] = '\0';
  write(fd == 2 ? 2 : 1, bytes, size);
  fflush(fd == 2 ? stderr : stdout);
  free(bytes);
}

Mackerelのプラグインを書く楽しみ ― Rustでプラグインを書くためのライブラリを作りました!

$
0
0

Mackerelは「エンジニアをワクワクさせる」ツールであることをサービスの大事な考え方の一つとして捉えています。 一体どういう場面でエンジニアはワクワクするのでしょうか。 簡単にインストールできるmackerel-agentや、直感的で触りやすい画面、チャットツールとの連携は大事な機能です。 しかし、監視ツールとしてもっと重要なのは、ミドルウェアのメトリックをどのように可視化し、何を監視するかということです。

Mackerelは公式のプラグインリポジトリに各種プラグインを揃えています (contributorの皆様ありがとうございます)。 これらはすべてGo言語で書かれています。 しかし、MackerelのプラグインはGo言語で書かなければいけない、ということはありません。 例えばカスタムメトリックのヘルプページのサンプルプラグインRubyで書かれていますし、メタデータのヘルプのサンプルスクリプトPerlで書かれています。

Mackerelのプラグインはどんな言語で書いていただいても構いません。 Rubyで書いてもシェルスクリプトで書いてもHaskellで書いても構いません。 世の中にはいろいろなミドルウェアがありますから、公式プラグインのなかに要件を満たせない場合もあるかもしれません。 自分の好きな言語でミドルウェアのメトリックを可視化する、そして監視をかけて、異常をいち早く察知する、つまり自らコードを書いて監視を作ること、これはすごくワクワクすることなのです。

こういうMackerelのhackableな部分はすごく大事だと思っていますし、これからも大事にしていきたいと思います。 初期のデザインドキュメントを見てみても、APIで様々な操作を行えることを最初から重要視していたことが伺える記述が見つかります。 そのまま実行したらグラフを確認できるcurlコマンドサンプルが置かれているのも、このあたりの考えが反映さています。 f:id:itchyny:20171005000230p:plain

ところで、私はRustという言語がとても気に入っています。 家ではだいたいRustのことを考えていて、Rustで書かれたプロダクトのコードを読んだり、いろいろ作ったりして楽しんでいます。 好きな言語があるとMackerelのAPIを叩いてみたくなるわけで、mackerel-client-rsmackerel-client-hsを作ってきました。 ただ、APIクライアント作りってやることが淡々としていて、正直飽きてくるんですよね。 まだすべてのAPIに対応できていないのでがんばらないといけないわけですが、いかんせんモチベーションが上がりにくいわけです。

一方で、Mackerelのメトリックプラグインを作るのは、APIクライアントを作るよりも格段におもしろいわけです。 そもそもメトリックのとり方はいろいろありますし、サーバーの変化を時系列データとして可視化できると、これがすごく楽しい。 ひょっとして、MackerelプラグインをRustで書いたらもっと楽しいんじゃない?

本題です。 RustでMackerelのプラグインを書くためのヘルパーライブラリ、mackerel-plugin-rsを作りました。 公式のgo-mackerel-pluginのRust版です。

github.com

まだ作ったばかりで、差分計算など一部の機能をまだ足りていませんが、今はモチベーションがすごく高いので、すぐに実装されると思います。 上記ライブラリを使って、手始めにloadavgプラグインuptimeプラグインを書いてみました。 f:id:itchyny:20171005005452p:plain topやuptimeコマンドで3つの数字が並んだ状態のloadavgはよく目にしますが、実際にグラフにしてみると激しく変化するloadavg1、それに上下するloadavg5、よりなだらかなloadavg15というメトリックの特徴がよく出ていて、すごく美しいわけです。 f:id:itchyny:20171005005912p:plainプラグインのコードはこういう感じです。

externcratelibc;
#[macro_use]externcratemackerel_plugin;

usestd::collections::HashMap;
usemackerel_plugin::*;

pubstructLoadavgPlugin {}

#[inline]fnget_loadavgs() ->Result<[f64; 3], String> {
    letmut loadavgs: [f64; 3] = [0.0, 0.0, 0.0];
    unsafe {
        let ret =libc::getloadavg(loadavgs.as_mut_ptr(), 3);
        if ret !=3 {
            Err("failed to get load averages".to_string())
        } else {
            Ok(loadavgs)
        }
    }
}

impl Plugin for LoadavgPlugin {
    fnfetch_metrics(&self) ->Result<HashMap<String, f64>, String> {
        letmut metrics =HashMap::new();
        let loadavgs =get_loadavgs()?;
        metrics.insert("loadavg.loadavg1".to_string(), loadavgs[0]);
        metrics.insert("loadavg.loadavg5".to_string(), loadavgs[1]);
        metrics.insert("loadavg.loadavg15".to_string(), loadavgs[2]);
        Ok(metrics)
    }

    fngraph_definition(&self) ->Vec<Graph> {
        vec![
            graph! {
                name: "loadavg",
                label: "Load averages",
                unit: "float",
                metrics: [
                    { name: "loadavg15", label: "loadavg15" },
                    { name: "loadavg5", label: "loadavg5" },
                    { name: "loadavg1", label: "loadavg1" },
                ]
            },
        ]
    }
}

プラグインを作るとき、まずどのようにしてメトリックをとるかを調べなければいけません。 これが普段ウェブアプリケーションを書くのとは違っていて実に楽しいわけです。 loadavgの取得方法について調べていくと、getloadavg(3)LinuxBSDの両方で同じように使えることが分かりました。 mackerel-plugin-loadavgはlibc::getloadavgを使うことで、macOSUbuntuの両方で同じようにコンパイルして動作することを確認しています。 一方でuptimeのportableな取得する方法は難しく、LinuxではsysinfoBSDではsysctlboottimeを取得してから計算する必要がありました。

今回プラグインを書くためのライブラリを書く上で、公式のgo-mackerel-pluginをかなり読んで挙動を確認しました。 歴史的な経緯でinconsistentな挙動となってしまっていたり、プラグイン側でやったほうがよさそうな余計な機能があったり、エラーハンドリングを丁寧にやったほうがよさそうなところなど、よろしくないところがいくつかあることに気が付きました。 mackerel-plugin-rsを作る時間にはお給料は出ていませんが、go-mackerel-pluginへの理解が深まり、問題点が把握できたのはよかったなと思います。 いくつか良くない処理は直そうと思っています。 また、ヘルプの文言も不正確な部分は既にいくつか修正しています。

Mackerelのプラグインを知ることは、その監視対象を知ることです。 Redisのプラグインを作ろうと思ったら、redis-cli infoの各項目が何を表しているか、正確に把握する必要があります。 Linuxのメトリックを取得するということは、Linuxに対する理解を深めるということです。 Rustはそういう領域が得意な言語ですので、loadavgをとるには→man 3 getloadavgを見る→libcライブラリのドキュメントでgetloadavgを探すというようにすぐに目的の関数にたどり着くことができます。

Linuxや各種ミドルウェアへの理解を深めることは、エンジニアとしてすごくワクワクすることです。 監視対象に詳しくなると同時に、プログラムを書く楽しさを改めて教えてくれるのです。

mackerel.io

Mackerelはただのサーバー監視ツールではありません。 プラグインを作るワクワクを、みなさんも是非実感してみてください。

本日10/5はMackerel Dayを開催します。 私も会場にいるので、是非お声がけください。

mackerelio.connpass.com

Mackerel開発チームのメンバーが書いた公式の入門書、出ました。

Mackerel サーバ監視[実践]入門

Mackerel サーバ監視[実践]入門

はてなでは、ワクワクしたいエンジニアに限らず、ディレクターやCRE (Customer Reliability Engineer) も募集しております。 hatenacorp.jp

負荷を均すための『時間軸シャーディング』という考え方

$
0
0

ウェブアプリケーションを作っていると、負荷を分散させるために「タイミングをばらけさせる」場面に時々遭遇します。 データの更新、キャッシュのフラッシュ、バッチ処理など様々な問題で、同じ構造が見られます。

例えば、スマホアプリからバックグラウンドで1時間ごとに何らかの情報をサーバーに送りたいとします。 愚直に毎時0分に更新処理を行うようにすると、すべてのユーザーから同じタイミングでリクエストが来てしまいます。 ですから、リクエストのタイミングをユーザーごとにばらして負荷を均す必要があります。

他のケースを考えます。 5分ごとにジョブを投入して何らかの更新を行うタスクがあるとします。 本来ならデータベースに更新を行いたいのですが、データベースのハードウェアの限界が近いので、更新データをまずキャッシュに乗せるようにしました。 何らかのタイミングでキャッシュからデータベースにフラッシュする必要があります。 データベースに書き込むタイミングをジョブによってばらして負荷を均したいという要求が出るのは自然なことです。

負荷を均すときに考える基本的なことは、何が時間軸上でばらけるかということです。 例えばインストール時刻がばらけるならば、インストール時刻から1時間ごとに更新するという処理でユーザーごとにばらけるでしょう。 そんな都合のいいタイミングがない場合や、ストレージに情報を持てない場合には、id番号のようにすでにばらけているものから処理する時刻を決めるのが有効です。 例えば5分毎のジョブ投入のケースの場合、id番号のmod 12を取った値が分を5で割った値と同じになったタイミングで処理を行うようにすると、重い処理を1時間に一回にしてかつ負荷を分割することができます。

func shouldUpdate(job: Job) bool {
    return (job.ID % 12) == (time.Now().Minute() / 5)
}

idが文字列の場合は文字コードの和をとる、SHA-1を取って文字コード和をとる、CRCをとるなど、とにかくmodでばらけるような数字を作れたら成功です。

本来なら毎回行う更新処理を数回に一回にしてばらけさせる、あるいはリクエストの集中を避けるためにタイミングをばらけさせる。 このように、負荷を分割して均す目的で時間軸上でばらけさせる処理は、時間軸方向のシャーディングと言うことができます。

一般にシャーディングというと、データベースやストリームをスケールアウトさせて負荷を分散させる、空間軸方向のシャーディングを指します。 一つのデータベースに集中させていたものを、ハードウェアの限界などのためにデータベースを分割して負荷を軽減させるのがよくあるケースです。 何らかのハッシュ関数のmodからノードを決定してデータを書き込む。 ハッシュ値のmodが均等に分散すれば、各ノードの負荷は1/(ノード数)に軽減されます。

更新リクエストを時間軸上で分散させるときも、idから計算したハッシュ値のmod (単純なケースではidのmod) を取ります。 1時間を5分毎に分割するときは、ノード数が12ですからmod 12を取ってノードを決定します。 15ノードに分けても30ノードに分けても構いません。 一日一回の更新タスクを24分割するならmod 24を取って時刻と比較するとか、96分割してその日の経過した分を15で割った数字と比較するなど、分割方法はいくらでもあります。 ノードの分割方法は、タスクの投入の仕方、どれくらいの失敗が許容されるか、短い期間で同じ処理を行なっても大丈夫かなど、問題の性質によって変わってきます。 いずれにしても、idや名前などの固定値からハッシュ値を作ることが大切です。

もっと古典的なユースケースとして、重いバッチ処理をmodで分割するのも、時間軸のシャーディングと言えます。 偶数番号と奇数番号でバッチを分割するというのは最も単純なシャーディングです。

これまで「タイミングをばらけさせる」と言っていたものを「時間軸のシャーディング」という言葉で表現することで、空間軸のシャーディングの用語を類推して用いることができます。 例えば「シャードが偏る」という言葉がありますが、時間軸シャーディングの場合で偏る場合はだいたいハッシュ関数か元の値の選び方が悪いでしょう。 「シャードを分割する」場合は、特定の一つのシャードを分割するよりも、10分毎のノードを5分毎に分割するという風に時間軸上のノード数を増やすのが有効だと思います。 空間軸シャーディングとは違って、時間軸シャーディングの場合はデータ移行を考えなくて良いので、ハッシュ関数やノードの分割方法を変えて「リシャーディング」を行うことでうまくいく場合が多いと思います。

上記で書いた内容は、特に新しいことはやっていませんし、タイミングをいい具合にばらけさせるコードを書いたことがある人はたくさんいると思います。 そういう処理を時間軸のシャーディングと捉えることで、言葉を類推して用いることができるようになり、空間軸シャーディングと対比して利点と欠点を比較し、より良い方を選ぶことができるようになると思います。

「更新が多くてハードウェアの限界に来てますが、仕様としてもう少し更新回数減らしてよさそうですね。ただ、タイミングがばらばらになるとよさそう」のようなあやふやな言葉で伝えていたものを、「時間軸シャーディングするとよさそうですね。1時間を12ノードに分けましょう。1時間に一回の更新で仕様上は問題はありません。空間軸シャーディングはこれでだめになった時にまた考えましょう」のようにはっきりと伝えられるとかっこいいですね。

追記: ジョブキュー使いましょうというのは真っ当なご意見ですし、負荷のパターンと工数によってジョブキューを作るべき場面もあるでしょう。もともと時間軸シャーディングを考えたのは負荷が一定でスパイクのない系で工数をかけずにタイミングをバラすというものだったので、負荷を均す一つの解法にすぎないことは把握しております。

Serverlessconf Tokyo 2017で『サーバレスアーキテクチャによる時系列データベースの構築と監視』という発表してきました

$
0
0

先日開催されたServerlessconf Tokyo 2017にスピーカーとして参加しました。

2017.serverlessconf.tokyo

Mackerelの今の時系列データベースは、マネージドサービスを組み合わせて作っています。 検証・実装・投入フェーズを終えて、運用・新機能開発フェーズに入っています。そんな中で、監視サービスを提供する私たちが、サーバーレスアーキテクチャで作ったミドルウェアをどのように監視しているかについてお話しました。 何かしら役に立つことや発想の元となるようなことをお伝えできていたらいいなと思います。

私も他の発表から様々なことを学びました。特に面白かった発表を挙げておきます。

真のサーバレスアーキテクトとサーバレス時代のゲーム開発・運用

ゲーム開発を支えるBaaSを開発されるなかで得られた様々な知見についてお話されていました。 プッシュ通知のために大量のコネクションを張りたい場面でAWS IoTを用いるといった話はとてもおもしろいと思いました。 アクセス権限の話でLambdaのメモリーにキャッシュを持つという話をされていて、そんな手があるのかと驚きましたが、物理的な限界がありスケールしないので、使いどころ (というかヒット率に基づくキャッシュの削除) が難しいなと思いました。

サーバーレスについて語るときに僕の語ること

軽快な語り口と盛り上げ方はランチセッションにふさわしく、うまいなぁと思いました。 サーバーレスというのは文字通りだと枠としては大きすぎるんですよね。 スケーラビリティーやイベント駆動により処理を繋げるアーキテクチャも包含する言葉がほしいなぁという気持ちになりました。 よっしゃサーバーレスやとかいってサービスを選んだ時に、その用途が本当にそのサービスの適した使い方なのかどうかはきちんと考えるべきですよね。

Open source application and Ecosystem on Serverless Framework

なるほど確かに、こういう方向に進んでいくよねという気持ちになりました。 サーバーレスミドルウェアという言葉が面白かったです。聞いた瞬間、様々なものが繋がった気がしました。 マネージドサービスを組み合わせて作るという流れはおそらくこれからも続いていくと思いますし、それを組み合わせることで1つの「ミドルウェア」として機能するものを作るのはよくあることだと思います。 そういうサーバーレスミドルウェアは一発で立ち上がるべきだし、利用者が実装内部 (どういうマネージドサービスを使っているか) を気にする必要なく使えるようになっているのが理想の形。 こういうミドルウェアって再利用可能な形でシェアできるといいし、それらを組み合わせてサービスを構築できるとかっこよさそうですよね。 そう、docker-composeみたいなやつが欲しいですね (あるのかしら、まだよく知らないです)。

最後に

私は対外発表は初めてこともあり、とてもよい経験になりました。 慣れていなくてストーリーを組み立てるのに苦労したり、資料を作るのに時間がかかったりしましたが、なんとか終わって今は安堵しています。 E2E監視というところを抽象化して、系全体として動いているかを見るというところに落とし所を見つけられたのは、発表の前日でした。 私たちが作ったのは時系列データベースの「サーバーレスミドルウェア」なんですよね、こういうまとめて見た時の概念を思いついていなかったのは正直くやしい。 k1LoWさんには発表や質疑応答の中でもdiamondに触れていただいて、親近感がわきました。

マネージドサービスを組み合わせてサービスを作ることは、セキュリティの問題やスケーラビリティー、運用コストの削減など様々な問題を解決してくれると思います。 ただし、各マネージドサービスにはそれぞれ使いどころや苦手なユースケースがありますので、他の成功事例だけを見て盲目的にコンポーネントを選ぶのではなく、適した使い方をしているかどうかをきちんと吟味しましょうということですね。 こういうアーキテクチャーに乗ったサービス開発はどんどん加速していくと思いますので、私も技術的に置いていかれないように精進していきたいと思います。

主催の吉田さん、運営スタッフのみなさま、本当にありがとうございました。

zshの標準エラー出力の色を赤くする

$
0
0

最近stderrを赤くするように設定したら、コマンドの出力がかなり見やすくなりました。 f:id:itchyny:20171116233845p:plain

設定はこんな感じに書いてます。

zmodload zsh/terminfo zsh/system
color_stderr() {
  while sysread std_err_color; do
    syswrite -o 2"${fg_bold[red]}${std_err_color}${terminfo[sgr0]}"done
}
exec2>>(color_stderr)

fg_bold[red]のところを fg[red]とかbg_bold[red]とかするとスタイルを変更できます。 古いzshでは動かないらしいので、古い環境も気にしたい場合は is-at-least 4.3.4でチェックするとよさそうです。

この設定の元ネタはcoloring stderr - was Re: piping stderrです。 大体はうまくいくし、普段使う分には大きな問題は起きていないのですが、わりと乱暴なことをやっているという自覚はあります。 元ネタのスレッドでも触れられていますが、 echo L1; echo L2 >&2; echo L3; echo L4 >&2とやると L2L3が逆に出力されたりします。 あと bash -iしたあとに topを起動できないといったりします。

いくつか問題が起きるケースはありそうだけど、二週間ほど試してみて普段端末を触る範囲では困っていないのと、stderrに色が付くのがありがたいので使っています。もっといい方法があれば教えてください。

ついでに使っているPROMPTの設定はこんな感じです。終了コードを元に色を変更しています。

PROMPT="%(?.%{$fg[green]%}.%{$fg[blue]%})%B%~%b%{${reset_color}%} "
PROMPT2="%{$bg[blue]%}%_>%{$reset_color%}%b "
SPROMPT="%{$bg[red]%}%B%r is correct? [n,y,a,e]:%{${reset_color}%}%b "

Go言語のHTTPリクエストのレスポンスボディーとEOF

$
0
0

Reader interface の Read関数は、どのタイミングで io.EOFを返すのでしょうか。 まずは strings.Readerで見てみましょう。

package main

import (
    "fmt""strings"
)

func main() {
    r := strings.NewReader("example\n")
    for {
        var b [1]byte
        n, err := r.Read(b[:])
        fmt.Printf("%d %q %v\n", n, b, err)
        if err != nil {
            break
        }
    }
}

結果

1 "e" <nil>
1 "x" <nil>
1 "a" <nil>
1 "m" <nil>
1 "p" <nil>
1 "l" <nil>
1 "e" <nil>
1 "\n" <nil>
0 "\x00" EOF

Readの結果は、読み込んだbyte数です。なにもなくなってから io.EOFを返していることがわかります。

ファイルだとどうでしょうか。

package main

import (
    "fmt""os"
)

func main() {
    f, err := os.Open("main.go")
    if err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }
    defer f.Close()
    for {
        var b [1]byte
        n, err := f.Read(b[:])
        fmt.Printf("%d %q %v\n", n, b, err)
        if err != nil {
            break
        }
    }
}
1 "b" <nil>
1 "r" <nil>
1 "e" <nil>
1 "a" <nil>
1 "k" <nil>
1 "\n" <nil>
1 "\t" <nil>
1 "\t" <nil>
1 "}" <nil>
1 "\n" <nil>
1 "\t" <nil>
1 "}" <nil>
1 "\n" <nil>
1 "}" <nil>
1 "\n" <nil>
0 "\x00" EOF

同じですね。

ではHTTPリクエストだとどうでしょうか。

package main

import (
    "fmt""net/http""os"
)

func main() {
    resp, err := http.Get("http://example.com")
    if err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }
    defer resp.Body.Close()
    for {
        var b [1]byte
        n, err := resp.Body.Read(b[:])
        fmt.Printf("%d %q %v\n", n, b, err)
        if err != nil {
            break
        }
    }
}
1 "<" <nil>
1 "/" <nil>
1 "h" <nil>
1 "t" <nil>
1 "m" <nil>
1 "l" <nil>
1 ">" <nil>
1 "\n" EOF

なぜなのか… なぜなのか!!!

検索するとruiさんのエントリーが出てきました。 qiita.com (三年前の記事だった… もしかして… これは常識なのか!?) そして、mattnさんがgolang-nutsでスレッドを立てられていたのでざっと見ました。 Issue 49570044: code review 49570044も勉強になります。 これはContent-Lengthがセットされたレスポンスボディーを最後まで読んだ後に、すぐにコネクションを使いまわせるようにするための意図した挙動であるということがわかりました。

Readerはわりと使い慣れたinterfaceなのにハマってしまいました。 Go言語を書いていると手癖ですぐにerrを返してしまいがちですが、Readは読み込んだバイトがありながら io.EOFになることがあることに気をつけなくてはいけません。 いや、これはruiさんの記事を同じことを言ってますね… ほんとは常識なのかもしれない…

ライブラリーが Reader使ったインターフェースを公開しておきながら、io.EOFの扱いが適切でないと簡単にバグを踏んでしまいます。 strings.Readerでテストして満足していると、resp.Bodyに繋いだ瞬間なぜか挙動が変わるということがあるかもしれません。

Readの返り値のバイト数を捨ててませんか? err != nilでもデータを読み込んでいるかもしれませんよ?

みなさん、気をつけましょう。おわり。

Go言語のsyscall.Sysctlは最後のNULを落とす

$
0
0

カーネルのパラメータを引いたり設定したりする時に便利なのが sysctlコマンドです。

 $ sysctl kern.ostype
kern.ostype: Darwin

このコマンドのシステムコールをGo言語から叩いて、OSの種類を引いてみましょう。

func main() {
    ret, _ := syscall.Sysctl("kern.ostype")
    fmt.Printf("%s\n", ret)
}
Darwin

問題ないですね。 数字を返すものを叩いてみましょう。

 $ sysctl machdep.cpu.feature_bits
machdep.cpu.feature_bits: 9221959987971750911
func main() {
    ret, _ := syscall.Sysctl("machdep.cpu.feature_bits")
    val := *(*uint64)(unsafe.Pointer(&[]byte(ret)[0]))
    fmt.Printf("%d\n", val)
}

出力結果

9221959987971750911

unsafeパッケージは使いたくないだって? アーキテクチャエンディアン固定になっちゃうけどなぁ。

   val := binary.LittleEndian.Uint64([]byte(ret))
9221959987971750911

次は extfeature_bitsを見たい?

 $ sysctl machdep.cpu.extfeature_bits
machdep.cpu.extfeature_bits: 1241984796928
func main() {
    ret, _ := syscall.Sysctl("machdep.cpu.extfeature_bits")
    val := binary.LittleEndian.Uint64([]byte(ret))
    fmt.Printf("%d\n", val)
}

実行してみます

panic: runtime error: index out of range

goroutine 1 [running]:
encoding/binary.binary.littleEndian.Uint64(...)
    /usr/local/Cellar/go/1.9.2/libexec/src/encoding/binary/binary.go:76
main.main()
    /private/tmp/main.go:16 +0x1d7
exit status 2

!!!> index out of range <!!!

なにが起きたのか。 syscall.Sysctlの返り値 (string) の長さを見てみると、すぐにわかります。

ret, _ := syscall.Sysctl("machdep.cpu.feature_bits")
fmt.Printf("%d\n", len(ret))
ret, _ = syscall.Sysctl("machdep.cpu.extfeature_bits")
fmt.Printf("%d\n", len(ret))
8
7

え、つまりこれは… https://github.com/golang/go/blob/8776be153540cf450eafd847cf8efde0a01774dc/src/syscall/syscall_bsd.go#L474

// Throw away terminating NUL.if n > 0&& buf[n-1] == '\x00' {
        n--
    }
    returnstring(buf[0:n]), nil

な、なんということを…

つまりstructでも…?

ret, _ := syscall.Sysctl("kern.boottime")
fmt.Printf("%d\n", len(ret))
15

お、おう… わかった… わかったよ…

  • sysctl.Sysctlは最後のNULを落とす。
  • 文字列を返すような場合はよい挙動だが、数字や構造体の場合は注意が必要。64bit整数を返す場合、その値に依存して8byteだったり7byteだったりする。
  • unsafe.Pointerで数字や構造体にキャストする分には問題ないように思われる (ほんまやろか?)。 + "\x00"してから処理したほうがいいかもしれない。

困っている人もいる () が、指摘されているように sysctlパッケージは変更を受けつけていないので、この挙動が変わることや別の関数が追加されることはなさそう。 そもそも stringで返すのなんかおかしくない? []byteで欲しいよね…

それあります! golang.org/x/sysパッケージの unix.SysctlRawを使いましょう。 https://github.com/golang/sys/blob/53aa286056ef226755cd898109dbcdaba8ac0b81/unix/syscall_bsd.go#L524

func main() {
    ret, _ := unix.SysctlRaw("vm.loadavg")
    fmt.Printf("%d\n", len(ret))
    ret, _ = unix.SysctlRaw("kern.boottime")
    fmt.Printf("%d\n", len(ret))
    ret, _ = unix.SysctlRaw("machdep.cpu.extfeature_bits")
    fmt.Printf("%d\n", len(ret))
}
24
16
8

これで安心して眠れそうですね。

sysctl.Sysctlは最後のNULを落とします。文字列として欲しい時はこれで良いが、バイト列として欲しい時は unix.SysctlRawを使いましょう。


2017年を振り返って

$
0
0

今年は仕事で関わっているプロダクトが大きな転換期を迎えて、様々な経験ができました。 ミドルウェアを自ら作り上げ、データをオンラインで移行し、運用を始めるというのはなかなか経験できないことだと思います。 サービスは以前より安定し、穏やかな年末を過ごしています。

今年は初めてカンファレンスで登壇しました。 慣れないことばかりで色々と戸惑いましたが、沢山の方に発表を聞きに来ていただいて嬉しかったです。 マネージドサービスを組み合わせて1つのソフトウェアを作り、それをサーバーレスミドルウェアとして抽象度を上げて捉えることができるようになったもの、このカンファレンスに参加してよかったことでした。

今年は19記事書きました。 特に、以下の記事は多くの方に読んでいただきました。

一年の後半にアウトプットが減速しているのは、カンファレンスの登壇に体力を使ってしまったこと、Prime VideoやAbemaTVを見ながらぼーっとする時間が増えたこと、プロダクトコードのリファクタリングに随分と入れ込んでしまったことなどが原因だと思います。

年始はコンパイラーやインタープリターに興味があり、インタープリターを書いてみたりLLVMを試したりしていたのですが、3月ぐらいには興味も薄れていきました。8月くらいまでは仕事が忙しく余裕がありませんでしたね。その後Rustを真面目に書き始め、システムコールに興味を持ち、そしてMackerelのシステムメトリックに興味が移り、go-osstatを作り始めました。最近はエディターの実装に興味を持ち、色々と調べています。ことごとくトレンドを外した感じに共感を持てますね、まぁ自分のことですが。

Vim周辺の変化で言うと、vimshellを辞めてterminalを使いだしたことくらいですが、他はほとんど変化はありませんでした。lightlineのスター数は順調に伸び、2500を突破しました。ユーザー数は増えてもissue報告はそんなに増えていないし、ほとんどが設定の仕方に関する質問なので楽なものです。

今年は投資を始めた年でした。これまで一切経済のニュースとか興味がなかったのですが、今のペースで経済が成長していくと現金で持っているのがアホらしく思えてくるので、少しずつ時間を取って勉強し始めています。まだド素人なので特に語れることはありません。

生活面、仕事面共に大きな変化はありませんでしたが、緩やかに成長できた一年だったと思います。来年は少しずつアクセルを踏んで頑張っていきましょう。

山村美和「なんでも本気でやるから楽しいんじゃん。」

ばらかもん

HHKBを買いました・他近況報告

$
0
0

4月頃から仕事中に両手の小指に痛みを感じるようになり、コードを書いては少し手を休めないとつらくなっていました。意図的に小指を使わないように打鍵したり、比較的指を使わないレビュー作業を合間に挟んだりしていたのですが、痛みは出てきたり引いたりという状況が続いていました。特に家に帰ってから再度キーボードの上に手を置くと、また痛みを感じたり、痛くなくてもあまりコードを書くのがはかどらない状況が続いていました。

MacBook Airは六年前から使い続けています。大学の時に買ったMacBook Airが今でも現役で動いています。手に馴染んだキーボードとキー配列とキーストロークを六年も使ってきたので、ここに来てMacのキーボードがつらくなるとは思っていませんでした。

どのキーボードを使うかかなり悩んだのですが、やはり定評のあるHHKBがいいのだろうなぁと思い、特に店舗で試したりすることもなく思い切って購入しました。広く評価されていることは知っていたので、手に馴染まなくて後悔するかもしれないという気持ちはあまりありませんでした。

家で二週間ほど使い、会社に持って行って一週間ほど使ったところ、かなり手に馴染むようになってきました。おかげさまで手の痛みも引いていき、かなり楽にタイピングできるようになりました。それでも使い始めて一週間は、深いストロークに苦しめられました。ずっとMacのキーボードに慣れてきてましたから。最近では柔らかいキータッチにも慣れて、快適に過ごしています。

ここ二か月ほどは、Vimソースコードに没頭していました。これまで自らの道具は自ら磨くという信念の元、様々なVimプラグインプラグインマネージャーを作ってきました。そして、私の関心は次第にVimというエディターそのものに向けられ始めていました。3月からVimソースコードを読み始め、独自のコマンドを作ったりして感覚を掴み、4月に入ってからはVim scriptの実行系のリファクタリングにとりかかっていました。実行系から構文解析処理を分離して構文木をキャッシュするという作業を進めていたのですが、あまりにも進捗が悪く、だんだんモチベーションも下がってきたため、途中で投げ出してしまいました。evalNの関数を構文解析と評価器に分けるだけなのですが、それらの関数が再帰していることや、handle_subscriptあたりの処理が複雑すぎて挫折しました。しばらくして興味が戻ってきたら再開したいと思います。

Vimの複雑なコードを前に立ち止まってしまった後はしばらくコードを書いていなかったのですが、また別のことに興味が向き始めました。最近は画像処理を始めました。各ファイルフォーマットの構造や、圧縮、類似画像検索など、興味深いテーマがたくさんあります。まだ素人同然なのですが、まとまってきたらエントリー書きたいなと思っています。

最近どんどん暑くなってきていますね。以前住んでいたところはマンションの最上階の南向きで、夏は陽射しが強く、また屋上からの熱も加わって、すぐに温度が上がって嫌な朝を過ごしていました。今は最上階でもなく、また西向きなので朝は快適です。その代わり昼を過ぎると日光が差し込んで、ぐんぐん室温が上がります。実家は夏でも扇風機で過ごす環境だったので、クーラーはかなり苦手です。かつて体調を崩したこともあるので、腕から汗が出始めてから付けるようにしています。今日は曇っていて、快適な室温です。

AtCoder Beginner Contest 040

$
0
0

A - 赤赤赤赤青

n個の中の特定の一つのブロックを隣り合うブロックの入れ替えによって両端のいずれかに移動する最小の回数。

main :: IO ()
main = getContents >>= print . (\[n, x] -> min (x -1) (n - x)) . map read . words

B - □□□□□

n個のタイルをどれだけたくさん使ってできる限り正方形に並べられるか。指標は二辺の差と使い切れなかったタイルの数の和が最小のもの。

main :: IO ()
main = readLn >>= print . solve

solve :: Int -> Int
solve n = minimum [ abs (l - k) + n - k * l | k <- [1..round (sqrt (fromIntegral n))], let l = n `div` k ]

C - 柱柱柱柱柱

柱を端から端まで飛び移っていく。一本飛ばしでも飛べるのだが、柱を飛び移るときに高さが違うだけ疲れるので疲れを最小にしたい。

import Data.List (foldl')

main :: IO ()
main = print . solve . tail . map read . words =<< getContents

solve :: [Int] -> Int
solve (a0:as) = fst $ fst $ foldl' f ((0, a0), (0, maxBound)) as
  where f ((x, ax), (y, ay)) az = (min (x + abs (ax - az), az) ((y + abs (ay - az)), az), (x, ax))

Array使ってDPしてもいいけど、せいぜい最後の二本の情報を持っていればよいのでそこまでする必要はないかな。一本飛ばしまでしかできないからそうだけど、何本飛ばしでもできて、過去の全てを使って新しいところの値が決まるという問題ならDPしたほうがよさそう。

D - 道路の老朽化対策について

重み付き無向グラフ上で、ある重み以上の道しか通れない時にどれだけの数のノードに辿り着けるか。

import Control.Monad (replicateM)
import Data.Graph (buildG, reachable)

main :: IO ()
main =do
  [n, m] <- getInts
  abys <- replicateM m (toTriple <$> getInts)
  q <- readLn
  vws <- replicateM q (toTuple <$> getInts)
  mapM_ print $ solve n abys vws

getInts :: IO [Int]
getInts = map read . words <$> getLine

solve :: Int -> [(Int, Int, Int)] -> [(Int, Int)] -> [Int]
solve n abys vws = [ length $ reachable gr v | (v, w) <- vws, let gr = buildGraph w ]
  where buildGraph w = buildG (1, n) $ concat [[(a, b), (b, a)] | (a, b, y) <- abys, y > w]

toTuple :: [a] -> (a, a)
toTuple xs = (head xs, xs !!1)

toTriple :: [a] -> (a, a, a)
toTriple xs = (head xs, xs !!1, xs !!2)

すみません、Largeは通らないです。50点は取れた。本当は、最初に各ノード間の最小の重みを計算しないといけなさそうだけど、元気がなかった…

30のプログラミング言語でFast inverse square rootを実装してみました!

$
0
0

あなたの好きな言語は何ですか。そして、あなたの好きなアルゴリズムは何ですか。

好きな言語があると、その言語でどんな問題でも解決しようとなりがちになります。その言語を極めるのは素晴らしいことですが、その言語や似たような言語でしかコードが書けなくなったり、他の言語に対して見向きもしなくなってしまう可能性があります。

勇気を出して新しい言語にチャレンジしてみませんか?色々な言語に挑戦してみませんか?

何から始めればいいか分からない。次にどの言語を学べばいいか分からない。いま特に何も困っていない。何でも得意な言語で書けてしまう。そういう人が多いのではないでしょうか。

新しい言語にチャレンジするきっかけを作る一つの方法は、ある特定のアルゴリズムを一つ決めて、あらゆる言語で実装してみることです。解く問題が大きすぎると力尽きてしまうので、せいぜい20〜30行程度で書ける簡単なものが良いでしょう。大事なことは、そのコードが実際に実行できることを手元で確認することです。実行処理系を実際にインストールし、コードをコンパイルして実行してみる、それだけでも多くのことを学ぶことができます。

実際、私もこういうチャレンジをするのは今回が初めてです。このエントリーでは、Fast inverse squre rootを30の言語で書いてみたので、それぞれのコードを紹介したいと思います。

なお、以下のコードは全てGitHubに公開しています。

Fast inverse square rootアルゴリズム

まずC言語による実装を見てみましょう。

#include <stdio.h>#include <stdlib.h>float fastInvSqrt(float x) {
  int i = *(int*)&x;
  i = 0x5f3759df - (i >> 1);
  float y = *(float*)&i;
  return y * (1.5F - 0.5F * x * y * y);
}

int main(void) {
  char *line = NULL, *endptr = NULL;
  size_t size;
  float x;
  while (getline(&line, &size, stdin) != -1) {
    x = strtof(line, &endptr);
    if (*endptr == '\n') {
      printf("%f\n", fastInvSqrt(x));
    }
  }
  return0;
}

32bit浮動小数点数平方根の逆数の近似を求めるアルゴリズムです。

 \displaystyle f(x) = \frac 1 {\sqrt x}

あるベクトルからその方向の単位ベクトルを求める (各要素の二乗和の平方根の逆数が必要になる) ために、かつて実際に製品で使われたことがあるそうです。 あくまで近似値ではありますが、およそ0.3%未満の誤差で求まります。

アルゴリズムのミソは、floatintのpointer castingにあります。

int i = *(int*)&x; // float xfloat y = *(float*)&i; // int i

この変換によって xi (yi) はどういう関係にあるのでしょうか。

32bitの浮動小数点数は、下のbitから23bitの仮数部と、127のゲタを履かせた8bitの指数部と1bitの符号部から成ります。 従って、正の32bit浮動小数点数xと、そのビット列を32bitの整数値として解釈した値 I_xは、次の近似式が成り立ちます。

 I_x \sim 2^{23} (\log_2 x + 127 - \sigma) \quad (\sigma \sim 0.045)

特別なケースとしてxが2の冪である場合 (仮数部 = 0) は、近似なしに次のようになります。

 I_x = 2^{23} (\log_2 x + 127)

例えば、floatの16.0をintにpointer castingすると 2^{23} \times 131 = 1098907648、0.125をintにすると 2^{23} \times 124 = 1040187392のようになります。

#include <assert.h>#include <stdio.h>int main(void) {
  int i, j = 30, passed = 0;
  float x = (float)(1<< j);
  while (j >= -30) {
    i = *(int*)&x;
    assert(i == ((127 + j) << 23));
    x /= 2; j--; passed++;
  }
  printf("%d test passed\n", passed);
  return0;
}

求めたい値を  z = x^{-&frac12;}とすると、  \log_2 z = -(&frac12;) \log_2 xとなりますので、次の関係式が得られます。

 \displaystyle I_z \sim \frac 3 2 2^{23} (127 - \sigma) - \frac {I_x} 2

これが i = 0x5f3759df - (i >> 1);の行に対応します。 この値をまたpointer castingでfloatに戻すことで、最初の近似値が得られます。 さらに、ニュートン法のiterationを一回行うことで精度を高めています。

更に詳しい説明は、いくらでも文献はでてきますので、そちらをご参照ください。 https://www.google.com/search?q=Fast+inverse+square+roothttps://en.wikipedia.org/wiki/Fast_inverse_square_root

さて、あらゆる言語で一つのアルゴリズムを書こうとなった時に、私はなぜこのアルゴリズムを選んだのでしょうか。以下の理由があげられます。

  • コードが小さすぎず大きすぎない。ただHello worldするだけならチュートリアルの最初を見ただけで終わってしまい、面白くありません。一方で、問題が難しいと様々な言語で書いている途中で挫折して投げ出してしまいそうです。
  • floatintのpointer castingがアルゴリズムのポイントです。これをどう実装するか、初めて触る言語においていかに速く見つけるかというところが重要になってきます。pointerがない言語ではbinary表現を介して変換するわけですが、これもドキュメントをわりと探さないと見つかりません。この変換がちゃんとできないと、めちゃくちゃ答を吐くことになるでしょう。
  • なによりも、このアルゴリズムが好きだから。浮動小数点がどう表わされるかを実にうまく使っています。最初に気がついた人は頭がいいなぁと思います。そして、今やもう何の役にも立たないという儚さにも心惹かれるところです。
  • ほとんどの言語において、このアルゴリズムを書いた人間はおそらく自分が初めてで (マイナーなアルゴリズムだしスクリプト言語で書いても無意味)、前人未踏の地を制覇していく感覚が楽しい。無意味なことはなぜか楽しい。

改めて、問題のレギュレーションをプログラミングコンテスト風に並べてみます。

  • 正の浮動小数点の数が改行区切りで与えられるので、その平方根の逆数を改行区切りで出力せよ。
  • float⇔int32のFast inverse square rootアルゴリズムを用いること。
  • seq 10000000を入力としても動作すること。
  • 数値として不正な入力は無視して良いが、例外などで落ちないようになっていると好ましい。
  • 許される誤差は、真値に対して0.5%未満とする。

サンプル入力

100
10
1
0.1
0.01
0.0025

サンプル出力

0.099845
0.315686
0.998307
3.157232
9.982522
19.965044

さあ、問題設定ができました。 他の言語でこの問題を解くとどうなるのか、違いを楽しみながらご覧ください。

C++

#include <iostream>#include <string>float fastInvSqrt(float x) {
  int i = *reinterpret_cast<int*>(&x);
  i = 0x5f3759df - (i >> 1);
  float y = *reinterpret_cast<float*>(&i);
  return y * (1.5F - 0.5F * x * y * y);
}

int main() {
  std::string line;
  while (std::getline(std::cin, line)) {
    try {
      std::cout << fastInvSqrt(std::stof(line, NULL)) << std::endl;
    } catch (...) {}
  }
  return0;
}

C++には4つのcast演算子がありますが、今回は最も危険なcastであるreinterpret_castを使わざるを得ない場面です。 また、文字列からfloatへの変換にはstr::stofを使ってみました。 この関数は、入力が正しくないと例外を吐きます。

C#

using System;

class FastInvSqrt
{

    publicstaticvoid Main()
    {
        string line;
        float number;
        while ((line = Console.ReadLine()) != null)
        {
            if (float.TryParse(line, out number))
            {
                Console.WriteLine(fastInvSqrt(number));
            }
        }
    }

    unsafepublicstaticfloat fastInvSqrt(float x)
    {
        int i = *(int*)&x;
        i = 0x5f3759df - (i >> 1);
        float y = *(float*)&i;
        return y * (1.5F - 0.5F * x * y * y);
    }

}

Microsoftによって開発された.NET系言語です。 Cの関数にunsafeキーワードをつけるとそのまま動きました。 float.TryParseが変換できたかを例外ではなく返り値で扱えるのは素敵です。 参照渡しを表すrefキーワードがあることも、言語として面白いところです。

Visual Basic

Imports System
Imports System.IO

Public Class FastInvSqrt

    Shared Sub Main(args AsString())Dim s AsString= Console.ReadLine()Dim x AsSingleWhile s IsNot NothingIfSingle.TryParse(s, x)
                Console.WriteLine(fastInvSqrt(x))EndIf
            s = Console.ReadLine()EndWhileEndSub

    Shared Function fastInvSqrt(x AsSingle)AsSingleDim i AsInteger= BitConverter.ToInt32(BitConverter.GetBytes(x),0)
        i =&H5f3759df -(i >>1)Dim y AsSingle= BitConverter.ToSingle(BitConverter.GetBytes(i),0)Return y *(1.5-0.5* x * y * y)EndFunctionEnd Class

.NET系言語です。 BitConverterを使って数値の変換を行ってみました。 Main関数はC#とほぼ同じです。 DimキーワードやEnd Subなどが懐かしい感じがします。 16進数表現リテラル&Hという表記はユニークです。

F#

letfastinvsqrt(x: float32): float32=leti= System.BitConverter.ToInt32(System.BitConverter.GetBytes(x), 0)letj=0x5f3759df - (i >>>1)lety= System.BitConverter.ToSingle(System.BitConverter.GetBytes(j), 0)
    y *(1.5F - 0.5F * x * y * y)seq{whiletruedoyieldstdin.ReadLine()}|>Seq.takeWhile ((<>)null)|>Seq.iter (fun x ->tryfloat32 x |> fastinvsqrt |>printfn"%f"with :? System.FormatException ->())

.NETの言語が続きます。 BitConverterを使っていることなど、fastinvsqrtの実装はVisual Basicとほぼ同じです。 floatが64bitの浮動小数点 (=double) を表すというのが少し違和感があります。 1.5と書くとfloat (=double) になりコンパイルエラーになるので1.5F (float32 = single) と書く必要があります。 例外のtry ... with :?や、pipe operator (|>) が特徴的です。

Perl

use strict;
use warnings;
use featureqw(say);

while (<>) {
  say fastInvSqrt($_);
}

sub fastInvSqrt {
  my ($x) = @_;
  my$i = unpack("l", pack("f", $x));
  $i = 0x5f3759df - ($i>> 1);
  my$y = unpack("f", pack("l", $i));
  $y * (1.5 - 0.5 * $x * $y * $y);
}

歴史のある言語ですが、テキスト処理が得意で、今なおwebアプリケーションに広く使われています。 pointerを扱えない言語では、pack/unpackによりbinary表現を介して実装することができます。 厳密にはpointer castingしていないコードはFast inverse square rootと言うべきではないかもしれませんが、それでは書ける言語がかなり限られてしまいますのでお許しください。

PHP

<?phpwhile($line=fgets(STDIN)){echo fastInvSqrt(floatval($line)), "\n";
}function fastInvSqrt($x){$i=unpack('l', pack('f', $x))[1];
  $i=0x5f3759df-($i>>1);
  $y=unpack('f', pack('l', $i))[1];
  return$y*(1.5-0.5*$x*$y*$y);
}?>

webアプリケーションに特化した言語。 Perlのコードを直しただけです。 フォーマット指定も同じです。

Ruby

deffastInvSqrt(x)
  i = [x].pack("e").unpack("i")[0]
  i = 0x5f3759df - (i >> 1)
  y = [i].pack("i").unpack("e")[0]
  y * (1.5 - 0.5 * x * y * y)
endif__FILE__ == $0STDIN.each_line do |line|
    p fastInvSqrt(line.to_f)
  endend

オブジェクト指向の言語で、webアプリケーションフレームワークRuby on Railsを中心として広く使われています。 Perl譲りのpack/unpackが使えるので、少し書き直すだけで動きました。 一行ずつ処理できるSTDIN.each_lineも素敵です。

Python

import struct
import sys

deffastInvSqrt(x):
    i = struct.unpack('>i', struct.pack('>f', x))[0]
    i = 0x5f3759df - (i >> 1);
    y = struct.unpack('>f', struct.pack('>i', i))[0]
    return y * (1.5 - 0.5 * x * y * y)

if __name__ == '__main__':
    for line initer(sys.stdin.readline, ""):
        print(fastInvSqrt(float(line)))

webアプリケーションから数式処理や機械学習まで幅広く使用されている言語。 やはりpack/unpackがあるので、binary表現を介して変換しました。 フォーマット文字に着目すると、一文字目の>< (他に @, =, !) などによりendianを指定する方法は、上の3つの言語は異なっています。

R

fastInvSqrt <-function(x){
  i <- readBin(writeBin(as.numeric(x), raw(), size=4),integer(), size=4)
  i <-0x5f3759df- bitwShiftR(i,1)
  y <- readBin(writeBin(as.integer(i), raw(), size=4),double(), size=4)
  y *(1.5-0.5* x * y * y)}

f <- file("stdin")
open(f)while(length(line <- readLines(f, n=1))>0){
  write(fastInvSqrt(as.numeric(line)), stdout())}

統計処理に広く用いられている言語です。 ポインタもありませんし、浮動小数点型に32bitと64bitの区別がないので苦労します。 readBindouble, size=4を指定すると32bitの数値として読み込んでくれます。 他のシステムが吐いたデータファイルを読み込むという需要はあるためでしょうか。 主にファイルとのやり取りに使う関数ですが、raw vectorを引数に取ることもできます。

JavaScript

require('readline').createInterface({
    input: process.stdin,
    output: process.null}).on('line', function(line) {
    console.log(fastInvSqrt(parseFloat(line)));
});

function fastInvSqrt(x) {var i = floatToUInt32(x);
    i = 0x5f3759df - (i >> 1);
    var y = uint32ToFloat(i);
    return y * (1.5 - 0.5 * x * y * y);
}function floatToUInt32(x) {var buf = new ArrayBuffer(4);
    new Float32Array(buf)[0] = x;
    returnnew Uint32Array(buf)[0];
}function uint32ToFloat(i) {var buf = new ArrayBuffer(4);
    new Uint32Array(buf)[0] = i;
    returnnew Float32Array(buf)[0];
}

ブラウザーで動くことはもちろん、JavaScriptCSSへのトランスパイラがJavaScriptで書かれていることもあり、web開発には欠かせない言語。 pointerなどもちろんなく、また数値にfloatとdoubleの区別はおろか、整数型と浮動小数点型の区別もなく、Rよりもさらに不利な状況です。 TypedArrayをうまく使うとなんとか実装することができました。 ArrayBufferを引数にしてUint32ArrayFloat32Arrayを作れるのがポイントです。

CoffeeScript

require 'readline'.createInterface
    input: process.stdin
    output: process.null
  .on 'line',(line)=>
    console.log fastInvSqrt parseFloat line

fastInvSqrt =(x)->
  i = floatToUInt32 x
  i =0x5f3759df-(i >>1)
  y = uint32ToFloat i
  y *(1.5-0.5* x * y * y)

floatToUInt32 =(x)->
  buf =newArrayBuffer4(newFloat32Array(buf))[0]= x
  newUint32Array(buf)[0]

uint32ToFloat =(i)->
  buf =newArrayBuffer4(newUint32Array(buf))[0]= i
  newFloat32Array(buf)[0]

JavaScriptに変換されるAltJSの一つで、JavaScript自体の構文にも大きな影響を与えました。 上のJavaScriptのコードが吐かれるように書くだけです。 (new Float32Array(buf))[0] = xnew Float32Array(buf)[0] = xと書けないのは残念なところです。

LiveScript

require\readline
  .createInterface doinput:process.stdin
    output:process.null
  .on \line, (line) ->
    line |> parseFloat |> fastInvSqrt |> console.log

fastInvSqrt = (x) ->
  i = floatToUInt32xi = 0x5f3759df - (i .>>. 1)
  y = uint32ToFloatiy * (1.5 - 0.5 * x * y * y)

floatToUInt32 = (x) ->
  buf = newArrayBuffer4
  (newFloat32Arraybuf).0 = x
  (newUint32Arraybuf).0uint32ToFloat = (i) ->
  buf = newArrayBuffer4
  (newUint32Arraybuf).0 = i
  (newFloat32Arraybuf).0

CoffeeScriptとよく似ているAltJSですが、さらに先進的な機能が入っています。 バックスラッシュから始まる単語が文字列になったりする (\line == 'line') のは、他の言語では見たことがないsyntaxです (しいて言えばRubyのsymbolに似ています)。 pipe operator |>や composition operator >>など、F#に大きな影響を受けていることが分かります。 >>が関数合成なので、シフト演算子.>>.になっています。

PureScript

module Main whereimport Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, logShow)
import Control.Monad.Eff.Exception (EXCEPTION)
import Data.ArrayBuffer.ArrayBuffer (create)
import Data.ArrayBuffer.DataView (READER, WRITER, whole, getInt32, getFloat32, setInt32, setFloat32)
import Data.Int.Bits (shr)
import Data.Maybe (fromJust)
import Data.Monoid (mempty)
import Global (readFloat)
import Node.Process (stdin)
import Node.ReadLine (READLINE, setLineHandler, createInterface)
import Partial.Unsafe (unsafePartial)

main :: forall e. Eff (console :: CONSOLE, readline :: READLINE, err :: EXCEPTION, reader :: READER, writer :: WRITER | e) Unit
main =do
  interface <- createInterface stdin mempty
  setLineHandler interface $\line ->
    logShow =<< fastInvSqrt (readFloat line)

fastInvSqrt :: forall e. Number -> Eff (reader :: READER, writer :: WRITER | e) Number
fastInvSqrt x =do
  i <- floatToUInt32 x
  let j =0x5f3759df- unsafePartial i `shr`1
  y <- uint32ToFloat j
  pure $ y * (1.5-0.5* x * y * y)

floatToUInt32 :: forall e. Number -> Eff (reader :: READER, writer :: WRITER | e) Int
floatToUInt32 x =dolet dataView = whole $ create 4
  setFloat32 dataView x 0
  unsafePartial $ map fromJust $ getInt32 dataView 0

uint32ToFloat :: forall e. Int -> Eff (reader :: READER, writer :: WRITER | e) Number
uint32ToFloat i =dolet dataView = whole $ create 4
  setInt32 dataView i 0
  unsafePartial $ map fromJust $ getFloat32 dataView 0

Haskellに似たsyntaxを持つAltJSです。 JavaScriptコンパイルされる言語ですので、基本的にはJavaScriptのコードを直すだけなのですが、Haskellの経験がないと難しいでしょう。 Effmonadの型定義やPartial type classなど、Haskellと異なるところも多くあります。

J

fastInvSqrt =: 3 : 0
  i =. _2 ic 1 fc y
  i =. (dfh'5f3759df') - _1 (33 b.) i
  z =. _1 fc 2 ic i
  z * (1.5 - 0.5 * y * z * z)
)

echo & fastInvSqrt & (0 & ".) & (-. & CRLF) ;. 2 &. stdin ''
exit ''

書くのも読むのも難しい言語ですが、数日ほど家に籠もってドキュメントを読みこめばそれなりに分かるようになります (Vocabularyがとても便利)。 関数ではなく動詞と表現したり、動詞の挙動を変える関数を副詞と表現したりする言い回しはかなりユニーク。 演算子が常に右結合だったり、ic =: 3!:4fc =: 3!:5などによる数字とbinary列の変換、dfhによる16進数表現の変換、0 ". yによる文字列から数字への変換、3 : 0による動詞の定義など、上のたった数行のコードが書けるようになるまでに学ばなければならないことは多い。

Go

package main

import (
    "bufio""fmt""os""strconv""unsafe"
)

func fastInvSqrt(x float32) float32 {
    i := *(*int32)(unsafe.Pointer(&x))
    i = 0x5f3759df - i>>1
    y := *(*float32)(unsafe.Pointer(&i))
    return y * (1.5 - 0.5*x*y*y)
}

func main() {
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        if x, err := strconv.ParseFloat(scanner.Text(), 32); err == nil {
            fmt.Println(fastInvSqrt(float32(x)))
        }
    }
}

Googleが開発したコンパイル言語です。 ここまでしばらくスクリプト言語が続いていましたが、ここからコンパイル言語が続きます。 普通にpointer castingできるだけで安心感が出てきますね。 unsafe.Pointerという字面は、いかにも危険なことをやっているんだということを喚起してくれます。 strconv.ParseFloatの返り値の扱いにも見られるように、エラーは例外ではなく複数返り値で扱うのが慣習となっています。 標準のコードフォーマッターgofmtでコードを整形する慣習も、いい文化です。

Swift

funcfastInvSqrt(x:Float) ->Float {
    varz:Float= x
    vari:Int= UnsafeMutablePointer<Int>(withUnsafeMutablePointer(&z, { $0 })).memory
    i =0x5f3759df- (i >>1)
    lety:Float= UnsafeMutablePointer<Float>(withUnsafeMutablePointer(&i, { $0 })).memory
    return y * (1.5-0.5* x * y * y)
}

whileletline= readLine() {
    ifletx= Float(line) {
        print(fastInvSqrt(x))
    }
}

Appleが開発し、WWDC 2014で発表された言語です。 普通はこんな危なっかしいコード書かないでしょうけど、低レイヤーとのインターフェースも用意されており、ポインタにアクセスすることができます (たすかりました)。 if letwhile letなど、optional bindingは素敵だと思います。 変数 varと定数 letは区別されます。

Rust

usestd::io::BufRead;

fnfast_inv_sqrt(x: f32) ->f32 {
    let i =unsafe { *std::mem::transmute::<&f32, &i32>(&x) };
    let j =0x5f3759df- (i >>1);
    let y =unsafe { *std::mem::transmute::<&i32, &f32>(&j) };
    y * (1.5-0.5* x * y * y)
}

fnmain() {
    let stdin =std::io::stdin();
    for line in stdin.lock().lines().filter_map(|x| x.ok()) {
        match line.parse::<f32>() {
            Ok(x) =>println!("{}", fast_inv_sqrt(x)),
            Err(_) => {},
        }
    }
}

Mozillaにより開発されている言語です。 std::mem::transmuteという関数により、pointer castingを行うことができます。 unsafeキーワードはC#に少し似ています。 危険なことをしている感じがよくでています。

D

import std.conv;
import std.stdio;
import std.string;

void main()
{
    foreach (line; stdin.byLine())
    {
        try
        {
            writeln(fastInvSqrt(to!float(chomp(line))));
        }
        catch (ConvException e) {}
    }
}

float fastInvSqrt(float x)
{
    int i = *cast(int*)(&x);
    i = 0x5f3759df - (i >> 1);
    float y = *cast(float*)(&i);
    return y * (1.5F - 0.5F * x * y * y);
}

赤いDの文字の形をしたキャラクターが特徴的なコンパイル言語です。 綺麗なC/C++という印象です。 上のコードでは取り立てて特徴なところはありません。

Crystal

deffastinvsqrt(x : Float32) : Float32
  i = pointerof(x).as(Int32*).value
  i = 0x5f3759df - (i >> 1)
  y = pointerof(i).as(Float32*).value
  y * (1.5 - 0.5 * x * y * y)
endwhile line = gets
  puts fastinvsqrt(line.to_f32)
end

Rubyに似た構文を持つ、静的型付けのコンパイル言語です。 C言語との運用も考慮されていて、pointerを扱うインターフェースが用意されています。 このsyntaxは他の言語ではあまり見かけませんが、私はかなり好きですね。 C言語風の&やら*よりも読みやすい気がするのですが、どうでしょうか。 コード全体も引き締まっていてシンプルです。 コンパイル言語で、実行時のパフォーマンスも優秀です。

Nim

import strutils

procfastInvSqrt(x: float32): float32 =
  let i = cast[int32](x)
  let j = 0x5f3759df - i shr1let y = cast[float32](j)
  return y * (1.5 - 0.5 * x * y * y)

whiletrue:
  try:
    let input = stdin.readLinetry:
      echo fastInvSqrt(parseFloat(input))
    except ValueError:
      continueexcept IOError:
    break

こちらはPythonに似た構文を持つ、静的型付けの言語です。 この言語もCrystal同様、わりと低レイヤーが触れる言語です。 bit patternを保存するtype castはcast[T](v)のようにとてもすっきりしています。 あえてpointerでのcastであることを強調するために、let i = cast[ptr int32](addr(z))[]と書くこともできます。 addrが取れるのは変更可能な変数だけというのはSwiftに似ています。 pointer dereferenceがptr[]なのは他では見たことがありません (Cでは *ptr) が、配列と似ていてこちらのほうが一貫性があるように思えてきます。 Crystalと同様、実行時パフォーマンスは優秀です。

Java

import java.util.Scanner;

publicclass FastInvSqrt {

    publicstaticvoid main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            try {
                float x = Float.parseFloat(scanner.nextLine());
                System.out.printf("%f\n", fastInvSqrt(x));
            } catch (NumberFormatException e) {}
        }
    }

    publicstaticfloat fastInvSqrt(float x) {
        int i = Float.floatToRawIntBits(x);
        float y = Float.intBitsToFloat(0x5f3759df - (i >> 1));
        return y * (1.5F - 0.5F * x * y * y);
    }

}

業務系、webアプリケーション、スマホアプリ開発など、広く使われているオブジェクト指向の言語です。 生のpointerはありませんが、標準ライブラリの関数でfloatとintを変換する関数があるのでこれを用いることができます。 文字列からfloatへの変換関数Float.parseFloatが例外NumberFormatExceptionを投げるので、これをcatchする必要があります。

Groovy

Scanner scanner = new Scanner(System.in)
while (scanner.hasNext()) {
  try {
    println fastInvSqrt(Float.parseFloat(scanner.nextLine()))
  } catch (NumberFormatException e) {}
}

def fastInvSqrt(float x) {
  int i = Float.floatToRawIntBits(x)
  float y = Float.intBitsToFloat(0x5f3759df - (i >>1))
  y * (1.5F - 0.5F * x * y * y)
}

JVM上の言語でJavaと親和性が高い言語。 上記のコードもJavaのコードをそっくりそのまま持ってきたものです。 セミコロンやreturnを書かなくてよくてすっきりしています。

Kotlin

fun main(args: Array<String>) {
  while (true) {
    val line = readLine()
    if (line == null) breaktry {
      println(fastInvSqrt(line.toFloat()))
    } catch (e: NumberFormatException) {}
  }
}

fun fastInvSqrt(x: Float): Float {
  val i = java.lang.Float.floatToRawIntBits(x)
  val y = java.lang.Float.intBitsToFloat(0x5f3759df - (i shr 1))
  return y * (1.5F - 0.5F * x * y * y)
}

これまたJVM上で実装されていて、Javaとの相互運用が可能な言語です。 右ビットシフトがshr中置演算子なのがユニークです。 nullの扱いがSwiftと似ています。

Scala

object FastInvSqrt {

  def main(args: Array[String]) {
    Iterator.continually(scala.io.StdIn.readLine()).takeWhile(_ != null).foreach { line =>
      util.control.Exception.catching(classOf[NumberFormatException]) opt line.toFloat map { x =>
        println(fastInvSqrt(x))
      }
    }
  }

  def fastInvSqrt(x: Float): Float = {
    val i = java.lang.Float.floatToRawIntBits(x)
    val y = java.lang.Float.intBitsToFloat(0x5f3759df - (i >> 1))
    y * (1.5F - 0.5F * x * y * y)
  }

}

同じくJVM上の言語です。 上記のJVM系の言語と同様ですが、String#toFloatが例外を投げるので、これをcatchする必要があります。 セミコロンもreturnも不要です。 valvarがあります (他にKotlin, Swift, Rust, Nimなどがこの特徴を持ちます)。

Clojure

(defn fast-inv-sqrt [x](let[i (Float/floatToRawIntBits x)
        y (Float/intBitsToFloat (- 0x5f3759df(bit-shift-right i 1)))](* y (- 1.5 (* 0.5 x y y)))))(doseq[line (line-seq (java.io.BufferedReader. *in*))](try(println(fast-inv-sqrt (read-string line)))(catch java.lang.RuntimeException e ())))

JVM言語繋がりで、Clojureです。 Javaで書ければClojureで書くのも簡単です。 LISP系ですがletやdefnあたりの括弧の数が少なく、コードはすっきりしています。

OCaml

let fast_inv_sqrt x =let i =Int32.bits_of_float x inlet j =Int32.sub (Int32.of_int 0x5f3759df)(Int32.shift_right i 1)inlet y =Int32.float_of_bits j in
  y *. (1.5 -. 0.5*. x *. y *. y)let()=trywhiletruedolet line = input_line stdin intrylet x = float_of_string line inPrintf.printf "%f\n%!"(fast_inv_sqrt x)withFailure_->()done;withEnd_of_file->()

floatは64bitの数字ですが、Int32.bits_of_floatが32bitでのbinary表現にしたものをint32で返してくれます。 float上の演算子-.*.となっており、int上の演算子とは区別されています。 printf%!でfflushするのは、他の言語ではあまり見ない特徴です。

Haskell

import Control.Monad ((<=<))
import Data.Bits (shiftR)
import Data.Traversable (mapM)
import Data.Word (Word32)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (castPtr)
import Foreign.Storable (peek, poke)
import Prelude hiding (mapM)
import Text.Read (readMaybe)

main :: IO ()
main = mapM_ (mapM (print <=< fastInvSqrt) . readMaybe) . lines =<< getContents

fastInvSqrt :: Float -> IO Float
fastInvSqrt x =
  alloca $\ptr ->do
    poke ptr x
    i <- peek (castPtr ptr)
    poke (castPtr ptr) $0x5f3759df- (i :: Word32) `shiftR`1
    y <- peek ptr
    return $ y * (1.5-0.5* x * y * y)

遅延評価が特徴的な純粋関数型言語です。 メモリー領域を用意して、そこに書き込んで(poke)、別の型で読み取る(peek)という発想が必要です。 Float -> Word32とその逆で二回の変換が必要ですが、サイズが同じなので同じメモリー領域を使い回すことができます。 getContentsしてlinesで分割してそれぞれの行を… と書いたコードが巨大な入力にもクラッシュしないのは遅延評価のおかげです。

Erlang

-module(fastinvsqrt).
-export([main/0, fast_inv_sqrt/1]).

main() ->case io:get_line("") ofeof->ok;
    {error, _}->ok;
    Line->catch io:format("~f~n", [fast_inv_sqrt(parse_float(Line))]), main()
  end.

fast_inv_sqrt(X) -><<I:32/integer>>=<<X:32/float>>,
  J=16#5f3759df- (Ibsr1),
  <<Y:32/float>>=<<J:32/integer>>,
  Y* (1.5-0.5*X*Y*Y).

parse_float(Str) ->case string:to_float(Str) of{error, _}->case string:to_integer(Str) of{error, _}->error;
        {Int, _}->float(Int)
      end;
    {Float, _}->Floatend.

並行処理を重視した動的型付けの関数型言語です。 このエントリーのコードの中で、動いた時の喜びが最も大きかったコードです。 << ... >>はバイナリー列を表します。 バイナリー列のパターンマッチによりfloatintergerとの変換を行っています。 16進数表現リテラル16#という表記、io:formatのフォーマット指定~f~n、ビットシフトのbsrなどはユニークです。 string:to_float("100")がエラーを返すのが残念です (いい方法があれば教えてください)。 カンマやセミコロンやドットなどが最初は難しい。

Elixir

defmoduleMaindodefmain() docaseIO.gets ""do:eof->:ok
      {:error, _} ->:ok
      line ->caseFloat.parse(line) do:error->:ok
          {float, _} ->IO.puts FastInvSqrt.fast_inv_sqrt(float)
        end
        main()
    endendenddefmoduleFastInvSqrtdouseBitwise, only_operators:truedeffast_inv_sqrt(x) do<<i::size(32)-integer>>=<<x::float-32>>
    j =0x5f3759df- (i >>>1)
    <<y::float-32>>=<<j::size(32)-integer>>
    y * (1.5-0.5* x * y * y)
  endend

Erlangの仮想環境上で動作する言語、Elixir。 Erlangのコードとほぼ同じ。 ビットシフトは>>>なのは、>>がバイナリー列の閉じ括弧で既に使われていたからでしょうか。 syntaxはRubyに似ていますが、Erlangから学ばないと書きにくい。

Scheme

(use binary.pack)(define(main args)(define(main-loop)(let((line (read-line)))(cond((not(eof-object? line))(let((x (string->number line)))(cond((not(eqv? x #f))(display(fast-inv-sqrt (*1.0 x)))(newline))))(main-loop)))))(main-loop)0)(define(fast-inv-sqrt x)(let*((i (car(unpack "l" :from-string (pack "f"(list x) :to-string? #t))))(i (- #x5f3759df (ash i -1)))(y (car(unpack "f" :from-string (pack "l"(list i) :to-string? #t)))))(* y (-1.5(*0.5 x y y)))))

LISP系の言語の一つです。 歴史が長く登場したのは1975年ですが、今でも教育の現場で使われています。 私も大学時代に触ったのを思い出しました。 binary.packライブラリーがあって助かりました。

Assembly

さて、せっかくここまで様々な言語で書いてきたわけですが、以上のコードは平方根の逆数を高速に求めるという目的において、もはや意味がありません。 X86の命令にrsqrtssという、まさに実装したいことそのままの命令があり、上記のあらゆるコードよりも速い (はずだ) からです。

fmt0: .string"%f"
fmt1:    .string"%f\n"

.globlmainmain:
    pushl    %ebpmovl %esp, %ebpandl $-16, %espsubl $32, %espjmp.L4.L5:
    movl28(%esp), %eaxmovl %eax, (%esp)
    callfastInvSqrtfstpl4(%esp)
    movl $fmt1, (%esp)
    callprintf.L4:
    leal28(%esp), %eaxmovl %eax, 4(%esp)
    movl $fmt0, (%esp)
    call__isoc99_scanfcmpl $-1, %eaxjne.L5movl $0, %eaxleaveretfastInvSqrt:
    pushl    %ebpmovl %esp, %ebpsubl $8, %espmovd8(%ebp), %xmm0rsqrtps  %xmm0, %xmm0movd %xmm0, -4(%ebp)
    flds -4(%ebp)
    leaveret

私はアセンブリを書くのは慣れておらず、C言語のコードをgcc -Sしたコードを元に書いてみたので変なところがあるかもしれません。 ただ注目して欲しいのはこの一行です。

rsqrtps   %xmm0, %xmm0

この一命令によって平方根の逆数の近似値を求めています。 憶測に過ぎませんが、速さが勝負のゲームエンジンの実装では、この命令への最適化が使われていることでしょう。

ちなみにCでの実装をgcc -Sしたものは次のような感じでした。

f0:   .float    0.5f1:  .float1.5fastInvSqrt:
    pushl    %ebpmovl %esp, %ebpsubl $16, %espleal8(%ebp), %eaxmovl (%eax), %eaxsarl %eaxmovl %eax, %edxmovl $1597463007, %eaxsubl %edx, %eaxmovl %eax, -8(%ebp)
    leal -8(%ebp), %eaxmovl (%eax), %eaxmovl %eax, -4(%ebp)
    flds8(%ebp)
    fldsf0fmulp    %st, %st(1)
    fmuls    -4(%ebp)
    fmuls    -4(%ebp)
    fldsf1fsubp    %st, %st(1)
    fmuls    -4(%ebp)
    leaveret

lealしてmovlしているところが面白いところです。

まとめ

30個のプログラミング言語でFast inverse square rootというアルゴリズムを実装してみました。 コードは全て以下のリポジトリからcloneできます。 この言語がないじゃないか!とかいうツッコミがありそうですが、許してください。 SmalltalkLua, Cleanなどは書こうとしましたが、float⇔int32の変換をどうすればよいか分かりませんでした。 AdaやDelphi, Hexe, Io, Miranda, Tcl, Racket, Pony, Forthなど、まだまだ書きたい言語は残っているので、時間があるときに書いていこうと思います。

自分が書いたことがない言語の処理系をインストールし、目的のアルゴリズムをいかに速く実装するかというのは、さながらスポーツのようです。 このエントリーのために色々調べていたら、最初のCの実装を書いてから二か月くらいかかってしまいました。 単純に30で割ると2日に1言語ですが、一時間ほどで書けるものもあれば、数日間ドキュメントを読まないと書けないものもありました。 長く孤独な戦いでした。 それでも、自分が初めて触る言語で動いた瞬間はとても嬉しく、それが何度も何度も起きるわけですから、本当にこのチャレンジは楽しかったです。

どんな言語にも、得意不得意ががあります。 どんな言語にも、良いところと悪いところがあります。 素晴らしい型システムの代償がコンパイル時間だったり、実行時パフォーマンスの代償がメモリー領域に関する脆弱性だったりします。 歴史のある言語もあれば、歴史の浅い言語もあります。 様々な言語を研究した上で作られた言語もあれば、様々な言語に多大な影響を及ぼした言語もあります。

大事なことは、目的に沿って適切な道具を選ぶことです。 仕事で新しい挑戦ができなかったとしても、プログラマーとして生きていく上で、色々な言語の特性を知っておくことは大事だと思います。 少なくとも処理系を自分のラップトップに入れてコードを実行したり、ドキュメントを探しまわったという経験があるのとないのでは、言語に対する心理的障壁がだいぶ違ってきます。

ある問題を様々な言語で書いてみるという課題は、技術を深める第一歩に過ぎません。 次のステップは、これをきっかけに興味を持った言語を更に調べて身に付けるということになるでしょう。 私はこれまでHaskell, Scala, Go, JavaScriptといった言語を書いてきましたが、今回のチャレンジで新たにRust, OCaml, Erlang, Clojureなどにも興味を持つようになりました。 これまで書いてみようと思うことすらなかった言語です。 それぞれ活躍する場面が異なっており、これらを身につけることで、より多くの場面で適切な言語選択ができるようになれたらいいなぁと思っています。

あなたも色々な言語を触ってみませんか?九九表でもいいし、Project Eulerでもいいし、数式処理系でもいいし、JSONパーサーでも、数独ソルバーでも、HTTP echo serverでもいいと思います。大きすぎない問題を選ぶことと、一つの言語に時間をかけ過ぎないこと、30程度の言語 (あるいはそれ以上) で書いてみることが大事です。そして、公式のドキュメントを見ること、きちんと処理系をインストールして実行すること、できればテストを通すこと。実装言語が増えていき、全ての言語でテストが通ることはとても楽しいことです。

さあ、問題は設定できましたか?次はあなたの番ですよ。

素敵な言語との出会いがあらんこと

そして、あなたのブログ記事をお待ちしております。

あとがき ― 比較プログラミング言語学を夢見て

以前、moznionさんという方がYAPCAsia Hachioji 2016でのトークの感想エントリーとして次の記事を書かれていました。

やられた。 そう思いました。 このエントリーを見た時点で、fastinvsqrtも15言語くらいまで進んでいて、このブログ記事の用意を進めているところでした。

しかし、冷静になってみれば、一つのアルゴリズムをいろいろな言語で書きましょうというのは定期的に誰かが言っていることですし、慌てて出すよりも、もっと言語を増やしてからしっかり言語比較しつつ出したいという気持ちが高まり、公開するまでに時間が少しかかってしまいました。 20言語を超えたあたりから進捗が悪くなり、たった10行程度のコードを書くのに数日かかったりしていました。 実装しようと思って必死になったけど結局書けなかった言語もありました。 でも、あれもこれも自分にとっていい経験になったと思います。

自然言語の研究分野の一つに、比較言語学という分野があります。 民族の移動により言語は分裂し、また文化の交流により影響しあってきました。 関連している言語の文法や音韻を比較研究することで言語のルーツを辿るのが比較言語学の主たる目的です。 私は大学時代に一般教養の科目でこの分野の講義を受けて、謎解きみたいでとてもおもしろいと感じました。

あらゆる言語で一つのアルゴリズムを書いてみるというのは、スキルの向上や言語理解よりも、もっと深い意義があるように思います。 私はこのエントリーを用意する中で、「比較プログラミング言語学」というものを妄想しました。 言語は他の言語の影響を受けて生まれ、他の言語に影響を与えながら成長し、そして廃れていきます。 構文やキーワードから、演算子や関数名など細部を比較していくと、言語計者が影響を受けた言語や、言語の設計思想があらわになってきます。

なんか偉そうなこと書いてきましたが、似たようなこと言っている人は他にいないのかなと調べてみたら、以下の記事を見つけてしまいました。

流石です、西尾先生。 当時この記事を見たかどうか覚えていませんが、こういう風に言語を観察する事の面白さに、私はようやく気が付きましたよ! どんなに人に言われてもやろうとしなかったことでも、ふと始めたら面白くて夢中になってしまいました。 でも、人が成長するってそういうことでしょう?

エンジニア募集

はてなでは、自ら研鑽し技術を深め、あらゆる場面で適切な道具を使い分けられるようになりたい、そんな意欲的なエンジニアを募集しています。

Go言語でbase58コマンドを作りました

$
0
0

Go言語でbase58コマンドを書きました。コマンドラインツールだけではなくて、Goのパッケージとして作っています。

github.com

仕事の関係でbase58を扱うことがあり、base64コマンドのように素早くコマンドラインで変換できるツールが欲しくなったので作りました。ご自由にお使い下さい。

コマンドラインツールは次のように使うことができます。

 $ go get -u github.com/itchyny/base58-go/cmd/base58
 $ base58
100000000
9QwvW
79228162514264337593543950336
5QchsBFApWPVxyp9C
^D
 $ base58 --decode# or base58 -D
9QwvW
100000000
5QchsBFApWPVxyp9C
79228162514264337593543950336
^D
 $ echo 100000000 | base58
9QwvW
 $ echo 9QwvW | base58 --decode100000000
 $ echo 100000000 | base58 --encoding=bitcoin
9qXWw

既存のパッケージは出来が微妙でした。Go言語のbase58ライブラリはtv42/base58があるのですが、encodingがFlickr固定になっていたり、pull requestが何か月も放置されていたり、0から始まる数字の時の挙動がおかしかったりします。0から始まる数字の時の問題はパッケージのインターフェースの問題であり、簡単なpull requestで直せるものではなかったので、新しくパッケージを作らざるを得ませんでした。

引数は16進数のほうが良いのか、あるいはbig-endianでencodeされたbinary列のほうが良いのかとかいまいち分かってなくて、とりあえずコマンドラインツールとの結合がやりやすい10進表現のbyte列にしてしまいました。Bitcoinはよく分からないけど16進表現が使われているらしいので、要望があれば用意するかもしれません。

バグや機能要望はGitHubのissuesからお願いします。

CSVファイルをSQLのクエリで集計できるqコマンドをHaskellで実装してみました!

$
0
0

先日、Twitterでqコマンドが話題になっていました。 github.comスターが3000を超えていてすごいですね。2014年から開発されているツールで、Pythonで書かれています。

これはGoで実装してみたいなーと思っていたところ、mattnさんが素早く実装されていました。 mattn.kaoriya.net一本取られたと思ったものの、よく読むとまだ標準入力しか対応していないようです。

いったいどういう仕組みなのか、何の実装が難しいところなのか、qコマンドが嬉しい場面はどういうケースなのか、自分も知りたくなったので1から実装してみました。 私が一番素早く書ける言語ということでHaskellを選びました。

qhs

qコマンドのHaskell実装、ということでqhsと名づけました。 github.comstackが入っていればインストールは簡単です。

 $ git clone https://github.com/itchyny/qhs
 $ cd qhs
 $ stack install
 $ export PATH=$PATH:$HOME/.local/bin
 $ qhs "SELECT 100+200"300

基本的な使い方

qコマンドの基本思想は、CSVのようなファイルをSQLのテーブルとみなしてSELECTできることです。 qhsコマンドもqコマンドと同じように動きます。 SELECT * FROM [ファイル名]が基本の形です。

 $ cat basic.csv
a0,1,a2
b0,3,b2
c0,,c2
 $ qhs "SELECT * FROM basic.csv"
a0 1 a2
b0 3 b2
c0  c2

カラム名は自動的に c1, c2 ... のようになります。c2がNULLではない行だけにしてみます。

 $ qhs "SELECT * FROM basic.csv WHERE c2 IS NOT NULL"
a0 1 a2
b0 3 b2

数字の列の空文字はNULLになるようになっています。例えば、二行目の平均を取ることができます。

 $ qhs "SELECT avg(c2) FROM basic.csv"
2.0

1と3の平均なので2です。

一番上の行にカラム名がある時は、-H (--skip-header) 引数を与えてあげてください。

 $ cat basic.csv
foo,bar,baz
a0,1,a2
b0,3,b2
c0,,c2
 $ qhs -H"SELECT foo,baz FROM basic.csv"
a0 a2
b0 b2
c0 c2

普通のSQLと同じように、JOINしたりUNIONしたりすることもできます。 例えば、先月と先々月の家計簿CSVから高かった買い物トップ10を表示してみます。

 $ qhs -d , -O-H"SELECT * FROM 家計簿06.csv UNION SELECT * FROM 家計簿07.csv ORDER BY 金額 DESC LIMIT 10"日,時刻,金額,用途,場所
27,0000,66000,家賃,銀行振込
27,0000,66000,家賃,銀行振込
30,0000,8200,端末料金,サンプルコミュニケーション
30,0000,8200,端末料金,サンプルコミュニケーション
16,2200,7948,親ビールギフト,サンプルショッピング
5,1542,5690,ポロシャツ・ベルト他,サンプル洋服店
25,2245,5300,会社飲み会,いつもの飲み屋
2,2215,5000,同期と飲み会,四条の飲み屋
25,1913,3839,食料品,サンプルマート
3,1440,3740,散髪,サンプルサロン

とても便利ですね。そんなに大きな買い物はしてないことが分かって安心です (家計簿はサンプルです・私の実際の出費とは関係ありません)。

CSVファイルをSQLのテーブルのように扱い、JOIN・UNION・サブクエリによって集計する、これがqコマンドの真髄なのだと思います。 (mattnさん頑張ってください…)

標準入力

qコマンドやqqコマンド、そしてqhsコマンドは標準入力からテーブルを読み込むことができます。

 $ cat basic.csv
foo,bar,baz
a0,1,a2
b0,3,b2
c0,,c2
 $ cat basic.csv | qhs -H"SELECT foo,baz FROM - WHERE bar IS NOT NULL"
a0 a2
b0 b2

-は標準入力から読み込むことを表しています。

他のUNIXツールとの合わせて様々な集計を行うことができます。 例えばwcコマンドの行数でソートしたり

 $ wc * | qhs "SELECT c4,c1 FROM - WHERE c4 <> 'total' ORDER BY c1 DESC"
Main.hs 118
File.hs 66
Option.hs 61
Parser.hs 51
SQL.hs 45

psコマンドと合わせたりなど、組み合わせは自由自在です。

 $ ps -ef | qhs -H-O"SELECT UID,COUNT(*) cnt FROM - GROUP BY UID ORDER BY cnt DESC LIMIT 3"
UID cnt
503102086893

このpsの例はqコマンドのExampleから拾ってきたものです。 よく考えられていますね、脱帽しました。

実装基礎

qコマンドを自分の好きな言語で実装したいと考えたとします。 実装のイメージは湧きますか? SQL相当のクエリ実行器を作らないといけないのかと嫌な予感がよぎるかもしれませんが、そこまで苦労する必要はありません。

モリー上にデータベースを作り、ファイルを読み込んでテーブルを作り、そこにクエリを投げる、これが基本的な構成です。 これだけで理解できた人は以下は読まなくてよいでしょう。

qコマンドもmattnさんのqqコマンドもSQLite3のオンメモリデータベースを用いています。qhsもこれに倣ってSQLite3を使っています。初めて触るライブラリーでしたが、インターフェースが分かりやすいのですぐに使えました。

テーブルの構築

qコマンドの実装で最も難しいのはここだと思います。 qhsを実装するときも、かなり頭を捻りました。

例えば、次のような入力を考えます。

 $ qhs -d , -O-H"SELECT * FROM ./家計簿06.csv UNION SELECT * FROM 家計簿07.csv ORDER BY 金額 DESC LIMIT 10"

この「クエリ」自体は、SQLのクエリとして実行できるものではありません。 また、標準入力からの読み取りという簡単なクエリでさえ、通常のテーブル名としては不正なものになっています。

 $ wc * | qhs "SELECT * FROM -"

おそらくどんな言語でもSQLライブラリに突っ込んだらエラーになるでしょう。

まずは、この「クエリ」からファイル名を抽出します。 例えば ./家計簿06.csvとか 家計簿07.csvとか -のようなものです。 ファイルはテーブルに相当しますから、おそらくFROMJOINの後にくる単語がそうでしょう。 そして、このファイル名を有効なテーブル名に置換します。

SELECT * FROM temp_table_0 UNIONSELECT * FROM temp_table_1 ORDERBY金額 DESC LIMIT 10

テーブル名はなんでもよいです。 ただ、以下のことが大事です。

  • ファイル名との対応をきちんと持つこと (後で使う)
  • あるファイル名に対しては一意に定まること
  • SQLのテーブル名として正しいものであること

また、ファイル名と同じくらいの長さが好ましいと思います。 SQLのパーサーのエラーメッセージを元に戻してユーザーにフィードバックする時に、エラー位置がずれないようにするためです。

今回の例では、

"./家計簿06.csv" => temp_table_0
"家計簿07.csv" => temp_table_1

という対応が取れました。 標準入力のみの簡単なクエリでは、例えば次のような形になるでしょう。

"-" => temp_table_0

temp_table_の部分は何でも構いません。 qhsの実装では、ファイル名をSHA1エンコードしたものを使っています。 少しトリッキーですが、SHA1を使っておけば入力に対して安定します。

さて、次にやることは、このファイルたちを読み込んで、テーブルを作ることです。 コマンドの引数の区切り文字に従って、ファイルを読み込みます。 この段階でカラム名が決定しますので、CREATE TABLEできるようになります。 数字のカラムを自動判別して型を数字にしておくとか、gzipされたファイルを読み込むときにはdecodeするみたいな細かい芸はいろいろあります。

基本的には、一行ずつ読み込み区切り文字で分割していけばいいわけです。 ただしカラム数を超える分は分割してはいけません。 例えばps -efの結果があったとして

  UID   PID  PPID   C STIME   TTY           TIME CMD
    0100月10AM ??        40:25.50 /sbin/launchd
    04510月10AM ??         3:26.75 /usr/sbin/syslogd
    04610月10AM ??         1:15.17 /usr/libexec/UserEventAgent (System)04810月10AM ??         0:34.95 /usr/libexec/kextd
   555410月10AM ??         0:00.64 /System/Library/CoreServices/appleeventsd --server501148410月10AM ??       301:08.31 /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    01645110水10AM ??         0:00.08 /usr/libexec/syspolicyd
  50198979989770土02AM ttys013    0:00.14 -zsh

スペースによって分割しないといけませんが、CMDのカラムを必要以上に分割してはいけません。 もし Google Chromeがカラムで分割されてしまったら、次のような基本的なLIKE文も動かなくなるでしょう。

 $ ps -ef | qhs -H"SELECT * FROM - WHERE CMD LIKE '%Google Chrome%'"

UNIXツールの扱いやすいように見えてなんか扱いにくい出力には、いつも頭を悩まされます。 だからこそqコマンドも一定の支持を得られてきたのだと思います。

さらに、CSVはダブルクオートでセルが複数行にまたがることがあります。ダブルクオートの中のダブルクオートはダブルのダブルクオートで表現するらしいです。

 $ cat multiline.csv
foo,bar,baz,qux,quux
a0,1,"a2b0"",3,""b2c0",,c2
 $ qhs -d , -H-O"SELECT foo,bar,quux FROM multiline.csv"
foo,bar,quux
a0,1,c2

bar1baz"a2\nb0\",3,\"b2\nc0"qux""quuxc2なので、これで合っています。 めでたしです。 律儀にもqコマンドがこのケースに対応しているのでqhsでも対応してみました。 最悪です。

なんやかんやで入力を読み込むことができれば、一行ずつテーブルに流し込んでテーブルの準備は完了です。

クエリの実行と結果の表示

テーブルの準備ができれば、後はクエリを実行するだけです。 ここで言うクエリとは、ファイル名を置換した後の、ちゃんと実行できるクエリのことです。

qコマンドでは出力の区切り文字を指定することができます。qhsも同じインターフェスにしてみました。 例えば、入力ファイルはカンマ区切りだけど、出力ファイルはタブ区切りにしたいという時は次のようにします。

 $ qhs -d , -D$'\t'-H"SELECT * FROM basic.csv"
a0  1  a2
b0  3  b2
c0     c2

dはdelimiter (区切り文字) の頭文字です。小文字 -dが入力で、大文字 -Dが出力です。 いちいち $'\t'と書くのは面倒なので、 -Tを使うこともできます。 -T-D $'\t'は同じです。 -t-d $'\t'も同じです。

テスト

qhsはHaskellで書かれていますので、テストもHaskellのテストツールを使います。 HspecHaskellにおける標準的なテストフレームワークです。 Rspecにインスパイアを受けて作られたフレームワークです。

一行を特定のカラム数で分割する関数や、クエリのファイル名を置換する処理は難しいので丁寧にテストしています。 テストの中でファイルを読んだり書いたりすることも可能です。

コマンドラインツールですから、ビルド結果のコマンド自体が正しく動くことをテストするのも大事です。 テストのディレクトリーには、シェルスクリプトと、期待されるの出力ファイルを置いています。 これらをHspecの中で実行して、その出力と期待される出力ファイルを比較するというテストを書いています。 こうしておけばテストを追加するのも簡単ですし、最悪Hspecが滅びたとしてもシェルスクリプトを実行し出力を比較することができれば、どんなテストフレームワークでも生き残るはずです (いざとなったらシェルスクリプトでテストを回すこともできる)。 以前、Go言語で迷路コマンドを作った時も同じようなテスト構成を取りました。 itchyny.hatenablog.com

Travis CI上のstackでのテストはstackのドキュメントを参考にしています。

今回は、qコマンドという元の実装が存在します。 qコマンドのテストがかなり役に立ちました。 qコマンドのリポジトリにある112個のテストのコマンドをqhsに置き換えて、テストの実行と実装を繰り返しながらテストカバレージを上げていきました。 実装する必要があるのかよく分からないオプションがあったりするので、完全互換性を目指しているわけではありません (qコマンドの置き換えテストの中で通るのは4割ほどですが、これでもわりと通っている方だと思います)。

コマンドライン引数パーサー

Haskellにおけるコマンドラインの引数パーサーにはいろいろなライブラリーがあります。 今回は、optparse-applicativeを使ってみました。

Control.Applicativeの演算子と親和性がよくてコードが書きやすいです。 何よりも、オプションの性質を表す Mod f aという型が、Monoidのインスタンスになっていることがイケてます。 Monoidになっているということは、 (<>)で追加できますし、機能を足すのも引くのも簡単です (Haskellを書く時にMonoidのイメージはとても大事です)。 用途は思いつきませんが、オプションパーサーを動的に構築するのも簡単です。

おわりに

CSVファイルをSQLのクエリで集計できるqコマンドを、Haskellで実装してみました。 複数ファイル、標準入力、区切り文字の設定、gzip対応など、基本的な機能は揃っていると思います。 できるかぎり、オプション名などインターフェースはqコマンドに寄せています。 github.com

コメントやimport文を除くと300行程度です。 土曜日に「よし書こう」と思い立ってから丸一日で実装できるサイズでした。 やはりこの言語は楽しいなぁと感じました。 好きな言語は書いていて楽しいし、実装できたときの疲労感も心地よいものです。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

コマンドラインツールの作り方、SQLのクエリのパースの仕方、そしてDBの触り方など、qコマンドには興味深い課題が詰まっていました。 SQLのクエリの構文木からテーブル名を抜き出す処理には、sjspコマンドでもお世話になっているData.Genericsの知識が役に立ちました (ラフにテーブル名を置換した後、さらにバリデーションで厳格なSQLパーサーを通しています)。 itchyny.hatenablog.com構文木からの抽出や変換というタスクではData.Generics.Schemesの関数が驚異的な威力を発揮します。

DBのライブラリーを触ったり、optparse-applicativeを初めて触ってみたり、コマンドラインツールのテストについて改めて考えなおしてみたり、あるいはそのテストをTravis CI上で実行するまで持って行ったりと、いろいろと楽しいチャレンジだったなと思います。

さて、あなたの好きな言語は何ですか? 次は、あなたがqコマンドを好きな言語で書く番ですよ。

エンジニア募集

はてなでは、好きな言語はとことん好きだ!そんな情熱と愛情あふれるエンジニアを募集しています。

JavaScriptのsetTimeoutをログに出す

$
0
0

setTimeoutは難しい。いつ呼ばれるかよく分からないし、ライブラリーを使うとそのライブラリーがsetTimeoutを使いすぎてしまう。よく分からなかったけどsetTimeoutすると動くからそうしていた、んだけど実はタイミングの関係で偶然うまく動いているように見えているだけだった、なんてこともよくある。

ウェブアプリケーションの描画が遅い。「なぜか遅い」が、処理を丁寧に追っていっても手がかりがつかめないということがある。色々な方法を駆使した後に、なぜかsetTimeoutの発火が遅いということにたどり着いた。どれくらい遅いか。

window.setTimeout = (function(setTimeout) {returnfunction() {var handler = arguments[0];
    var wait = arguments[1] || 0;
    var log = function(action, color) {var d = newDate();
      console.log('%c%s%c [%c%d%c/%c%d%c] (%s %s) %s', 'color:' + color, action, '',
                  'color:blue', (Date.now() - registered), '', 'color:blue', wait, '',
                  d.toLocaleTimeString(), d.getMilliseconds(), handler.toString().slice(0, 100));
    };
    var args = [].slice.call(arguments, 2);
    var registered = Date.now();
    args.unshift(function() {
      log('FIRED', 'red');
      handler();
      log('DONE', 'blue');
    }, wait);
    log('REGISTERED', 'green');
    return setTimeout.apply(null, args);
  };
})(window.setTimeout);

setTimeoutをログに可視化してみた。コードの中で使っているあらゆるsetTimeoutのログが出力される。 [X/Y]みたいな表示がされるが、 YがsetTimeoutの第二引数で、 XがsetTimeout登録からの経過時間を表している。

f:id:itchyny:20160822124018p:plain

ああ、最悪だ。setTimeout 0のつもりが2秒も後に発火されている事がわかった。

本来ならば setTimeout(f, 500);REGISTERED [0/500]FIRED [500/500]DONE [530/500]のようになるのが正しい (f()に30msかかる時)。ブラウザー描画処理が入る可能性があるので、setTimeout 0でもFIREDが [20/0]とかになることはある。しかし、上の画像のように2秒も遅れていると「何かがおかしい」ということが分かる。

setTimeoutが可視化されたのは良いが、実際の原因は別のところにあることが多い。setTimeoutが意図しているよりも遅れるということは、そこで別の重い処理が走っているということになる。その処理とは、自分の書いたJavaScriptの処理だったり、大きなDOMの挿入、ブラウザーレンダリングだったりする。あとは、ブラウザーのTimelineやProfilesといった普通のデバッグ手法をとる。

console.logで時刻を出すというのは確かに原始的なやり方だ。printfデバッグと同じである。しかし、setTimeoutのタイミングが目に見えるのはとても便利だ。以前作ったsjspも同じように古典的な手法だが、今でも重宝している。

実行処理系のデバッガと、古典的な手法を行ったり来たりして原因を特定していくというのは、どの言語でもある気がする。どちらも良いところと悪いところがある。デバッグ技・プロファイル技は職人芸になりがちだが、文章にして伝えにくいところもあるので、どうしても特定の人にスキルが偏ってしまう。うまく伝えていきたい。


Vim 8.0 リリース!

$
0
0

Vim 8.0 released!

Vim 8.0が先ほどリリースされました。10年ぶりのVimのメジャーバージョンアップです。

Vimのバージョンをcronで毎日上げ続け、最新のパッチを確認し続ける日々を送ってきました。そして、今日も夜11時のcronでバージョンが上がりました。新しいメジャーバージョン、8.0でした。

ここ一年はVimにとって様々な重要な機能が入りました。JSONエンコーダーとパーサー、パッケージ機構、channelとjob、タイマー、ラムダ式など、プラグイン製作者にとって大事な機能ばかりです。今後、より高度なプラグインがでてくることでしょう。これらの機能に対する日本人の貢献は素晴らしいものです。

リポジトリGoogle codeからGithubに移動するという重要な決定も行われました。この決定の過程にも、vim-jpの皆さんが深く関わっています。私は傍から応援することしかできませんでしたが、皆さんの貢献によりVimの開発が更に活発になっていくのを嬉しく思いました。

新機能のうちすぐにユーザーが設定したり使用したりできる機能もたくさんあります。 set breakindentを設定しましょう、インデントのある長い行の折り返しの見た目が美しくなります。検索中に<C-g>, <C-t>を押してみましょう、カーソルはコマンドラインのまま、ハイライトを移動することができます。ビジュアルモードのg<C-a>, g<C-x>で連番を作れるようになりました。便利な機能がいっぱいです。

個人的に嬉しかったのは、getwininfo()でlocation listかquickfix listか判別できるようになったことです。この2つはコマンドが全く違うのに、これまで正しく判別する方法がありませんでした。これからは自信を持って、これらを区別するコードを書くことができます。

また、最近入ったScalaのサポートも嬉しいものでした。私はVimScalaを書いています。プラグインを使えば何不自由なく書けるのですが、やはりVimに正式にScalaのサポートが入った (有名なScalaプラグインが本体に取り込まれた) ことは大事なことです。Scalaを書く道具を迷っている人に、胸を張ってVimという選択肢を提案することができます。

最新のパッチをずっと追いかけている人にとっては連続した道の通過点に過ぎません。8.0がリリースされた後も、矢継ぎ早に8.0.0001, 8.0.0002とパッチは上がり続けています。しかし、メジャーバージョンを上げるというBramの決意には、こんなにも素晴らしい新機能が入ったVimをもっと多くの人に使って欲しいという思いが感じられます。

私はVimを開発されている皆さんが好きです。Vimプラグインを書かれている皆さんが好きです。Vimを使っている皆さんが好きです。

そしてVimが大好きです。新時代の幕開けに、祝杯を。

スマホが割れた日

$
0
0

その瞬間は、前触れもなくやってきた。

いつものように、仕事帰りに烏丸御池の交差点で信号待ちをしていた。ちょうど赤に変わったタイミングで時間があったので、スマホでニュースを眺めていた。信号が切り替わり、そろそろ渡ろうとしてスマホをポケットにしまおうとした瞬間だった。左手と右手がいきなりぶつかり、右手の力が緩み、スマホは宙に舞った。

何が起きたのか自分でもわからなかった。はっと我に返ったら、スマホは硬いコンクリートに打ち付けられていた。

一体左手で何をしようとしたのだろうか、今となってはもう思い出せない。右手に痒みを感じて掻こうとしたのか、スマホを左手に持ち替えようとしたのか、あるいはゴミが入った目を掻こうとしたのか。両手が別の目的を持って動線を描いた結果、衝突してしまった。

精密機器がコンクリートにぶつかる瞬間、明らかによくない音がした。すぐに拾い上げて状況を確認したが、画面には無数のひびが入り、悲惨なものだった。全くなんでもないですよ、最近のスマホは衝撃には強いんですよと、周囲の人に心配されないよう冷静を装いながら横断歩道を渡ったが、画面の割れたスマホを握る手は震えが止まらなかった。

家に帰ってから改めて詳細に観察してみた。画面はもう見るも無残な姿だった。インクを流して蝶の羽根に見立てるのも悪くないアイディアだ、それくらい無数のヒビが走っていた。しかし枠は意外と無傷だった。いや、ほんの少しの傷はついていたが、枠が地面に当たったとは思えなかった。おそらく画面を下にして落下したのだろう。何よりもの救いだったのが、画面が割れた以外は、動作に全く問題がなかったことだ。落ちた直後に拾い上げたときも電源はついたままだったし、画面が割れていてもタッチパネルの操作は通常通り行うことができた。ただ、画面から少し変な油の臭いがした。

暫くの間、様々な思いが頭を駆け巡った。二年間大事に扱ってきたものを、一瞬の不注意で落としてしまったことが悔しかった。あの瞬間、なぜ両手がぶつかってしまったのだろう。反射的に足で受け止めていれば衝撃は和らいだだろうか、本当にインクを流してやろうか、流石にこの見た目はお客さんに会うときには恥ずかしすぎるな、自分でネジを開けて交換して直そうか、細かい紙やすりで削ったらちょっとはマシになるのではないか、新しい機種が出ているわけだし自分で直してうまくいかなければ新しいものを買えばいいんじゃないか。

シャワーを浴びながら様々なことを考えたが、改めて机の上においている無残な画面を見て、餅は餅屋、根は張るかも知れないが専門家に任せるのが一番だと言い聞かせて、修理の予約を取った。なんだ、ジーニアスバーだなんて小洒落た名前をしているじゃないか。

快晴だった。阿蘇山の噴火のニュースを明け方まで聞いていて、よく眠れなかった。2万円までは払う覚悟で銀行でお金を下ろし、阪急電車に乗った。帰省の時くらいしか使わないから、夏の盆以来だった。土曜日の昼間にも関わらず、満席で立っている人も多かった。

いつもならスマホでニュースや技術ブログを眺めるところだったが、画面の割れたスマホを電車の中で見るのはなぜか自分が許さなかった。未成年での飲酒は流石に良くないんじゃないかと無粋なツッコミを入れながら、新海誠の作品を読み耽った。孤立感と憧憬、美しい女性たちの繊細な心情描写にすぐに惹き込まれていった。

梅田は久しぶりだった。いつも帰省のときには十三で折り返してしまうし、そもそも梅田に用事があるなんてことはなかった。もしかしたら就活でスカイビルに来て以来ではないか。そう思った瞬間、就活の時の様々な嫌な思い出が蘇り、慌てて心から追い出した。もうあの頃の自分とは違うんだ。

予約時刻までしばらく時間があったので、ヨドバシで暇を潰すことにした。京都でもそうなのだが、ヨドバシに入るとまず必ず地下一階に降りる。ラップトップなんてめったに買わないものだし相場感覚が分からなくなるものなので、たまにはこうしてスペックや価格を眺めて歩くのだ。SONYのラップトップを片手で持ち上げてみたり、こんな薄いものに未だにLANポートを作る技術に感心したり、いつまでも変わらないメモリー容量に落胆したりした。少し奥まったところに自作用のパーツも並んでいた。自作PCというのは作ったことないのだが、ラップトップのHDDをSSDに交換したときから、部品の価格にも目を向けるようになった。

京都のカメラコーナーは一階なのに、梅田は二階なんだなと呟きながらエスカレーターを登る。特に買いたいものはないのだが、唯一持っているNicon 1用のレンズは欲しくなることがある。財布に入っている現金で買えるのではないかという邪念を振り払い、カメラコーナーを後にする。途中で円偏光フィルターが目に止まった。大学で量子力学をやっていたものだから、偏光板や波長板などは研究室に身近にあった。円偏光を直線偏光にして偏光板を通して… あんなに一所懸命研究したのにだんだん記憶があやふやになっている。必死にBloch球に右円偏光と左円偏光を配置する。大丈夫だ、まだ思い出せる。このフロアの中でこのフィルターを量子現象として説明できる者は他にはいまい。なぜか勝ち誇ったような気分でヨドバシを後にした。

f:id:itchyny:20161008135759j:plain

陽射しはいよいよ増していた。道端に銀杏が落ちていなければ、夏かと勘違いするような暑さだった。タワークレーンにレンズを向けてみたが、こんな陽射しの中じゃ新海誠にはならないな、そんなあたり前なことを思いながら南下した。地下鉄で6分だと書いていたので、徒歩でのんびり行くことにした。

f:id:itchyny:20161008141221j:plain

大阪の地理には疎い。神戸に生まれ育った身として三宮の商店街の店には多少詳しいのだが、なんせ大阪まではでてくる機会すらなかった。だから心斎橋とか難波とか言われても位置関係はわからないし、大阪城にはどうやって行けば辿り着けるのかも知らない。しかし、今日の目的地はとにかく御堂筋を歩いていけば着くはずだ。道路標識が、和歌山へと続く道だと主張していた。陽射しの中で歩いていると気分も明るくなり、和歌山には行ったことないな、いつか行ってみるか、そういう気分になった。

大江橋淀屋橋を超えると金融街に入る。銀行支店が道に並ぶ。伏見町はここにもあるのか、水が良いところには酒蔵があると聞くがここはどうなのだろうか、道修町というのはどうしゅうではなくどうしょなのか、難読だなぁとか、1ブロック渡るたびに新しい発見がある。

f:id:itchyny:20161008143044j:plain

本町通を超えて阪神高速をくぐると、また街の景色が変化する。フランク・ミュラーエルメス、スワロフスキー、オメガ、カルティエルイ・ヴィトン。畏れ多くも店の入口に店員が待機していて入ろうものなら扉を開けてくれる親切なお店が立ち並ぶ。夏なんかはあそこに立つのは暑いだろうなぁ。スーツくらいきちんと決めていつか行きたいものだな。そんな風に思って通り過ぎていると、短パンジーンズ麦わら帽子の兄ちゃんが彼女らしき人物を連れて出てきたりしてずっこけそうになる。

心斎橋はおそらく生まれて初めて来た場所だ。高級ブランド店の並ぶ北側よりも、心なしか南側のほうが活気があるようにみえる。人の交通をうまく交わしながら、目的の店へと足を運ぶ。

f:id:itchyny:20161008171021j:plain

店内は混雑していた。店員の数と客の数が1:1くらいなのではと思うくらい、店員がたくさん働いていた。それでもどの店員に声をかけたらいいものかよく分からず、しばらくうろうろしていた。サポートは二階だと聞かされ、オシャレな螺旋階段を登った。

二階も同様に混雑していた。なぜか電源がつかないとかパスワードが分からないとか様々な事情でここに来るんだろう。一時間ほど歩いてきたので少し汗をかいており、座るのが憚られた。しかし、予約時刻を過ぎてもなかなか呼ばれず、貧血で倒れそうになり、仕方なく隅の方に座らせてもらった。

しばらく観察していたが、様々な人が働いていた。男性で長髪の人も、背が高い人も、韓国語や中国語が流暢な人も、足の不自由そうなスタッフもいた。上はみなスタッフのシャツを着ていたが、下はジーンズが多かった。そういう企業文化なんだろうなぁ、いいことだと思った。それにしても少し混雑しすぎていないだろうか。

対応してくれたのは、自分と同じくらいの背丈の男性だった。どうやら3500円ほどで画面の交換をしてくれるらしい。2万円は覚悟していたから、かなりありがたかった。一時間後にできますと言われたので、再び街に散歩に出かけた。用を足したかったので東急ハンズに入り、申し訳程度にボールペンを購入した。一階はすっかりハロウィン模様だった。作りたてのポップコーンの臭いに吐き気を感じながら、その場を離れた。

心斎橋は東西に地下街が発達していた。もう夕方だが今日は朝食をとったきりだったし、一時間ほど歩いたので空腹だった。昼夜兼用にしてしっかり食べよう。学生の頃なら牛丼屋で済ませていたんだろうな。そんなことを考えながら、店を選ぶ。西の端から東の端はかなり距離があり、地下街を歩くだけでも疲れてしまった。地下街の方向と東西南北が数度ずれているのがとても気になった。

結局、東端にある洋食屋を選ぶことにした。少し照明を落としており、こじんまりとした店だった。セットを頼んでから、また新海誠を読み始めた。いい具合の照明と、店内にかかるショパンのワルツが、物語を趣深く装飾してくれて最高の気分だった。中華料理屋を選ばなかった自分を密かに褒め称えた。ハンバーグがとても美味しくて、どうやって作ったのか聞きたいくらいだった。ご飯はおかわりできると案内されたが、ガツガツ食べる気分でもなかったので遠慮してしまった。

人通り忙しない商店街を抜けて、また店まで戻ってきた。数分したら、修理されたスマホが出てきた。間違いなく自分のスマホだったし、それはもう見事だった。素人が開けるときにつけがちなドライバー傷もなかった。保険がなかったら1万を超えていたという。これを修理してくれた技術者は、経験を積み、腕の立つ、顧客の製品を修理するという責任を負うことができる人間に違いない。それは素晴らしい仕事だ。対応してくれた人に対価を払いたい。保険は対価報酬の関係を不確実なものにする。そんなわけの分からない思考も、御堂筋に吹く秋風に飛ばされて、心は澄み切っていった。

もう梅田まで歩く体力は残っていなかったので、帰りは御堂筋線に乗った。ホームボタンが少し固くなっていたり、タッチパネルの滑りが少し悪くなっていることも、部品が新品である証拠だった。あのヒビの入った画面とはもうおさらばなのだ。いつもは必死にタイムラインを消化するのだが、そんな気力もなかった。

阪急京都線に乗り座席につくと、疲れがどっと出てきた。対応してくれた店員の笑顔、一週間は予約がいっぱいだと聞かされて唖然としているおじさん、洋食屋で手をそっと添えてお釣りを渡してくれた店員、修理を待つために店内で座り込んでいた中国人たち、限られた時間内で修理しなければいけないという使命を負って仕事をしている技術者たち、高級宝石店の扉の向こうに立っていた執事のような人たち。色々な人生や考えを想像し、その立場に立って見える風景を思い描きながら、電車の心地よい揺れに誘われ、眠りに引き込まれていった。

git grepで仕事してる

$
0
0

私はコードを書く時に頻繁にgit grepを使っていて、一日に何回くらいgit grepを使っているのか気になったのでログを取ってみました。

2016 10/24 月: 61
2016 10/25 火: 36
2016 10/26 水: 19
2016 10/27 木: 80
2016 10/28 金: 51
2016 10/31 月: 96
2016 11/ 1 火: 47
2016 11/ 2 水: 53
2016 11/ 4 金: 84
2016 11/ 7 月: 56
2016 11/ 8 火: 33
2016 11/ 9 水: 19
2016 11/10 木: 71

これは私が会社のPCでエディタの中でgit grepしたログを集計したものです。 結構ばらつきはありますが、40〜50回くらい、多い日で100回近くgit grepしているということが分かりました。 仕事の時間を割って平均したら大体5〜10分に一回はgit grepしていることになります。 もちろんコンスタントに使っているわけではなく、コードを書く時は増えますしミーティングがあれば減ります。 ですから、会議の多い日や設計段階を考えている日などは少なくなります。

私はVimScala・TypeScript・Goなどを書いています。 例えば、サーバーサイドを触る時にはコードとテンプレートを行き来するためにgit grepしますし、フロントエンドのコードも大体サーバーサイドと似たような変数名でモデリングしているため、git grepで関連ファイルを開いています。 JSONのフィールド名でもgit grepしますし、サイトのこの部分を変更したいという時は、クラス名を拾ってgit grepしてhtmlファイルを開きます。 ファクタリングや変数名関数名の変更も、まず最初にgit grepし、全て変更してからコンパイルします (手元で継続的にコンパイルするのはつらい)。 昔はcexprを使ってquickfixに流していましたが、今はlexprでlocation listに流してファイルを開いています。

他の人のコードのレビューする時は、動作確認したあとに、変更箇所の関数が使われている場所を必ず見るようにします。 関数に限らず、オブジェクト名でも、CSSのクラス名でも、サイトのパスの一部でも、なんでもgit grepします。

ある一つのプロジェクトのコードを長いこと触っていると、大体のコードの見た目が頭に入ってしまいいます。 DBでgroup byして引くときのORMのコードパターンはこんな感じなので…みたいな曖昧な記憶でも、git grepで目的のコードを開けたりします。 他のケースとして、エラーメッセージが出た時には、そのメッセージでgrepしてメッセージIDを見つけて、さらにそのメッセージIDでgrepするという二段git grepが発動します。

外部ライブラリも大体手元に落としてきてgrepするようにしています。 自分たちのコードからctagsで外部ライブラリに飛び、さらにそのリポジトリの中でgit grepするということがよくあります。

とにかくgit grepに頼っているので、たぶん自分はgit grepで仕事しているんだと思います。

コードを書いたり読んだりすることばかりが仕事ではないので、単純には仕事量の尺度にはなりませんが、しばらくログを取ってみて集計してみるとおもしろいことが分かったりするかもしれません。ぜひログを仕込んで回数を調べてみて下さい。あなたは一日に何回くらいgit grepしていますか?

珍しいSHA1ハッシュを追い求めて

$
0
0

SHA1ハッシュってあるだろう?」

放課後、いつものように情報処理室に行くと、高山先輩が嬉しそうな顔でそう言った。

「ええ、SHA1、ありますね」

SHA1って何桁か覚えているかい?」

「えっと…」

一年下の後輩、岡村が口を開いた。

「50桁くらいはありましたっけ…?」

先輩はパソコンに向かって何かを打ちはじめた。

現在、情報部の部員は三人しかいない。部長の高山先輩と、二年の自分と、後輩の岡村だ。いや、正確に言うと、先輩の学年にはもう少しいたのだが、もうほとんど部室に来ることはなくなってしまった。無理もない、この季節になると先輩たちは受験勉強で忙しくなる。

「例えば、こういうふうに… 適当なSHA1をエディタで開いて…」

echo -n | openssl sha1 | vim -

部長だけは今も部活に来てこうやって色々なことを教えてくれている。本人曰く、普通に勉強したら受かるでしょ、らしい。そう言ってもあまり嫌味には聞こえないのが先輩のいいところだ。

「カラム数を見ると、40だ。40桁だね」

「そうなんですね」

後輩の岡村は身を乗り出して先輩の端末操作を観察している。

「正確には160桁ですよね…」

僕は思わず口を開いた。

「そう、桁というより、160ビット、それを16進数で表現するから…」

先輩はそう言いながらプリント用紙の山から一枚手にとって説明しはじめた。

「こうやって4ビットずつ区切って0からfで表現するから、160ビットのハッシュを40桁で表現できるんだ」

「なるほど〜」

岡村は指を折りながら2進数表現を確認している。岡村の理解が追いついていることを確認しながら先輩は口を開く。

「つまり、SHA1ハッシュは16文字40桁で表現される。つまり16の40乗種類考えられるわけだね。これはもちろん2の160乗と同じ」

そう言いながら先輩はプリント用紙に簡単な式を書く。

「えっと、0.3かけると… これくらいかな…」

16^{40} = 2^{160} \simeq 10^{48}

「10の48乗…たくさんありますね…」

「そうだね、そこそこ多い」

先輩はシャーペンを顔の前で振りながらこう言った。

「そこでだ、SHA1ハッシュで、全てが数字になることはあるだろうか」

全て数字になるSHA1ハッシュ

情報部の活動は、いつもこんな感じで始まる。 誰かが問題を持ち寄って、それを下校時刻までにそれぞれ解く。 今日は部長の出題というわけだ。

空文字のSHA1ハッシュは da39a3ee5e6b4b0d3255bfef95601890afd80709だ (これはさっき先輩がやってみせたようにecho -n | openssl sha1すると表示される)。これにはdやらaやらeなど、沢山アルファベットが含まれている。 つまり、SHA1ハッシュの16進数表現が、全て数字になるような元メッセージを求めよという問題なのだ。

早速、自分も作業に取り掛かった。 まずどれくらい存在するかを考える。 16文字の中で10文字が数字なので… と言いながらブラウザーのアドレスバーに (10/16)^40と押してエンターを押した。

\displaystyle \left(\frac{10}{16}\right)^{40} = 6.84 \cdots \times {10}^{-9}

10の-9乗か… しかしSHA1ハッシュから直接元のメッセージを求めるのはできないので虱潰しに調べるしかない…

そんなことを考えながら、プログラムを書き始めた。 メッセージは適当にアルファベットと数字から作って、とにかくsha1ハッシュを計算して条件にあっていれば表示するだけのプログラムだ。 後輩の岡村の様子を見ると、どうやらSHA1ハッシュのアルゴリズムを確認しているようだった。 部長はシャーペンで何やら数式を書いている。

自分のプログラムは、しばらくして次のように出力しはじめた。

gflyP 9708825594493053358052040804954710052563
OqmSX 5405673447021682949913714302263814023323
pcBsd 0830855821698284247230340812332913765683
jPHSf 0747815384663891403181360803310055645039

おお、やはりあるのか…と思いながら、opensslコマンドでハッシュが間違っていないことを確認しながら、プログラムの出力を眺めていた。

「先輩、けっこうありますね…」

「おう、速いな」

「もう計算できちゃったんですか…」

岡村はシャーペンを机に放って、結果を見にやってきた。

「元メッセージはどう選んだんだい?」

先輩は自分が書いたコードを眺めながらそう言った。

「アルファベット大文字小文字もしくは数字です」

「なるほど…」

先輩は、シャーペンを手にとって何やら書き始めた。

「探したのは5桁のアルファベットもしくは数字のメッセージというわけだ。つまり62の5乗は…」

先輩はシャーペンを手に持ったまま、器用にアドレスバーに数式を入れる。

 62^{5} = 916132832

「およそ9億個ですね」

「ということは、そのうちSHA1が全て数字になるのは…」

岡村は必至に食いついてくる。

「9億に10/16の40乗をかけると…」

\displaystyle 62^{5} \times  \left(\frac{10}{16}\right)^{40} = 6.268 \cdots

「6個です。だいたい」

部長は、後輩の理解が追いついていることに満足そうに頷いた。 それは、自分のプログラムが6桁のメッセージの解を表示しはじめたのと、ほとんど同時だった。

gflyP 9708825594493053358052040804954710052563
OqmSX 5405673447021682949913714302263814023323
pcBsd 0830855821698284247230340812332913765683
jPHSf 0747815384663891403181360803310055645039
IL3rj 3243002591985408609566985352935152776909
MUeWp 8297833274599142233719359578426918109541
JG4Y80 0099530489181060830720140193389029330084
WiMtG0 2254364706744121285147874769100420502311
KMnsR0 4760110574803441139829661498017839123177
...

「つまり今考えているメッセージで5文字で、えっと」

部長がその言葉を遮ってこう続けた。

「アルファベット大文字小文字数字で5文字からなる元メッセージに対して、SHA1ハッシュが全て数字となるのは6つ、およそ期待値通りだね」

岡村も頷いた。

「でも、これに何の意味があるんですかね」

僕は思わず尋ねてしまった。

「意味というのは」

「いや、つまりですよ。SHA1というのは160ビットのバイナリー列って最初に言ってたじゃないですか。この16進数の表現は、なんというか、人間が勝手に4ビットずつ区切って勝手に0からfに割り当てただけじゃないですか」

「なるほど、たしかにそれはそうだね」

「つまり、SHA1ハッシュが数字だろうがアルファベットだろうがSHA1にとっては何の特徴にもならない気がするんです」

「なるほどね」

先輩は頷きながらそう言った。

「でも、それだからこそ、こうやって期待値通り、6個見つかったということにもなるね」

「そこなんですけど…」

岡村が怪訝そうな顔で口を開いた。

「それってつまり、16進数の表現の中で、文字がまんべんなく現れるということを仮定していますよね」

「たしかに」

「本当にそうなんですかね」

先輩はしばらく考えていたが、急に笑顔になって、こう言った。

SHA1の文字が本当に一様に分布するか、計算してみよう!」

その言葉と同時に、下校時刻を告げるチャイムが鳴った。

SHA1ハッシュの文字分布

家に帰ってから、改めて全て数字となるSHA1ハッシュの計算を再開した。 数字とアルファベット6文字の元メッセージは 62^{6} = 56800235584個あり、その中でSHA1が全て数字となるものは397個あった。 期待値は388.64個なので、少し多めに見つかっていることがわかった。 7文字の中でも計算しようとしたが、時間がかかってプログラムが終了しなかった (後日結果を見てみたら、24061個見つかった。期待値は24095.86個、その誤差は0.15%未満となった)。

夕食後、SHA1ハッシュの文字分布を集計するプログラムを組んだ。 元メッセージは先程の計算と同じように、アルファベット大文字小文字もしくは数字で選んだ。 元メッセージの長さを軸に集計すると、次のようになった。

|X|012345
05 (12.500%)133 (5.363%)9551 (6.212%)595152 (6.243%)36950623 (6.252%)2290283302 (6.250%)
11 (2.500%)157 (6.331%)9648 (6.275%)595439 (6.246%)36949237 (6.251%)2290285498 (6.250%)
21 (2.500%)139 (5.605%)9539 (6.204%)596209 (6.254%)36946427 (6.251%)2290302477 (6.250%)
33 (7.500%)140 (5.645%)9598 (6.242%)596671 (6.259%)36949064 (6.251%)2290259456 (6.250%)
41 (2.500%)164 (6.613%)9727 (6.326%)596720 (6.259%)36943267 (6.250%)2290339043 (6.250%)
54 (10.000%)143 (5.766%)9586 (6.234%)595214 (6.244%)36942983 (6.250%)2290318196 (6.250%)
62 (5.000%)175 (7.056%)9673 (6.291%)595193 (6.243%)36931008 (6.248%)2290360368 (6.250%)
71 (2.500%)163 (6.573%)9558 (6.216%)595012 (6.242%)36937662 (6.249%)2290455010 (6.250%)
82 (5.000%)149 (6.008%)9691 (6.303%)595441 (6.246%)36950679 (6.252%)2290411313 (6.250%)
94 (10.000%)172 (6.935%)9658 (6.281%)596341 (6.255%)36943415 (6.250%)2290385579 (6.250%)
a3 (7.500%)168 (6.774%)9797 (6.372%)596924 (6.262%)36936091 (6.249%)2290363615 (6.250%)
b3 (7.500%)131 (5.282%)9611 (6.251%)595164 (6.243%)36942624 (6.250%)2290301647 (6.250%)
c0 (0.000%)186 (7.500%)9417 (6.124%)596098 (6.253%)36928976 (6.248%)2290281914 (6.250%)
d3 (7.500%)176 (7.097%)9571 (6.225%)596065 (6.253%)36936884 (6.249%)2290307306 (6.250%)
e4 (10.000%)153 (6.169%)9567 (6.222%)595888 (6.251%)36930409 (6.248%)2290299939 (6.250%)
f3 (7.500%)131 (5.282%)9568 (6.223%)595589 (6.248%)36934091 (6.249%)2290358617 (6.250%)

右に行くほど、つまり沢山のSHA1ハッシュを調べるほど、全ての文字が均等に現れることがわかる。 ある特定の文字だけ沢山出現したり、少なかったりすることはなさそうだ。 この結果に満足して、僕は就寝についた。

SHA1SHA1がだいたい数字

「おお、たしかに収束しているようだね」

昨日の計算結果を見せると、先輩は満足そうにそう言った。

「100を16で割って6.25%ですね」

岡村も頷いている。

「まあ証明したわけじゃないが…」

先輩は軽く咳をしてから頭を掻いた。

SHA1の文字は一様に出現することを仮定して、いろいろな期待値をもとめてみよう」

そう言いながら、先輩はテキストファイルを順番に開きはじめた。

LvwEJe1 6051096335827944381165360280845795286423 111613f48230408056a71706b4174442640b6489
sG9wqMH 9788371029031493501751484666965269206614 e6279615888204167759744340b6c13236273b37
AIJsWgU 4833765415800580402215357543416053382134 823885871864d85103339b7110420321b64b7901
HFrIKpt 1515438875168251864313109613925545684403 820316645881e3020f35e48662206491972878c8
wc9U6dv 5898389906639826050665528530112945836398 5966347988220353b91383721534804a3a278c21

多くの数字が並んでいるが、一番右のSHA1には少しアルファベットが含まれているようだ。

「これはなんですか」

思わず僕は疑問を口にした。

「これは、SHA1ハッシュが全て数字となり、そしてそれをさらにSHA1ハッシュを取った時に、アルファベットが4文字以下となるものだ」

岡村は少し混乱した様子で紙を手に取った。

「つまり…まずは全てが数字で… さらにSHA1ハッシュを取ったら40文字の中で4文字以下がアルファベットとなるので、こうなりますね」

\displaystyle \left(\frac{10}{16}\right)^{40} \sum_{k=0}^{4} {40 \choose k} \left(\frac{6}{16}\right)^{k} \left(\frac{10}{16}\right)^{40-k} = 6.687 \times 10^{-13}

先輩は空中で指を振りながら、計算が間違っていないことを確かめながら口を開いた。

「そうそう、元メッセージは昨日と同様、アルファベットもしくは数字の7文字以下で探索したものだから… その数を掛けて…」

\displaystyle \cdots \times \sum_{i=0}^{7} {62}^{i} = 2.394\cdots

「期待値はこれくらいになる」

「あ、少し低くないですか」

岡村がすぐに指摘した。

「期待値が2.4で、見つかったのが5個ですか」

「まあこういうこともあるさ。次のファイルは」

HQC8su9 03003251839018941492807233288828427481a2 3395664741163489868183a7392270978a505005
VByKdTE 027985658547959903b826850439976465234220 1a3268963b445974667880579063493748834739

SHA1ハッシュと、そのさらにSHA1ハッシュをあわせた80文字の中で、アルファベットが3文字しか含まれないものだ。2つしか見つからなかった」

\displaystyle \sum_{k=0}^{3} {80 \choose k} \left(\frac{6}{16}\right)^{k} \left(\frac{10}{16}\right)^{80-k} \sum_{i=0}^{7} {62}^{i} = 3.173\cdots

「今度は期待値が少し高いんですね」

僕はファイルのSHA1を見つめ、目を凝らしてどこにアルファベットがあるのかを探しながらそう言った。

「でも、昨日考えていた、全てが数字になるものは期待値通りでしたよね」

「だいたいね」

「要するに、3個とか4個とかの時はあまり合わないけど」

「沢山見つかるほど、見つかった数と期待値の誤差が小さくなるということですね」

「それはまあ、そうだろうね」

岡村は自分のディレクトリに移動してテキストファイルを開いた。

「ところで僕も探索してみました」

ZDyHlIM 5954449490599416666154915409164008649984

そのファイルには一行しか書かれていなかった。

「これは?」

「全て数字なんですけど、7種類の数字しか含まれていないんです」

「なるほど…」

先輩も身を乗り出して後輩が見つけたSHA1ハッシュを眺めている。

「5と9と4と… 0と6、1、それから8で… 確かに7つの数字みたいだね」

「そうなんです。アルファベットと数字7文字以下の文字列のSHA1の中ではこの1つしか見つかりませんでした」

「貴重だね」

僕も感心して思わず軽く口笛を吹いた。

文字連続

先輩は、また計算結果が書かれたファイルを開いてみせた。

dGtWcS d6141925ca72293fba0b5f43000000000000f70b
9J2DJv8 a9fc511111111111125d3d87910d68ddfe350f24
QDi79qF c80b072b3f2c2b6b34cb404cfc2555555555555a
cE2sXYZ d59938cf4afe4effffffffffff6f8181ef568bc1
y1m5aTz b60a1e6666666666665f09a5d8a0c05eb382dca0

「これは、同じ文字が12個連続するところがあるSHA1ハッシュだ」

「おおお」

「それから、13個連続するのも1つ見つけることができた」

rcb0lQv 3333333333333455f30f9a9f761fb8e94b906a0f

「なんかすごい」

「つまり、12個以上連続するものがいま6個あるのだけど…」

そう言いながら先輩はシャーペンを持って期待値を計算しはじめた。

「連続している文字が16通りで、その位置が40-12+1通りだから…」

\displaystyle 16 \cdot (40 - 12 + 1) \left(\frac{1}{16}\right)^{12} \sum_{i=0}^{7} {62}^{i} = 5.900 \cdots

「だいたい期待値通りですね」

岡村は期待値の式を眺めながら数式の意味を追っている。

「他にもこんなものも見つかった」

先輩はそう言いながらファイルを開いた。

w5PPbh5 222222222220101b01d906e32e61373b45265361
bzajR5G 555555555558a91de2fb2fa98af5048bfd887c65
daycahL 2222222222281376525c8dbab5a18e3ca933d5c3
Q4dDYWa fffffffffffa03a907d9650eb91bc99642a86199
2o0XCwe 888888888886d55909a93a870fe44fd91aea5392

「先頭11文字が同じSHA1ハッシュだ」

「おもしろいですね」

GitHubのリンクで、こんなの見つけたらスクショを取りたくなりますね」

「そうそう、こんなSHA1はおもしろい、ただそれだけでいいと思うんだよね」

さらに珍しいSHA1を目指して

部室に夕日が差し込んでいる。まもなく下校時刻になる。

「色々な性質のSHA1ハッシュが集まりましたね」

岡村は感慨深くため息を付いた。

「そうだね。まず考えたのが全て数字、二回SHA1を取ったもの、それから岡村くんが見つけてくれた、7つの数字しか含まれていないもの、先輩が計算した連続文字…」

「いろいろあるね」

先輩はしばらく夕日を眺めてから、口を開いた。

「全て同じ文字となる確率はどれくらいかな」

岡村はアドレスバーに式を打ち込んで答えた。

\displaystyle 16 \cdot \left(\frac{1}{16}\right)^{40} = 1.095\cdots \times 10^{-47}

「10の-47乗ですね。だいたい」

「なるほど」

先輩は頷きながら暗算してこう続けた。

SHA1が全て0となる確率は、それを16で割ると、6、いや7かける10の-49乗くらいかな」

「そうですね」

岡村は式を調整しながら答えた。

「じゃあ、いくつSHA1を探したらどのくらいの確率で全て同じ文字のものを見つけられるかな」

先輩は少し意地悪そうにこう言った。

「えっと、さっきのを1から引いて… あれ」

どうやら検索エンジンの誤差で計算できないようだ。僕はすかさずWolframAlphaを開いて計算してみせた。

\displaystyle \left(1 - 16 \cdot \left(\frac{1}{16}\right)^{40}\right)^{10^{46}} = 0.8963\cdots

「10の46乗個探すと、10%の確率で見つかる計算になります」

「1秒に10の7乗個のSHA1を計算できるパソコンがあるとしよう」

岡村はすぐに計算して結果を答えた。

「およそ3かける10の31乗年かかりますね」

「宇宙ができてから確か138億年だよね」

僕がそう言うと、すぐに岡村は計算結果を割って答えた。

「宇宙が10の21乗個できちゃいますね」

「そりゃあ、大変だなぁ」

先輩が夕日を眺めながらそう言うと、下校のチャイムが鳴った。どのくらいの性質のSHA1なら現実的な時間で見つけられるか、そんなことを議論しながら、僕らは帰路に着いた。

2016年を振り返って

$
0
0

会社は二年目に入り業務にも慣れ、ある程度まとまった仕事を任せられるようになりました。 携わっているサービスのコードに詳しくなり、リファクタリングの方向性を示して改善を進めてきました。 難しい障害も乗り越えながら、引き継いだ手綱を何とか制御できるようになってきたという所感です。

今年は18記事書きました。特に反響の大きかったエントリーは次の3つの記事でした。 内容の方向性もバラバラであまり何したいかよく分からなくなっていますね。どういう技術を学んでいくか悩んでいた一年だったと思います。ブログには書いていませんが、Vimソースコードをいじったりmrubyのコードを読み込んだりしていた時期もありました。

一年に一つ言語を学べという教えを元に次に学ぶべき言語を模索するために様々な言語を触ってみるというチャレンジに取り組んだのですが、その後とくに新しい言語を触れていないのは反省しています。 少しRustのチュートリアルを進めましたが、実際に自分で何かを作ってみるところまでは進めませんでした。 観測範囲ではRustは徐々に人気が高まっているので来年には真面目に手を付けて扱えるようになりたいものです。

最近はVM関連の関心が高まっていて、インタープリタ言語の実装を調べているところです。 ソースコードだけを読んでいるとなかなか背景の技術まで理解できなかったりするのですが、mrubyに関しては最近出版された以下の本が参考になります。

mrubyのmrb_vm_execにあるoptableあたりのコードを最初見た時は何だこれはと思ったのですが、この書籍を読んでようやく意味がわかりました。 調べてみるとVMの実装では有名なテクニックらしいです。 来年は自分で言語を実装しながら色々と学びたいと思います。

Vimは今でももちろん毎日使っていますが、プラグインを制作したいという欲は沸かなくなっています。 既存のプラグインに対するissue報告に対処しながら、リファクタリングなどをのんびりやっていました。 特に、thumbnail.vimの大幅なリファクタリングは時間がかかりました。 thumbnail.vimは自主的に作った最初のプラグインですが、そのせいでコードは複雑に絡み合っており、冗長なtry catchや難解な再帰呼び出しを前に、長い間コードに手を付けられずにいました。 calendar.vimでの設計をベースに、絡み合ったコードを丁寧に解きほぐし、何とかきれいにまとめることができました。 設計のめちゃくちゃなコードを挙動を変えずにリファクタリングするのはとても大変ですね。

来年は、興味のあるVM実装周りの知識を深めながら手を動かして実装を進めていきたいと思っています。 また、良い言語だという予感はあるもののなかなかチュートリアルレベルを抜け出せていないRustを真面目に使ってCLIツールを作ってみたいと思っています。 HaskellScala、Go、JavaScriptそしてRustもスキルセットに揃うと、多くのシーンに柔軟に技術を選択できるのではないかなと思っています。

2016年もいい年でした。技術を深め研鑽しながら来年も頑張っていきましょう。

堤京介「終わらないよ。あきらめなければ、終わらない。俺は何度でも挑戦する。」

ef - a tale of memories.
Viewing all 233 articles
Browse latest View live