憂鬱な午後のひととき


6月28日_
intrinsic version of strlenSSE42を加筆.
pcmpestriなどは戻り値がecxとフラグの両方なのでintrinsicだと無駄が出るんだろうなと思ってたらちゃんと作られていたという話.
ところで_mm_cmpestrcがCFで*stroがOF, *strsがSFで*strzがZFなら*straはAFと思うだろう. 普通.
6月27日_

SSE4.2文字列処理命令のページを書いた.

もうSSE4.2が使えるCPUは大分旧世代で本当に今更なのだけど, きちんとマニュアルを読んだこと無かったので今回理解しようとした. 想像していた以上に複雑で驚いた.

よくもまあ, こんな複雑な命令を高速に処理させることができるものだ. 流石CISCの権化である.

簡単なベンチマークを取ってみた(strlen_sse42.cpp). 対象はXeron X5560とcore i7-2600. プログラムはWindows/Linux両対応(Macでも動くかな).

strlenLIBCはコンパイラ標準のstrlen, strlenCはナイーブなCによる実装.

size_t strlenC(const char *p)
{
    const char *top = p;
    while (*p) {
        p++;
    }
    return p - top;
}

strlenSSE2は昔作ったやつ(fast strlen and memchr by SSE2).

strlen vs strlenSSE2 vs strlenSSE42 onXeon X5650 + gcc 4.6.0
ave 1.98 4.96 6.76 9.75 11.60 15.23 18.86 28.65 51.63 84.67127.23 175.75 197.63
strlenLIBC 11.78 5.00 4.06 3.22 2.92 2.42 2.12 1.58 1.02 0.71 0.51 0.38 0.34
strlenC 14.06 8.40 6.96 5.59 4.95 4.26 3.83 3.27 2.71 2.46 2.31 2.19 2.19
strlenSSE2 12.77 6.40 5.11 3.92 3.42 2.77 2.35 1.73 1.15 0.79 0.52 0.33 0.28
strlenSSE42 9.26 3.98 3.23 2.70 2.50 2.24 2.03 1.59 1.01 0.69 0.51 0.35 0.31

strlen vs strlenSSE2 vs strlenSSE42 on Core i7-2600 + gcc 4.4.5
ave 1.98 4.96 6.76 9.75 11.60 15.23 18.86 28.65 51.63 84.67127.23175.75197.63
strlenLIBC 11.68 5.67 4.60 3.61 3.11 2.54 2.14 1.43 0.72 0.46 0.36 0.30 0.30
strlenC 14.47 6.95 5.55 4.37 3.94 3.42 3.07 2.56 2.07 1.76 1.60 1.51 1.46
strlenSSE2 12.30 5.74 4.58 3.42 2.97 2.35 1.92 1.26 0.71 0.40 0.30 0.24 0.23
strlenSSE42 12.56 5.26 4.12 3.29 2.95 2.59 2.31 1.56 0.83 0.52 0.39 0.32 0.29

aveは平均の文字列長. 徐々に長くしている. 数値はbyteあたりに掛かったクロック数. 小さい方が速い.

条件が違うので直接比較はできないのだけど, 昔SSE2版を作った頃のgccのstrlenLIBCは大分遅かったが最近はかなりよくなってるようだ.

ただstrlenLIBCとstrlenCを比べるとstrlenLIBCは明らかに専用の最適化コードが入ってるので汎用的ではない.

文字列が長いときは, 今でもstrlenSSE2が速い. これはループアンロールして32byte単位でデータを取り込んでるからだろう.

strlenSSE42はXeon上では好成績. こんな単純なコードでこれほど速いなら使い勝手がよいかも. i7だとXeonほどでも無いけど悪くはない. ループアンロールも不要, アライメントを気にしなくてもよい, 昔のrep stringみたいに初期コストが高いわけでもなさそうだ.

というわけで, あの複雑怪奇な仕様さえクリアできれば大分よさそう.

