2015-03-30

コンパイラーを負かす

roguelazer's website: beating the compiler

なかなか面白かったので翻訳して紹介する。

たとえば、97%の場合において、僅かな効率など忘れるべきである。。早すぎる最適化は諸悪の根源である。とはいえ、残りの重要な3%の機会を逃すべからず。

-- Donald Knuth

計測せよ。計測するまで速度の最適化を施してはならぬ。たとえ計測したにせよ、一部のコードが残りを圧倒するまではまだ最適化してはならぬ。

Rob Pike

最新のWebサービスを主体とした技術の業界に長年浸かった我々は、パフォーマンスの問題を忘れがちである。SQLAlchemy ORMの中で行うリクエスト一つが8,9秒かかる中で、関数呼び出しひとつを3ミリ秒最適化したところで何になるというのか。とはいえ、時にはそのような最適化スキルを養っておくのもいいことだ。今回は、ある簡単な課題を最適化して、やっつけ仕事よりどれだけよくできるのかを見ていくことにする。普通はやらないだろうが。

課題

今回の課題はとても簡単なものだ。n個の整数を合計せよ。この課題を公平にするために、整数値は32bit符号付きintに収まるものとし、その合計も32bit符号付きintに収まるものとする。

もちろん、我々は最適化をするのであるから、パフォーマンスの計測には注意を払わねばならぬ。通常なら、我々は単なる時刻を使う(Pythonのtimeitモジュールなどで計測するものだ)。ただし、time.clock()をPythonからただ呼び出すだけで、30マイクロものコストがかかる。おやおや、これはあまりよくないぞ。言い換えてみよう。time.clock()をPythonで呼び出すと、最近のCPUでは60,000サイクル消費するのだ。かわりに、筆者はCPUサイクルをカウントする手法を用いるものとする。これには問題もある(特に、周波数スケーリングが有効になっていたり、複数のコアがあってOSのスケジューラーがリスケジュールを行いたくなった場合など)。だが、これはCPUのパフォーマンスの計測として、最も正確でオーバーヘッドの最小な単位である。Intel x86では、RDTSC命令で実装できる(より具体的に言うと、シリアライズされるRDTSCP命令だ)。筆者はこれを使いやすいようにPythonライブラリとしてラップした。

これがテスト計画だ。

  1. n個のされた範囲(0,\(2^{12}\))の整数値のデータファイルを乱数で生成する。固定値のseedで乱数生成する。
  2. テストは、まずデータをすべて読み込む
  3. テストは、RDTSC命令でサイクル数を計測する
  4. テストは、少なくとも100回実行する

この記事のテストは、Intel Core i7-4850HQを搭載した2013年の15インチのrMBPで行われた。また、Intel Core i5-4308Uを搭載した2014年の13インチ rMBPでも実行して、結果がまともであることを確かめた。

最初の試み:Python

Pythonは好きだ。簡単でダックタイピングな言語で、オブジェクト指向と関数型の便利な機能がたくさんあるプログラミング言語だ。ファイルからデータを読み込むのは簡単だ。

import array

data = array.array("i")
with open(sys.argv[1], "r") as f:
    for line in f:
        data.append(int(line.strip()))

最もやっつけな解決法がこれだ。

def sum_naive_python():
    result = 0
    for i in data:
        result += i
    return result

コードはあまりに自明だ。美しい。このコードは、筆者のコンピューターでは、50,000,000 ± 3%サイクルかかる。これは約24ミリ秒だ。だが、筆者は絶対時間についてはそれほど気にしていない。問題なのはスケールするかどうかだ。そこで筆者が使う指標は、1サイクルあたり何個処理できるかだ。理想的な単一命令実行の非スーパースカラーで完璧な分岐予測を持つCPUでは、サイクルあたり1.0個処理できるはずだ。この指標では、最初の試みは悲惨だ。0.01個/サイクルだ。整数の個数辺りのスケールは以下の通り。

naive python

とても簡単な改良方法がある。Pythonにはネイティブのsum関数があり、これはCで実装されていて、もっと速いはずだ。次のテスト関数はこうだ。

def sum_native_python():
    return sum(data)

これはマシになった。12,000,000 ± 2%サイクル、0.04個/サイクルだ。まだ悲惨だが、4倍速い。

native python

Python用のイケてる数値ライブラリを使うとどうなるだろうか。numpyはどうだ。このライブラリは最低聞かされたFORTRANコードを呼んでくれる。すげー速いはずだ。

numpy

ダメだ。NumPyのオーバーヘッドで相殺されるようだ。0.004個/サイクルしかでない(ネイティブsum関数を組み込みのarray型に適用する場合の10分の1だ)

より高みへ:C

Pythonでそれ以上速くできないのであれば、Cに変えろというのは、Pythonでよくあるパターンだ。Cで同様のテスト環境を用意した。ファイルからヒープに確保されたintの配列に読み込み(少々長いのでわざわざコードはみせない)。この数値群を足し合わせるC関数の最初の実験がこれだ。