6月26日_
各会社がそれぞれに夏用特別就業時間割りを用意する動きをサマータイムというな. 時刻を変更する本来の意味のサマータイムとまぜこぜに使うとただでさえ対立してる議論がますます混乱するだけ.
新聞記事では, よく見ると個別の発言者は正しく区別してても見出しが「サマータイム」となって台無しなこともある. ことあるごとに「最近の若者の言葉が乱れてる」というマスコミが無頓着になぜ使うか.
「ボランティアの義務化」も似たようなものだ. 義務化した時点で自発的でなくなるのだから「奉公の義務化」などというべき.
6月25日_
第6回 自然言語処理勉強会 #TokyoNLP参加
@niamさんの発表が非常に面白かった. 専門外なので細かいところは当然分からないのだけど, 専門用語や, 聞いたことも無い言語やその発音記号などのオンパレードに圧倒された(資料:言語アフリカ起源説). 彼の発音も, なんというかリアルで感動した.
そういやアナウンサーの鼻母音問題といえば, 小学生の音楽の授業で「はーるのおがわは~」の「が」は「んが」といいなさいと習って「なんで区別するんだろう」と思ったのを覚えてる.
6月24日_

コードサイズと実行速度の関係に関する話(perfでL1命令キャッシュミスをみてみよう)

先日, 「やみくもにコード生成してもコードが肥大化してキャッシュから外れると遅くなるよ」とsinya8282さんに言ったけどどの程度だろうと思って実測してみた. 測定コードはicache.cpp(Windows/Linux, 32bit/64bit対応)

./a.out 0で実行すると次のようなシーケンシャルにaddを実行するコードを生成する.

 .lp:
    add eax, 1
    add eax, 1
    ...
    同じものをたくさん生成
    ...
    add eax, 1
    sub ecx, 1
    jnz .lp

Xeon X5650で走らせるとシーケンシャル版はコードサイズが32KBを超えても意外と速度低下は無かった.

sequential code size benchmark
コードサイズ(KB) <1 1.5 3 6 12 24 49 98 196
clk/unit 0.9 0.908 0.906 0.923 0.898 0.907 0.910 0.920 0.917

これは単純な命令のシーケンシャル実行なのでdecoderの先読みが十分速くて実行ユニットへの命令供給が問題ないということだろう.

次にオプションを変えて./a.out 1で実行すると次のようなjmpを含むコードを生成する.
./a.out 2では更にジャンプがシーケンシャルでなくなるコードを生成する.

 .lp:
    add eax, 1
    jmp L1
   L1:
    add eax, 1
    jmp L2
   L2:
    add eax, 1
    jmp L3
   L3:
    ...
    同じものをたくさん生成
    ...
    add eax, 1
    jmp Ln
   Ln:
    sub ecx, 1
    jnz .lp

このコードでベンチマークをとると次のようになった.

code size with jmp benchmark
コードサイズ(KB) <1 1 2 4 8 16 32 65 131
clk/unit 1~2 2.099 2.060 2.059 2.085 2.029 9.138 12.138 12.039

これらのjmpがからむコードでは, コードサイズが16KBを超えるあたりでガクンと実行速度が落ちた. i7-2600でもほぼ同じ傾向.

L1命令cache自体は32KBあるのにそれよりも前に速度劣化が発生している. これはjmp命令が複雑でμopへのdecodeが追いつかず, decoded icacheも足りなくなって底をついたせいじゃないかと想像. decoded icacheはSandy Bridgeで最大1536エントリ. もちろん命令の種類によっても変動はあるだろう. いろいろ試すと面白いかも.

せっかくなので先日の最適化手法の勉強方法で少し触れたLinuxプロファイラツールperfでicacheミスがどのぐらいあるかを見てみよう. perfはバージョンや環境によってによって結構オプションが違うみたいなので注意が必要.

>perf --version
perf version 0.0.2.PERF

>perf stat -e L1-icache-load-misses ./a.out 0
       24166341  L1-icache-load-misses    #      0.000 M/sec

>perf stat -e L1-icache-load-misses ./a.out 1
      239641246  L1-icache-load-misses    #      0.000 M/sec

>perf stat -e L1-icache-load-misses ./a.out 2
      342195755  L1-icache-load-misses    #      0.000 M/sec

-eオプションで指定できるパラメータは

perf list

で取得できる. L1-icache-loadl-missesの値がシーケンシャル版に比べて ジャンプ版は1桁icahceミスが増えている(もちろんjmp命令が増えてるので同一条件ではないのだけど).

ちなみにdecoded icacheについては先日書き始めてみた「Intelプロセッサ最適化マニュアルを読もう」で少し書いた(一次情報はもちろんIntelのマニュアルを参照してね).