int sum_simple(int* vec, size_t vecsize)
{
    int res = 0;
    for (int i = 0; i < vecsize; ++i) {
        res += vec[i];
    }
    return res;
}

最初のPythonコードと同じくらい綺麗だ。さて結果は?

3,423,072サイクル(~1.5ミリ秒)、0.162個/サイクルだ。これはPythonより10倍速い。

注意:Apple LLVM 6.0 / clang-600.0.56 -O0でコンパイルした。

C O0 Simple

もっと速くできるだろうか。

そういえば昔、ループの手動展開は高パフォーマンスであると聞いたことがある。ループを展開すると、ループの最後のジャンプのオーバーヘッドを隠して、分岐予測の為事を代わりに引き受けたことになる。4回展開版が以下だ。

int sum_unroll_4(int* vec, size_t vecsize)
{
    int res = 0;
    int i;
    for (i = 0; i < vecsize - 3; i+=4) {
        res += vec[i];
        res += vec[i+1];
        res += vec[i+2];
        res += vec[i+3];
    }
    for (; i < vecsize; ++i) {
        res += vec[i];
    }
    return res;
}

展開されたループのパフォーマンスはどうか。ほぼ同じだ(0.190個/サイクル)

C O0 Unroll 4

ひょっとしたら展開数が足りないのではないか。では、8回ループ展開をしてみよう。

int sum_unroll_8(int* vec, size_t vecsize)
{
    int res = 0;
    int i;
    for (i = 0; i < vecsize; i+=8) {
        res += vec[i];
        res += vec[i+1];
        res += vec[i+2];
        res += vec[i+3];
        res += vec[i+4];
        res += vec[i+5];
        res += vec[i+6];
        res += vec[i+7];
    }
    for (; i < vecsize; ++i) {
        res += vec[i];
    }
    return res;
}

やれやれ、0.192 PPC(Points Per Cycles)だ

C O0 Unroll 8

どうやら、最近のプロセッサーの分岐予測というのはめちゃめちゃ優秀らしい。ループ展開は、とても短いループか、あるいは何らかの理由で分岐予測が最悪の結果になるループには有効だが、まず時間の無駄というものだ。

最適化コンパイラー

Cを書いた経験がある読者は、ここで疑問に思っていることだろう。なぜ筆者は-O0でコンパイルしているのだ?、と。最新のコンパイラーは何十年もの最適化手法が積み上げられており、筆者ごときが書いた程度のものは余裕でこすことができるだろう、と。

実にそのとおりだ。-O3を使うと、精製されたコードは他の方法を消し炭すら残らないほどの煙にしてふっ飛ばしてしまう。4.462個/サイクルだ。

C O3 Simple

待てよ・・・1個/サイクル以上だと? どうやったらそんなことが可能なのだ。

さて、読者よ。プロセッサーの歴史のお勉強の時間だ。

複数命令発行、スーパースカラー、アウトオブオーダー、ベクトル計算機

今は昔、コンピュータープロセッサーというものは、とても簡単だった。プロセッサーは命令を一つづつ読み込み、1ないしは数サイクルを使って処理し、結果を吐き出すものだった。

simple architecture

これは、1965年にCray 6600がリリースされる以前の話で、デスクトップでも1995年にP6 Pentium Proが発売されたことで過去の話になった。この2つのプロセッサーは、スーパースカラーアーキテクチャの代表例だ。

簡単に言うと、スーパースカラーアーキテクチャーとは、複数の実行ユニット(大抵目的別に特化されている。たとえば2つの汎用数値演算ユニットといくつかのアドレス計算ユニットとひとつのSIMDユニットとひとつのFPUユニットなど)があり、複数の命令を並列に実行するために別々の実行ユニットに振り分けることができる。

superscalar architecture

筆者の持っているIntel Haswell CPUのアーキテクチャーは、以下のようになっている。

haswell

つまり、複数の命令を同時に実行できるということで、1.0加算/サイクルというのは、遅すぎるということだ。

最後の要素は、謎めいた略語であるSIMD(Single Instruction, Multiple Data)だ。SIMD命令はベクトル命令とも呼ばれていて、演算を複数のデータに対して一度に適用できる。例えば、IntelのSSEユニットでは、演算を4つの32bit整数値か、2つの64bit浮動小数点数に対して適用できる。

x86には多数のSIMDユニットがある。MMX拡張から始まり、SSE, SSE2, SSE3, SSE4.1, AVX, AVX2、そしてまだリリースされていないAVX-512。ここではその技術的詳細については述べないが、詳しく知りたければ、ここにすべて載っている

Haswellプロセッサーでは、2つのベクトルInt ALUがある、理論上、2つのAVX2処理を並列に行える。それぞれが8個の32bit整数値を同時に処理できる。Intel 64 and IA32 Architectures Optimization Reference Manualによれば、それぞれの命令は1サイクルのレイテンシーがあり、つまり理論上のPPCは16.0になる。だいぶ高い。実際、これは不可能なほどに高い。1サイクルごとに16個32bit整数値をCPUに転送するには、64バイト/サイクルのメモリースループットが必要であり、これは、筆者のCPUに当てはめれば、147GB/sのメモリースループットになる。筆者の環境のPC3-12800 RAMは、最大一本あたり12.8GB/s(トータルで25.6GB/s)であり、これはたったの11バイト/サイクルだ。つまり、メモリの最大のスループットを考えれば、2.75PPCをすこし上回るほどだ。これ以上高いスコアは、筆者のCPU上の128MBものeDRAM L4キャッシュに残っていたものである。

コンパイラーを負かす

さて、コンパイラーは負かせるか? 4.462 PPCより上を叩き出せるか。もちろん無理だが、データが広帯域のL3やL4キャッシュに乗るほど小さければ、まあ、可能ではある。いかがその例だ。PPCは4.808だ。

int sum_avx2_unroll4(int* vec, size_t vecsize)
{
    unsigned int vec_out[8] __attribute__ ((aligned(64)));
    size_t processed = 0;
    asm(
        "XORQ %%rax,%%rax\n\t"
        "VPXOR %%ymm0,%%ymm0,%%ymm0\n\t"
        "VPXOR %%ymm5,%%ymm5,%%ymm5\n\t"
        "VPXOR %%ymm6,%%ymm6,%%ymm6\n\t"
        "VPXOR %%ymm7,%%ymm7,%%ymm7\n\t"
        "VPXOR %%ymm8,%%ymm8,%%ymm8\n\t"
        "top%=:\n\t"
        "VMOVDQA (%1, %%rax, 4),%%ymm1\n\t"
        "VMOVDQA 32(%1, %%rax, 4),%%ymm2\n\t"
        "VMOVDQA 64(%1, %%rax, 4),%%ymm3\n\t"
        "VMOVDQA 96(%1, %%rax, 4),%%ymm4\n\t"
        "ADDQ $32,%%rax\n\t"
        "VPADDD %%ymm1,%%ymm5,%%ymm5\n\t"
        "VPADDD %%ymm2,%%ymm6,%%ymm6\n\t"
        "VPADDD %%ymm3,%%ymm7,%%ymm7\n\t"
        "VPADDD %%ymm4,%%ymm8,%%ymm8\n\t"
        "CMP %2,%%rax\n\t"
        "JL top%=\n\t"
        "VPADDD %%ymm5,%%ymm0,%%ymm0\n\t"
        "VPADDD %%ymm6,%%ymm0,%%ymm0\n\t"
        "VPADDD %%ymm7,%%ymm0,%%ymm0\n\t"
        "VPADDD %%ymm8,%%ymm0,%%ymm0\n\t"
        "VMOVDQA %%ymm0, 0(%3)\n\t"
        "MOVQ %%rax, %0\n\t"
        : "=r"(processed)
        : "r"(vec), "r"(vecsize), "r"(vec_out)
        : "rax", "ymm0", "ymm1", "ymm2", "ymm3", "ymm4", "ymm5", "ymm6", "ymm7", "ymm8", "cc"
    );
    int res = 0;
    for (int i = 0; i < 8 ; ++i) {
        res += vec_out[i];
    }
    for (; processed < vecsize; ++processed) {
        res += vec[processed];
    }
    return res;
}

これは4回展開したAVX2ベクトル化版だ。また、独立した複数の加算レジスターに合計させることにより、IPCを僅かに稼いでいる。

C O0 AVX2 Unroll4

本当にコンパイラーを打ち負かしたのだろうか。PPCの違いを比べてみる。

comparison

技術的に言えば、筆者のコードは負けることはない。参考に、これがコンパイラーの生成するアセンブリだ。


## BB#2:                                ## %vector.body.preheader
    pxor    %xmm0, %xmm0
    xorl    %ecx, %ecx
    pxor    %xmm1, %xmm1
    .align  4, 0x90
LBB8_3:                                 ## %vector.body
                                        ## =>This Inner Loop Header: Depth=1
    movdqa  %xmm1, %xmm2
    movdqa  %xmm0, %xmm3
    movdqu  (%rdi,%rcx,4), %xmm0
    movdqu  16(%rdi,%rcx,4), %xmm1
    paddd   %xmm3, %xmm0
    paddd   %xmm2, %xmm1
    addq    $8, %rcx
    cmpq    %rcx, %rax
    jne LBB8_3
## BB#4:
    movq    %rax, %rcx
LBB8_5:                                 ## %middle.block
    paddd   %xmm1, %xmm0
    movdqa  %xmm0, %xmm1
    movhlps %xmm1, %xmm1            ## xmm1 = xmm1[1,1]
    paddd   %xmm0, %xmm1
    phaddd  %xmm1, %xmm1
    movd    %xmm1, %eax
    cmpq    %rsi, %rcx
    je  LBB8_8
## BB#6:                                ## %.lr.ph.preheader13
    leaq    (%rdi,%rcx,4), %rdx
    subq    %rcx, %rsi
    .align  4, 0x90
LBB8_7:                                 ## %.lr.ph
                                        ## =>This Inner Loop Header: Depth=1
    addl    (%rdx), %eax
    addq    $4, %rdx
    decq    %rsi
    jne LBB8_7