6月23日_
・数論暗号勉強会参加
まだ基本的な部分なので大丈夫. 次回は7/21と7/28のどっちだっけ.
・暑すぎる. 押し入れの中の扇風機出してくるか.
6月22日_
やっぱり調子が出ない.
6月21日_
最近ランニングの調子が出ない. 体調悪いのかな.
6月20日_
Software Design』7月号
>「なぜ, 周りの(デキる)エンジニアはMacを使っているのか?」という素朴な疑問から企画がスタートしました.
技術情報誌なら, その疑問が事実かどうかの検証記事が欲しいなあ.
個人的にはWindowsが動くだの, MS Officeも動くだの, Linux系ソフトが動くだのと言った他人の褌の話じゃなくてMacならではのグラフィカルなツールな紹介をみたい.
6月18日_
JANTは無事応募があってよかった. 本当に直前に来るもんなんだなあ.
査読終了. 面白そうなのにtypoやミスが多くてうまくrecoverできなかった. 申し訳ない.
他にも締め切りがかぶったりでやること多かったが一段落.
6月17日_
『数学セミナー』7月号で「圏論の歩き方」の連載開始. 面白そう.
いいなあと思った点は二つ.
6月16日_
しばらく前に, 最近生成文法にはまってる@shuyoさんに右と左の概念の無い言語があるという話を聞いて, へえと感心していたのだが, 先日@nishioさんに英語で「○○は何代目の大統領?」という問いも直訳できないと聞いて驚いた. 少し検索すると割と有名な問題みたいでいろいろヒットする.
"What number president is ..."は覚えやすそうだ. でもProject Eulerなんかで何番目?という問いはいくらでもあっただろうと思ったけど, 改めて見ると 「何個あるか?」とか「そうなるときの値は?」みたいな聞き方ばかりみたい. 少なくとも"What number"という言葉は現れていない. ほぅ.
6月15日_

最適化手法の勉強方法

Intel 最適化リファレンス・マニュアル読書会

人数が少なかったので多分顔合わせ的なものだろうと参加してみた. 管理者のsyuu1228さんは何故か不在.

Intelの最適化マニュアルを読むのはある程度のスキルがついてからでよいと思う. 初心者にはあまりお薦めしない. 理由は資料が膨大なのでどれが重要なのか分からない, 読んでて退屈(個人的にはおもろいけど)なので途中で挫折しやすい, かつそうなると後半の大事な部分が抜け落ちるから.

こういうのは人によって勉強方法が異なるので「これが絶対正しい」というわけじゃない. だから自分の経験(主にx86/x64)を書いてみよう. なお, ここでは「そもそも最適化が必要なの?」という問い対しては触れない.

6月13日_
Intel Advanced Vector Extensions Programming Reference
ちら見した. AVX2登場. 全般的に整数計算するひと喜ぶ. 暗号系でも使いやすそうだ.
6月12日_
今日は10km走って7km歩いた. 結構疲れた.
6月10日_
いや, ちょっとまて. やっぱり変. myidはintで1もintだから引き算の結果-1もint. uint32_tとの割り算をするときにinteger promotionされて0xffffffffになってそれを64で割ったあまりだから63になるはず. なんでそれが0xffffffffになるんだ? そのためにNPROPSもunsignedにしてるのに. もしかしてsizeof(int) = 8かと思ったけど4だった.
いろいろ試してみた.
#include <stdio.h>

void f()
{
    int id = 0;
    unsigned int n = 64;
    unsigned int a = (id - 1) % n;
    printf("f=%u\n", a);
}

void g()
{
    int id = 0;
    const unsigned int n = 64;
    unsigned int a = (id - 1) % n;
    printf("g=%u\n", a);
}