コンパイラーは2回展開のSSE2レジスターを使う選択をしている。いえい。

結論

Pythonや、その他の動的型付け言語は、十分に速いかもしれないが、速いことは常にいいことだ。Cのような低級言語で速度を稼げるかもしれないし、アセンブリを手書きすることでもう少し上を行けるかもしれないが、コンパイラーを信頼したほうがいい。しかし、もし大量の12bitの数値を限りなく高速に足し合わせたいのであれば、まあ、これが答えだ。

comparison

Algorithm Cycles for 500k Points Points per Cycle
sum_numpy110681288 ± 0% Cycles0.005 ± 0% PPC
sum_naive_python48255863 ± 1% Cycles0.010 ± 1% PPC
sum_native_python12063768 ± 2% Cycles0.041 ± 2% PPC
-O0 simple3095247 ± 1% Cycles0.162 ± 1% PPC
-O2 simple2889994 ± 1% Cycles0.173 ± 1% PPC
-O0 unroll_42630305 ± 2% Cycles0.190 ± 2% PPC
-O0 unroll_82597596 ± 0% Cycles0.192 ± 0% PPC
-O2 unroll_82461759 ± 1% Cycles0.203 ± 1% PPC
-O2 unroll_42419625 ± 1% Cycles0.207 ± 1% PPC
-O2 unroll_4_asscom1134117 ± 1% Cycles0.441 ± 1% PPC
-O0 unroll_4_asscom1091086 ± 2% Cycles0.458 ± 2% PPC
-O3 unroll_4393973 ± 4% Cycles1.269 ± 4% PPC
-O3 unroll_8334532 ± 0% Cycles1.495 ± 0% PPC
-O0 asm_unroll_8266224 ± 3% Cycles1.878 ± 3% PPC
-O3 unroll_4_asscom254489 ± 1% Cycles1.965 ± 1% PPC
-O2 asm_unroll_8227831 ± 1% Cycles2.195 ± 1% PPC
-O3 asm_unroll_8227390 ± 0% Cycles2.199 ± 0% PPC
-O0 avx2_and_scalar_unroll149051 ± 0% Cycles3.355 ± 0% PPC
-O3 avx2_and_scalar_unroll146261 ± 0% Cycles3.419 ± 0% PPC
-O2 avx2_and_scalar_unroll144545 ± 0% Cycles3.459 ± 0% PPC
-O3 sse2125397 ± 2% Cycles3.987 ± 2% PPC
-O0 sse2123398 ± 0% Cycles4.052 ± 0% PPC
-O2 sse2119917 ± 0% Cycles4.170 ± 0% PPC
-O3 simple112045 ± 0% Cycles4.462 ± 0% PPC
-O0 avx2111489 ± 0% Cycles4.485 ± 0% PPC
-O2 avx2110661 ± 0% Cycles4.518 ± 0% PPC
-O3 avx2_unroll4_mem_address110342 ± 0% Cycles4.531 ± 0% PPC
-O3 avx2_unroll4109213 ± 1% Cycles4.578 ± 1% PPC
-O3 avx2107011 ± 0% Cycles4.672 ± 0% PPC
-O2 avx2_unroll4106253 ± 0% Cycles4.706 ± 0% PPC
-O2 avx2_unroll4_mem_address105458 ± 1% Cycles4.741 ± 1% PPC
-O0 avx2_unroll4103996 ± 0% Cycles4.808 ± 0% PPC
-O0 avx2_unroll4_mem_address101976 ± 0% Cycles4.903 ± 0% PPC

おめでとう。

2015-03-25

Ubuntu(Debian)でインストール済みのパッケージ一覧を得る方法

Ubuntuはインストール済みのソフトウェアを一覧表示することすらできない。 | ask.fm/EzoeRyou

すこし調べた結果、

dpkg --get-selections

で、全パッケージのインストールされたものと、インストールされたが除去されたものを得ることができるようだ。除去されたものは文末にdeinstallとついているので、まずはこれをgrepで取り除いたうえで、sedで文末の空白文字とinstallを除去すればよい。

dpkg --get-selections | grep -v deinstall | sed -e "s/[[:space:]]*install$//"

メモ代わりに書いておく。

2015-03-24

メンバー関数へのポインターを返すメンバー関数へのポインターを返すメンバー関数

class Foo;が存在したとして(1)Fooのメンバ関数ポインタ(2)を戻すメンバ関数のポインタが欲しいと思った(なお(1)で戻すメンバ関数もFooのメンバ関数ポインタを戻す)のだが、どうあがいても記述出来ないものだったりするのだろうか?

ようするに、以下のようなことがしたいわけだ。

class Foo
{
public :
    // メンバー関数a
    void a() { }
    // メンバー関数aへのポインターを返すメンバー関数b
    ??? b() { return &Foo::a ; }
    // メンバー関数aへのポインターを返すメンバー関数bへのポインターを返すメンバー関数c
    ??? c() { return &Foo::b ; }
}