int main()
{
    f();
    g();
    return 0;
}
このコードをgcc/g++(4.6.0)で動かすとどちらでも
f=63
g=63
mpicc(for 1.2.7 (release) of : 2005/11/08 03:56:31)でも同じ結果. でもmpicxxだと
f=63
g=4294967295
ん, 処理依存を踏んだ? でもconstのありなしで整数演算の挙動が変わるって聞いたことないからmpicxxのバグなのかなあ.
6月9日_
2011年度年会JANTオーガナイズド・セッション応募者募集中. 締め切り間近!
・今日の並列プログラミングの午前の講義の講師は元同僚だった. 世の中狭い.
最後の課題は別に難しくないと思ったけどどうしてもうまくいかない. 帰って家でいろいろ調べてようやく分かった.
const uint32_t NPROCS = 64;
const uint32_t to = (myid - 1) % NPROCS;
でuint32_tを使うことでwrap aroundしても大丈夫なようにしてたつもりが, 課題サンプルの元からあったプロセッサ番号を示す変数myidがintのままだったのでmyid = 0のときに0xffffffffになってたからだった. うわっちゃ. こないだ定数クイズなんて出してたくせに.
6月8日_
第14回お試しアカウント付き並列プログラミング講習会
「MPI基礎:並列プログラミング初級入門」
に行ってきた. とりあえずはMPI_BcastやMPI_Sendを使ってHello worldするとかそんなので慣れる.
6月7日_
ペースあげてPRML5章読み終わった. 2~4章はぼちぼちと本の式変形のギャップを埋めたり一部の演習問題の答え書いたりしてた. あと, PRMLの数学的な部分で個人的に許せない表現を修正したりとか(苦笑). 社内ではあんちょことか言われてる.
prml2.pdf
prml3.pdf
prml4.pdf(ほぼ本に沿ってる)
5章もそういうの欲しい人いるかな. ああ, その前に8章予習しなきゃ.
ああ, ああ, そのその前に査読もしなきゃ.
6月5日_
昨日, 今日と全然頭が働かない. ランニングも調子が出ない. Project Euler 282もやり方は分かってるのに計算を合わせるのにえらい苦労した.
6月3日_
PRML5.4.5 ヘッセ行列の厳密な評価の章,
xiが入力,
aj = Σi wji1 xi,
zj = h(aj),
bk = Σj wkj2 zj,
δk = ∂En/∂bk,
Mkk' = ∂2En/∂bk∂bk'
としてくれれば何も問題なかったのにbkをakとかいてるせいで数時間無駄にした. 5章はそんなのが多すぎ.
6月1日_

C/C++における定数クイズ(規格改定による仕様変更の差異について)

int x = -1;
int y = x == 4294967295;
int z = x == 0xffffffff;

としたときy, zの値は何か. もとネタはC/C++の定数の型の話 C90/C99の差分のびみょーな話. そこでも触れられているが接尾語(ULとかLLとかのこと)無しの10進数, 16進数リテラルの扱いは規格改定の際に意外と変わっている. まとめてみよう(以下long intをlongと略す).

整数定数が決まる型の順番
10進数 16進数
C90(yupo5656さんの記事から孫引き) int,
long,
unsigned long
int,
unsigned int,
long,
unsigned long
ISO/IEC14882:1998(C++98)
ISO/IEC14882:2003(C++03)
int,
long,
それ以外は未定義
ISO/IEC9899:1999(C99)
N3242.pdf(C++0xドラフト)
int,
long,
long long
int,
unsigned int,
long,
unsigned long,
long long,
unsigned long long

C++0xではC99で入ったlong long型がようやく正式に入る. ポイントは二つ. 10進数と16進数とで整数定数を決める型の順番が異なること, 昔のCとC++とでは仕様に違いがあり, C99とC++0xとで統一されたということ. yupo5656さんのところではC++03はC90と同じとあったけど違うと思う.

では問題を考えてみよう. sizeof(long long) = 8とする. まずsizeof(int) = sizeof(long) = 4の環境とする. すると0xffffffffはいずれの場合もunsigned intになるのでz = 1.

それに対して4294967295はC90ではunsigned longになるのでx = -1がunsigned longに整数拡張(integer promotion)されて4294967295になり, y = 1となる. C++98, C++03なら4294967295はlongで表せないのでなんと未定義. C99/C++0xではlong longになるのでxの値-1と比較してy = 0となる.

sizeof(int) = 4, sizeof(long) = 8の環境とする. すると0xffffffffは上記の環境と同じでz = 1. 4294967295はlongになるのでy = 0となる. まとめると

クイズの答え
sizeof(int) = sizeof(long) = 4 sizeof(int) = 4, sizeof(long) > 4
y z y z
C90 1 1 0 1
ISO/IEC14882:1998(C++98)
ISO/IEC14882:2003(C++03)
未定義 1 0 1
ISO/IEC9899:1999(C99)
N3242(C++0xドラフト)
0 1 0 1

試してみる... ありゃ, gcc 4.6.0だと32bitでは-std=c++0xでもy = 1になるなあ. -std=c99だと正しくy = 0になるので多分gccが間違ってるんだろうなあ.


御意見は光成滋生<herumi@nifty.com>までお願いします