ここで、???の部分に戻り値の型を記述しなければならない。

もちろんこれは記述できる。ただしその記述は、C++の規格のバージョンにより難易度が異なる。

C++14

最新の素晴らしい標準規格であるC++14では、この程度の問題は赤子の手をひねるより簡単だ。

C++14に追加された戻り値の型推定は、戻り値の型を書くべき場所にautoキーワードを書くことで、return文のオペランドの式の型を戻り値の型として書いた扱いになる。

// C++14
// 戻り値の型推定
class Foo
{
public :
    // C++14
    void a() { }
    auto b() { return &Foo::a ; }
    auto c() { return &Foo::b ; }
} ;

return文のオペランドの式の型はコンパイル時に決定できるため、当然、戻り値の型もコンパイル時に決定できる。これは具体的な形名を手で書くのと全く同じである。コンパイラーができることはコンパイラーにやらせれば良い。人間様が手をわずらわす必要はない。

C++11

残念ながら、4年も前の大昔の標準規格であるC++11には戻り値の型推定がない。そのため、戻り値の型を手で書かなければならない。ただし、C++11には、戻り値の型を後置する新しい関数記法がある。戻り値の型を書くべき場所にautoキーワードを書き、関数宣言の最後に->を書いて、その後に戻り値の型を書く。

// C++11
// 新しい関数記法
class Foo
{
public :
    void a() { }
    auto b() -> void (Foo::*)() { return &Foo::a ; } 
    auto c() -> auto (Foo::*)() -> void (Foo::*)() { return &Foo::b ;}
} ;

関数aの型は、void (Foo::*)()である。あるいは、auto (Foo::*)() -> voidである。とすると、この型を返す関数は、auto b() -> void (Foo::*)()となるここまでくればもう明白だろう。そう、関数cが返すのは auto (Foo::*)() -> ???で、???に入る戻り値の型はvoid (Foo::*)()だ。

C++03

C++03を今使うものは何か苦行でも行っているとしか思えない。

まず、関数型がある。


void (int)

しかる後に、関数へのポインター型がある。


void (*)(int)

関数ポインターを返す関数は以下のように書ける。


void f(int) { }

void (* g())(int)
{
    return &f ;
}

わかるだろうか。g()が関数gの関数名と仮引数リストだ。void (* ...)(int)の部分が戻り値の型だ。関数型には仮引数リストやその他の修飾などが含まれる。関数の型の文法上、仮引数リストと修飾はg()を囲む形で記述される。

つまりこういうことだ。

void (* // 戻り値の形名
    g() // 関数名と仮引数リスト
)(int) // 戻り値の形名
;

関数gへのポインターを返す関数hは以下のように書ける。


void (* (* h())() )(int)
{
    return &g ;
}

つまりこういうことだ。

void (* // 戻り値の形名
    (*  // 戻り値の形名
        h() // 関数名と仮引数リスト
    )() // 戻り値の形名
)(int)  // 戻り値の形名
;

そして、メンバーへのポインター型がある。

struct X { void f() ; } ;

void (X::* p1)() = &X::f ;

これらを組み合わせると、C++03という化石の様な古代の標準規格でも書くことができる。

// C++03
class Foo
{
public :
    // C++03
    void a() { }
    void (Foo::* b())() { return &Foo::a ; }
    void (Foo::* (Foo::* c())())() { return &Foo::b ;}
} ;

typedefを使うことで、いくらかマシにはできる。

class Foo
{
public :

    typedef void (Foo::* a_ptr) () ;
    typedef a_ptr (Foo::* b_ptr)() ;

    // C++03
    void a() { }
    a_ptr b() { return &Foo::a ; }
    b_ptr c() { return &Foo::b ;}
} ;

結論としては、早くC++14に移行しろということだ。

ドワンゴ広告

この記事はドワンゴ勤務中に書かれた。

ドワンゴは本物のC++プログラマーを募集しています。

採用情報|株式会社ドワンゴ

CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0

2015-03-22

妖怪ハウスで肉会をした

本の虫: 2kgのサーロインブロックが届いたので21日の夜はステーキ会

妖怪ハウスの住所の私宛に、突然2kgのサーロインブロックが届けられた。真空パックの冷蔵品で、何と賞味期限が週末までだ。これはまずい。なんとか週末に使ってしまわねばならない。

しかし、物は2kgの牛肉の塊である。私はそこまで大量の肉を食べたいとは思わない。こんなに急で人が集まるかわからないが、とにかく週末に人を呼んで肉会でも行うしかないだろう。

かくして、21日の夜に妖怪ハウスで肉会が計画された。

さて、2kgの牛肉をどうやって調理するべきだろうか。やはり、無難にステーキにするべきだろうか。ローストビーフにするという意見も出た。妖怪ハウスにはガスオーブンがあり、ローストビーフは調理可能だ。シチューにするという意見も出た。しかし、せっかくの巨大な肉の塊なのだから、やはりステーキにするべきだろうと結論した。

さて、サーロインブロックをステーキにするには、まず脂を切り取り、塩をすり込んで、包丁で軽く切れ目を入れた後、コショウを振ってフライパンで焼く。最初は強火で、ひっくり返して弱火で焼くとよいらしい。今回はバターも使った。

ステーキは、想定していたより手間も時間もかからなかった。これがカレーとなると、まずタマネギを5,6個みじん切りにするところからはじめなければならないのだから、段違いの手軽さだ。

また、妖怪ハウスにはガスオーブンが設置されているので、付け合せに野菜を焼いてみた。人参とタマネギとピーマンをオリーブオイルをかけて焼くだけだ。人参は最も調理に時間がかかるので先に焼く。人参を皮付きのまま数センチの厚みに切り、オリーブオイルをかけてすこし焼く。その後にタマネギとピーマンも入れて焼く。何の味付けもしなかったが、十分にうまかった。

さて、ステーキの味はなかなかのもので、客の食欲も旺盛であり、2kgの肉塊が一瞬にしてなくなってしまった。2kgでは足りなすぎる。

なかなか盛況であったので、今度は自前で肉を調達して、また近いうちに肉会をやりたいものだ。

2015-03-18

2kgのサーロインブロックが届いたので21日の夜はステーキ会

妖怪ハウスに私宛で以下のような品物が送りつけられた。

[The Meat Guy] サーロイン 2kg ブロック

箱を開けてみると、2kgのサーロインブロック、真空パックの冷蔵品だ。賞味期限を見ると、なんと3月の23日、週末までではないか。丸鶏の冷凍品が送られてきた時は、何ヶ月も期限があったので、しばらく放置した後にスタッフドチキンを作ったが、これには猶予がない。週末には調理しなければならない。

すこし考えたが、やはりなにも考えずにステーキにするのが一番だと結論した。

そんなわけで、3月21日土曜日の夜は、ステーキを焼く会を開こうと思う。ステーキを焼くのは初めてだが、食べたい人は妖怪ハウスまで来るといい。

2015-03-16

金の使い道が分からない

「毎日のように飲みに行っている」と男は我々に語った。我々は土曜日を丸一日エンジニアボードゲーム会@本郷に費やした後、会場の近くの中華料理屋で食事をしているのであった。

「毎日のように飲みに行っている。だいたい終電を逃してタクシーで帰る」

よく金が続くものだ。

「結局、タクシー代を考えると、都内に住んだほうがいいので、都内に住んでいる。しかし、家など寝るためにあるようなものなので、いらないのではないか。最近は荷物をどんどん減らしている」

「貯金がないし、たまらない。どうやって貯金をしたらいいのだろう」

贅沢な悩みだ。私と、その場にいたある女は、金の使いどころがなくて貯金額のみむなしく増えていく一方だというのに。

毎月の給与所得というものが発生するようになって早一年、最初こそ、東京に身ひとつで引っ越してきたばかりであり、色々と物入りであったが、基本的な日用品を買い揃えてしまえば、後はなにも要らなくなってしまった。一体、世間の人は金をなにに使っているのだろうか。

酒に金を使う種類の人間がいる。外で飲むと高くつく。これは、飲み屋には酒と食材の他にも、場所や建物や人員が必要なためである。また、終電を逃して家に帰りたければタクシー代もかかる。私は飲み歩きたいとは思わない。まず、飲み屋というものは大抵がタバコという薬物に中毒している救いようのない人間の多い場所であり、そのような場所に身を置きたくはない。また、酒も、相手が入ればこそ飲んでも楽しいが、一人寂しく飲みたいとは思わない。また、東京の飯はまずいので、食べ歩きたいとも思わない。

旅はどうか。旅には金がかかる。新幹線や飛行機はそれなりの値段であるし、宿も高い。私は旅に出たいとは、今のところ思っていない。特に見たい名所もないし、温泉というものにも興味がない。広い湯船につかりたければ、銭湯に行けばいい。

車やバイクにも興味がない。

ビデオゲームはどうか。私にとって、ビデオゲームとはマウスとキーボードで行うものである。当然、十分な性能のCPUとGPUとメモリ容量と高速なストレージを備えたコンピューターでなければならぬ。これを用意するには、数十万円かかる。それでもたったの数十万円だ。一度PCを組んでしまえば、一年以上使えるので、やはり月あたり数万円の出費でしかない。それに、今のところ、あまりおもしろそうなゲームがないため、数年はビデオゲームを控えるつもりである。

ボードゲームはどうか。ボードゲームは高い。僅かなコンポーネントの、実質ルールを書いた紙切れだけのようなボードゲームが数千円する。とはいえ、どんなに高いものでも、せいぜい一万円程度であり、一度買ってしまえば何年も遊べる。それを考えれば、それほど高くはない。MTGや遊戯王のような、高くつくゲームもあるが、私はデッキ構築ゲームにはそれほど興味がない。

最近は、ボルダリングをしている。ボルダリングは高くつくかというと、それほどでもない。ボルダリングジムの使用料は、一回あたり二千円弱だからだ。ボルダリング用の靴は高いが、一年以上使えることを考えると、やはりそれほどでもない。

妻子でもいれば色々と物入りなのかもしれぬが、あいにくと結婚とは縁遠いようだ。

最近、月一で江添ボドゲ会を開いているが、菓子や飲み物やビールを用意したり、カレーを5リットル作ったりするのに、それなりに金がかかる。とはいえそれほどでもない。

ただ、妖怪ハウスの賃貸契約が3年なので、来年以降存在しているかどうか怪しい。ボドゲ会のために、都内に広いリビングを備えた物件を借りるのにはそれなりに金がかかる。とりあえずはまだ先の話だ。

そういえば、このエンジニアボードゲーム会の後の食事の席で、コミット申請書とかコンパイル申請書などと言った闇の話を聞いたのだが、それはまたの機会に。

2015-03-11

最近のC++17事情

C++1z、あるいはC++17とも呼ばれている次のC++規格の、最近の事情はどうなっているのか。すでにドラフトに取り入れられた機能もあるので、現在の最新の状況を見ていこう。もうすでに紹介したものも含まれているが、おさらいとしてみていく。また、ここで解説する新機能は、いずれもすでにドラフト入りしているが、正式に規格制定される際に変わる可能性がある。

N3928: メッセージなしstatic_assert

C++11で入ったstatic_assertは、必ず文字列リテラルを書かなければならなかった。

static_assert( INT_MAX >= 2147483647, "This code assumes at least 32-bit int." ) ;
static_assert( true == true, "You're compiler is fundamentally wrong." ) ;

しかし、この文字列リテラルの使われ方は規定されていない。特にメッセージを書きたくなくても、文字列リテラルは、文法上必ず書かなくてはならない。


static_cast( std::numeric_limits<double>::is_iec559, "" ) ;

C++1zでは、文字列リテラルを書かなくてもよくなる。


static_cast( false ) ;

N4086: Removing trigraphs??!

トライグラフが取り除かれる。

// C++14までは#
// C++1zでは??=
std::cout << "??=" ;

N4051: Allow typename in a template template parameter

テンプレートテンプレートパラメーターにtypenameキーワードが使えるようになる。

template <
    template < typename T >
    class U
>
struct X ;

が、

template <
    template < typename T >
    typename U
>
struct X ;

とも書けるようになる。

N3922: New Rules for auto deduction from braced-init-list.

auto指定子で直接初期化でリスト初期化を書いた場合の挙動を変更する。C++14で合法なコードが違法になる珍しいケースだ。


auto a{ 1, 2, 3 } ;

このコードは、C++14までは合法なコードであり、decltype(a)は、std::initializer_list<int>となる。

C++1zでは、このコードは違法である。auto指定子で直接初期化でリスト初期化の場合は、単一のinitializer-clauseしか書くことができなくなった。

// aの型はint
auto a{ 1 } ;

N4295: Fold expressions

今回紹介する新機能の中で、一番面白い機能がこれだ。パラメーターパックに対するfold式がC++に入る。

パラメーターパック全てに対して演算子を適用したい場面がある。

// 与えた実引数にoperator +を適用して合計値を返す関数テンプレートsum
sum( 1, 2, 3 ) ; // 6
sum( 1, 2, 3, 4 ) ; // 10

このようなsumをC++14で書くと以下のようになる。

template < typename T >
T sum( T && t )
{
    return t ;
}

template < typename T, typename ... Args >
T sum( T && t, Args && ... args )
{
    return t + sum( std::forward<Args>(args)... ) ;
}

やりたいことは、パラメーターパックのそれぞれの実引数にoperator +を適用したいだけなのに、やたらと面倒なコードだ。

そこで、C++1zには、パラメーターパックに対するfold式が入る。

template < typename ... Args >
auto sum( Args && ... args )
{
    return ( args + ... ) ;
}

これは、sum( 1, 2, 3, )に対して、 (((1 + 2) + 3) + 4)のようにleft foldされる。

逆に以下のように書けば、

template < typename ... Args >
auto sum( Args && ... args )
{
    return ( ... + args ) ;
}

(1 + ( 2 + ( 3 + 4 ) ) )のように、right foldされる。

fold式は、必ず括弧で囲まなければならない。


( pack + ... ) ; // OK
pack + ... ; // エラー、括弧で囲まれていない

fold式には、unary(単項) foldとbinary(二項) foldがある。binary foldは、(e1 op1 ... op2 e2)という形を取る。op1とop2は同じfold演算子でなければならず、e1とe2は、どちらか片方だけがパラメーターパックでなければならない。e2がパラメーターパックであった場合はleft fold、e1がパラメーターパックであった場合はright foldとなる。


template < typename ... Types >
void f( Types ... args )
{
    ( 1 + ... + args ) ; // binary left fold
    ( args + ... + 1 ) ; // binary right fold
}

それぞれ、(( ( 1 + args0 ) + args1) + ... + argsN )と、args0 + ( args1 + ( ... argsN + 1) )のようにパック展開される。

N4267: Adding u8 character literals

UTF-8文字リテラルの追加。プレフィクスu8の文字リテラルで、UTF-8のコード単位一つで表現可能な文字が記述できる。


char a = u8'a' ; // OK
char b = u8'あ' ; // エラー、UTF-8で符号化するにはコード単位が3個必要。

N4230: Nested namespace definition (revision 2)

名前空間の宣言をネストできるようになる。

namepsace A {
    namespace B {
        namespace C {
            // ...
        }
    }
}

のようなコードが、

namespace A::B::C {
// ...
}

のように書ける。

N4266: Attributes for namespaces and enumerators

名前空間とenumeratorにattributeが記述できるようになる。C++14までは、文法上の制約で記述することができなかった。これにより、名前空間やenumeratorにdeprecatedが指定できる。記述する場所は、名前空間ならnamespaceキーワードの前、識別子の後、enumeratorならば、識別子の後だ。


namespace [[deprecated("This namespace is deprecated.")]] libv1 { }

enum class E { value [[deprecated("This enumerator is deprecated.")]] = 0 ; }

N4268: Allow constant evaluation for all non-type template arguments

すべての非型テンプレート実引数でコンパイル時評価を可能にするように制限を緩和する。

以下のコードは違法である。

template<int *p> struct A {};
int n;
A<&n> a; // ok

constexpr int *p() { return &n; }
A<p()> b; // エラー

理由は、定数として渡せるポインターはnullだけだからである。

constexpr int *p() { return nullptr ; }
A<p()> b; // OK

constexprがある今、この制約は時代にそぐわない。そこで、非型テンプレート実引数に、任意の定数式を渡せるように制限を緩和された。つまり、上のコードは違法ではなくなる。

ドワンゴ広告

この記事はドワンゴ勤務中に書かれた。

ドワンゴは本物のC++プログラマーを募集しています。

採用情報|株式会社ドワンゴ

CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0

江添ボドゲ会3月の様子

江添ボドゲ会3月が開催された。

当日は16人ほどのボドゲ好きの男が集まり、なかなかできない大人数ボドゲをした。

筆者は、ソビエトロシア・・・もとい妖精の国の原子力潜水艦を火災や漏水から協力して救うゲーム、レッドノーベンバーを救えを人から借りて用意したが、展開しただけでやらなかった。このゲームは火災を消すために日に飛び込む必要があり、そのためにはウォッカをひとあおりしなければならないという、とても面白い設定のゲームなのだが、残念ながら肝心のゲームの設計が悪く、同じく協力ゲームであるパンデミックやフラッシュポイントに面白さで劣るゲームとなっている。

なお、前日は花金を満喫して会社で徹夜でボドゲをしており、帰ってから、カレーを作って、またボドゲをして、日曜日も残った面子でやはりボドゲをした。実にボドゲ三昧の週末であった。

ドワンゴでは、ボドゲは社内で昼からやっているのだが、なかなかそういう環境にいない人にはボドゲを日常的にするのは難しいようだ。

そういえば、火曜日にギャモンボードが妖怪ハウスに届いた。

2015-03-09

ChromiumがLinuxカーネル3.17より前のサポートを打ち切り

Chrome/Chromium To Require Newer Version Of Linux Kernel - Phoronix

ChromiumがLinuxカーネル3.17以前のサポートを打ち切るようだ。

というのは釣りタイトルだが、Linuxカーネル3.16までで、最新のChromiumを使うと、ブラウザー拡張がインストールできないという問題がある。この理由は、Linuxカーネルのサンドボックス機能であるseccompに最近入ったTSYNC(SECCOMP_FILTER_FLAG_TSYNC)をChromiumが使っているためだ。

興味深いは、LinuxカーネルにTSYNCをいれたのは、Chromium開発者で今はGoogle社員でもあるKees Cookだ。Ubuntuの12.04と14.10のLinuxカーネルには、TSYNCをbackportするパッチがあたっているが、これもKees Cookが用意したらしい。

これは適切に情報共有されなかったらしく、かやの外の人間は、最新のChromiumが動かなくなった理由を、手探りで解明する必要に迫られた。

Issue 758063005: Linux sandbox: report TSYNC status in chrome://sandbox - Code Review

さて、ChromiumがLinuxカーネル3.17以上を必要とするようになったというバグ報告は、WontFixとされてしまった。なぜならば、「これは技術的にはregressionではあるが、ユーザーにはカーネルのアップデートという、十分にリーズナブルなworkaroundが存在するから」だそうだ。はたして、本当に十分にリーズナブルだろうか。

Linuxカーネル3.16というのは、去年リリースされたばかりのだいぶ新しいバージョンである。3.17以上を要求するのはあまりにも難しい。3.17以前のカーネルでChromiumを使うには、TSYNCのbackportが必要だが、少なくともDebianは、backportに消極的のようだ。

Debian 8/jessie - SECCOMP_FILTER_FLAG_TSYNC

2015-03-08

毎月第一週の土曜日は江添ボドゲ会

4月4日に妖怪ハウスでボードゲーム会を開催する。

江添ボドゲ会@4月 - connpass

毎月第一土曜日に開催する定例会にしていきたいところだ。