憂鬱な午後のひととき


6月30日_
アセンブラ(アセンブリ言語)で書くと本当に、コンパイラーで書いたのよりも速くなるんでしょうか?
まあ一般論で断言しちゃうと話がおかしくなるのでそれはしないとして. 個別論なら, たとえばこないだやってた暗号系のやつだと多倍長整数が必要なんだけど, CPUのcarryを扱えるコンパイラってあんまし聞いたことないので素直にアセンブリ言語で書いた方が速そう. 後x86やx64用のCコンパイラだとsizeof(size_t) * 2のサイズの乗算, 除算命令を直接使えないことが多いのでそれも辛いですね(32bit Cコンパイラで64bit x 64bit→128bit作って, て言われたらどうする?). VCだと_umul128(64bit only)や__emuluはあるけど割り算はなさそう. gccには乗算もなさそう(あったらすまん. 教えてください).
それから私にとってXbyakはC/C++の文法でアセンブリ言語を記述できるのが大きな利点だな. ループのアンロール回数変えながらのベンチマークなんてのも簡単だし.
6月28日_
・じんましんが出た. ほっといたら治るのかな.
・今日のバッドノウハウ. 5M個ぐらいの数値の整列と重複除きはnub + sortではなくtoList + fromListを使う.
import List
import Data.IntSet
n = 7000
a = [i + j | i <- [1..n], j <-[1..n]]
--b = List.nub $ List.sort a // メモリ莫迦食いで目茶遅い 途中で落ちる
b = Data.IntSet.toList $ Data.IntSet.fromList a
main = do
 print $ length b
6月25日_
昨日は両親がきて三渓園を見たいというので行ってみた. なんか京都っぽいなあ, と思ったら原三溪という人が京都(など)にあった建物をいろいろ集めてきたものらしい. 豪快だ. 天気も暑くなくて落ち着いた感じがかなりよかった.
6月24日_
kinabaさん
ううむ、複雑に考えすぎではないでしょうか…。fib=1:1:... は単にリストを作って変数fibに持っている式なので、素直に動かすと全リストが保持されます。たまたま1回しか使わないならば残す必要がないので最適化で消えるだけで。
そうなのか. リストの要素の遅延評価に何か多大なものを期待しすぎてたのかな. 必要になるまで作られない(と強調されていることが多い)のだから, 逆に必要なくなれば(要素ごとに)すぐ消えてしまうと思い込んでました. 今回の場合は(fibを2回使うまで)メモリ食ってなかったし. でも一般的に「必要ないなら」の判断は難しいからそういうわけにはいかないか(それはまさにGCだ).

なるほど, とするとMemoiseのfibのメモ化の答えの一つがfib = 1:1:...でもあるのか. ただし, n番目しか必要ない場合はリストを使うべきではないし, もちろん何度も計算が必要になる再帰呼び出しのfib n = fib(n - 1) + fib (n - 2)を使うべきでもないと. なんだ当たり前の結論だ. でもちまたのHaskellの解説はミスリーディングなのが多い気がする…(言い訳).

いやいやいや, でもHaskellの場合はループがないからリストを使うことが多いのに「普通は全部持つ」なんて言われてもあちこちで使う[1..]が全部メモリにもたれたらたまらんよなあ. 最適化で消えてくれるかもしれないけど, 全部保持してても文句言うなよだと困る気がする. まだ複雑に考えすぎ?

試したらそうだった.

cat t.hs
f = [1..]
main = do
 print $ f !! 100000000
---
>runghc t.hs
t.hs: getMBlocks: VirtualAlloc MEM_COMMIT failed:
The paging file is too small for this operation to complete.

>ghc t.hs && main
100000001
sumと一緒なんだな. わかったよ.

6月23日_
続き. 「遅延だから」というのはまず思いつく理由ですが, 私はそれは違うと思ってます([追記] この発言は間違いでした). 昨日たまたまHaskell勉強会に参加してそのときに教えてもらって試した別バージョンで試します.
zipWith'::(a -> b -> c) -> [a] -> [b] -> [c]
zipWith' f (x:xs) (y:ys) = ((f $! x) $! y):zipWith' f xs ys
zipWith' _ _ _ = []

fib = 1:1:zipWith (+) fib (tail fib)
fib' = 1:1:zipWith' (+) fib' (tail fib')

ベンチマーク(ghc -O3@core2duo 2.6GHz memory 2GB).

fib2 bench[unit:sec]
fib fib' fib''
!! n foldl1' (+) sum !! n foldl1' (+) sum !! n sum
100000 0.88 0.41 0.42 1.30 0.41 0.39 0.22 0.41
200000 4.61 1.44 1.44 8.08 1.44 1.44 0.73 1.44
300000 12.91 3.14 3.14 24.23 3.14 3.14 1.59 3.14
400000 27.80 5.55 5.55 54.02 5.55 5.55 2.75 5.55

なかなかおもろい結果になりました. 勉強会に参加したときもzipWith'を作れば速くなるのでは, という意見があったのですがやってみるとちょうど2倍遅くなりました. 正格評価は関係なくて組み込みのzipWithとの差が出たのかなという感じです. あるいはこれでは正格評価になってなくて, もっとちゃんとしたzipWith'じゃないといけないのかもしれません. 私はわかりません.

[追記] kinabaさんに教えていただきました.

zipWith'' f (x:xs) (y:ys) = ((:) $! (f x y)) (zipWith'' f xs ys)
fib'' = 1:1:zipWith'' (+) fib'' (tail fib'')
とすべきだそうです. ありがとうございました. このfib''によるベンチマークは一番理解できる挙動になりました. というわけで上記zipWith'は間違いでした.

sumは今朝試しました. 初めてHaskellを触ったとき, sum [1..1000000]がこけてsumは使うまいと思ってたので試してなかったのですが, 意外なことに正格評価のfoldl1'と同じでした.

[追記] -O3オプションでコンパイルするとsumもfoldl1'なみの速度で計算するようになりました. このレベルでは同じようになるようです.

一つ想像するのはfibは二つ前ぐらいの値はやはりキャッシュされているのではないかということです. でないとfold1'やsumでO(n^2)はでないと思います. というわけでMemoiseの「fib 7のときにfib 5を2度計算してる」というのは違うんじゃないでしょうか(もちろん最適化の程度には依存しそうですけど).

[追記] 捨ててるか残してるかは(fib !! n)したあと(fib !! (n-1))すればわかるのでは?という疑問があがりました. で, 試してみると予想外の挙動を示しました.

なんと2倍ぐらいの時間がかかりました. じゃあキャッシュしてないのかと更にfib !! (n-2)も追加して3倍の処理時間を期待しましたが, 2倍のまま. メモリ使用量を見るとどうやら複数箇所のfib !! をかくとfibであろうと正格評価版のfib''であろうと全リストのメモリを保存しようとするようです. たとえばsumとfib!!を組み合わせても全リストを保持しようとしてるように見えました.

遅延/正格評価だけでなく, 値が再利用されるかどうかで, 時間使用量だけでなく空間使用量も変動する. したがって(2GBメモリの私のマシンでは) n = 400000のときはfib''版ですら2回値を取得できない. この挙動は読めないなあ.

6月22日_
Haskellのfibはとても遅いのその後. 実はもうちょっといろいろ試した. で, 有名なそれは必ずしも遅いわけじゃないという気がしてきました. まずはテストコードとベンチマークから.
A.
fib = 1:1:zipWith (+) fib (tail fib)

B.
fib = 1:1:[f n | n <- [2..]]
  where f 0 = 0
        f 1 = 1
        f n = (fib !! (n-1)) + (fib !! (n-2))

C.
f (x, y) = (y, x + y)
fib = map fst (iterate f (1, 1))

D.
fib 0 = 1
fib 1 = 1
fib n = f n 1 1
    where f 1 _ y = y
          f n x y = f (n - 1) y (x + y)

E.
import Array
fib = array (0,n) ((0,1):(1,1):[(k, fib!(k-2)+fib!(k-1))| k<-[2..n]])
Aは定番. Bはtanakhさんの大昔のサンプル. Cは自分で手続き型プログラムのを素直にiterateしてみた. Dは西尾さんところの. Eはtosさんの. 別にこれらの種別に他意はありません(勉強しようと検索してヒットしたのを並べてます). テストは2種類.
  1. (fib !! n) * 0でn番目だけを求める
  2. import Data.Listしてfoldl1' (+) (take n fib)をする
結果(ghc -O3@core2duo 1.8GHz memory 2GB).
fib bench[unit:sec]
A1 A2 B1/B2 C1 C2 D1 D2 E1 E2
100000 1.23 0.56 x 0.06 0.61 0.33 x 3.32 3.21
200000 6.69 1.97 x 1.28 2.00 1.01 x x x
300000 18.11 4.30 x 6.22 4.32 2.25 x x x
400000 38.78 7.55 x 17.43 7.57 3.81 x x x

まずn = 100000だと最後の方の一つの数値は20KBほどで, n = 0から単に数値を格納するだけで2GBいってしまうので明示的に領域を作ってしまうのはほぼアウトです.

n番目だけ求めるならDの専用関数を用意したものが流石に速いです. D2がこけてるのは[fib i | i<-[1..n]]が各iごとに計算するのですごいオーダーになってるからでしょう.

AとCではCの方がよさそうですが, オーダーがA1はO(n^2.4)ぐらいなのに対してC1はO(n^3.8)ぐらいあります. n = 800000だと逆転しそうですね. となるとAが一番よいという感じです. しかも面白いのはfoldl1'したときの数値(A2)がC2とほぼ同じで最速だということです. 逆にA1の方が遅いのが気持ち悪いです. なんとなく憶測ですがA2がこんだけ速くなるということはAの値を計算するときに少しはメモ化(あるいは一つ前の値の再利用)が行われているということではないでしょうか. A2がD2とオーダーがほぼ同じだというのもその傍証です. どうなんでしょうかね.

6月21日_
こないだからPRML少しずつ読んでるのだけど, グラフを赤と緑と青で区別してるのがかなり駄目っぷり. モノクロコピーしたら困るし, 見にくい人も多いだろうに. シュプリンガーでもこんなことするのか. cf. 教科書のカラーバリアフリー, 色盲のひとにもわかる バリアフリープレゼンテーション法(via TAKESAKOさん).
6月20日_
ePrintでHigh-Speed Software Implementation of the Optimal Ate Pairing over Barreto-Naehrig Curvesが公開されたのでソースコードも公開. 今回の目玉はBN曲線上のateペアリングで126-bit AESセキュリティ相当のものをcore i7 2.8GHz 1コアで1msec切ったこと. 多分世界初(まあこの業界最近競争が激しいのでいつ破られるか分かりませんが… とはいえそう簡単じゃない…と思いたい). 2ヶ月前にチーム組んだとき, 丁度ライバルのNew software speed records for cryptographic pairingsが出て, Franciscoさんたちはそのあたりのスコア目指してたみたいで, これはやられた, 結構厳しいかもとなった. でも, 今回は前回のCANSのときと違って実装は殆ど私担当で, そっちの観点からパラメータにもかなり口出して, (下位の)アルゴリズムも路線変更になったけど結構いい曲線を選べた感じ. アルゴリズムは大分素朴(だけどトリッキーな部分もいくつか)でつき進む. 個人的にはライバルのSIMD全開路線に非SIMDで勝ったのがおもろい.
6月19日_
・首が回らない.
・近所の量販店にテレビつきパソコンを見に行ったが, どうもモニタがピカピカ反射してみにくそう. やっぱり個別に買うのがよいのかな.
6月18日_
・会社のマシンにログインしたらマウスカーソルに四角いノイズがついていて, あ, これはやばいなと思ってたら画面全体が凍ってビデオカードが本当にお亡くなりになった. やっぱり新しいの買おう.
・共著の論文を提出した. 今回は狙って実装したのでacceptされてほしいな.
6月17日_
Pythonの同様のコードと比較してみた. Haskellってコンパイルしないとと~っても遅いんだな. インタプリタ言語だと思ってました. (nishioさんによる)Pythonのコードは
def get_primes(N):
    primes = []
    for i in range(2, N):
        if all(i % p != 0 for p in primes):
            primes.append(i)
            yield i
print sum(get_primes(200000))
素朴な素数生成[unit:秒]
PythonHaskell(コンパイル) Hakell(インタプリタ) C++(番外)
10000 0.17 0.06 1.27 -
50000 1.67 0.56 18.41 -
100000 5.59 2.36 70.52 -
200000 19.27 14.14 272.86 -
250000 28.92 25.36 - -
300000 39.92 39.73 - -
400000 67.20 77.00 - -
2000000 - - - 0.08
4000000 - - - 0.13
8000000 - - - 0.27

ここでまず気になるのはPythonの処理時間がO(n1.7)ぐらいなのにHaskellはO(n1.9)ぐらいなこと. 係数だけでなくオーダーが異なるのは変な気がする. 処理系が変なことしてるのか. まあそれはさておき, もうちょっとまともな篩版.
primes = 2:[x | x <- [3..], all (\p -> x `mod` p /= 0) $ takeWhile(\i -> i * i <= x) primes]
main = do
  print $ sum . takeWhile( < 2000000) $ primes
Pythonは上のコードのprimes.append(i)の直前にif i * i <= N:を追加する. もう一度ベンチ(但し上のベンチとはマシンが違うので数値の直接比較はできない).
少しましな素数生成[unit:秒]
Python Haskell(コンパイル) Hakell(インタプリタ)
10000 0.38 - 0.78
50000 0.59 - 1.55
100000 0.92 0.42 2.78
200000 1.75 0.55 5.55
400000 3.44 0.81 12.28
800000 7.78 1.42 28.67
1000000 9.70 1.78 37.86
2000000 22.86 3.97 91.38
4000000 52.55 8.97 -

大分ましになった. 本当はtakeWhileの中身を(<=sqrt(n))としたかったけど怒られた. truncate $ sqrt(n)ならOK?と思ったけどまだ駄目だった. (Integral b) => b -> Boolだからよさそうな感じだけど. うーん, 分からん.
6月16日_
今回は「本読んだだけじゃ駄目だ!, Haskellのプログラムを書いてみよう」と始めたのに早速10番目の問題で詰まってしまった. 本によく載ってる
sieve [] = []
sieve (p:xs) = p:sieve[x | x <- xs, x `mod` p /= 0]
sum $ sieve [2..2000000]
だといつまで経っても終わらない. 待ってる間に10分弱で書いたC++なら一瞬なんだけどな.
#include <iostream>
#include <bitset>
#if defined(_MSC_VER) && (MSC_VER <= 1500)
    typedef __int64 int64_t;
#else
    #include <stdint.h>
#endif

const size_t N = 2000000;

typedef std::bitset<N> Table;

int main()
{
    Table t;
    t.flip();
    t[0] = false;
    t[1] = false;
    for (size_t i = 2; i < N; i++) {
        size_t m = i * 2;
        while (m < N) {
            t[m] = false;
            m += i;
        }
    }

    int64_t sum = 0;
    for (size_t i = 0; i < N; i++) {
        if (t[i]) {
            sum += i;
        }
    }
    std::cout << "sum=" << sum << std::endl;
}
sieveのオーダーはO(n^2)なんよね(それすら違ってる?). 単に係数が大きすぎるだけ? 仕方ないので他のやり方を考えるけど富豪的じゃないなあ.
6月15日_
先月は数十nsecをけちるというど貧民プログラミングばかりだったので, しばらくは富豪プログラミングをやってみよう. というわけで(今更ながら)Project Eulerを始めた. 三日坊主で終わるかな?
6月13日_
はやぶさ帰還. たいしたもんだ. また府中郷土の森博物館に行ってみようかな.
6月10日_
頭痛が酷くてダウン. ちょっと根詰めすぎた. ああ, 締め切り1週間延びたのか. でもまあもういいや.
6月9日_
今朝会社で作業を始めたら唐突にマシンが死亡した. 同僚のアドバイスでビデオカードを抜いて試したけど駄目. でもよくみたらビデオカードのヒートシンクが外れかかっていた(爪が折れてる?). で, それを付けなおしたら復活した. でも時々マウスが凍った状態になるのでやばいそうな感じはする. 新しいの買おうかな.
自宅用のマシンも悩む. FujitsuのはCore i5はいいとしてもグラフィックがオンボードか. NECのは全部載ってるけどちと高すぎ. パーツ買ってきて自分で組み立てるのもありだけどテレビってどの程度安定して見られるもんなんだろか.
6月8日_
VAIOのtype LにCore i7が載ったら買うぞと思ってたのにマイナーチェンジしかしなかった. どうしよう. type JだとCore i7搭載機種があるのか. でもこっちは21.5インチだしなあ.
6月7日_
久しぶりにTeXを触って文献引用がうまくいかないと思ったらjbibtexではなくpbibtexを使わないといけないそうな.
6月6日_
手元のプログラムの最適化の過程でCore i7とOpteronが抜きつ抜かれつしてたが結局ほぼ同じクロック数に落ち着いた. ターゲットとしていたとあるプログラムはSSE全開で, そちらではi7の圧勝だったのでおもろいな.
6月4日_
↓管理者権限で実行するとyamyが効くようになった. ただしスタートアップに入れても管理者権限では動かない. そのためにはタスクスケジューラで「ログオン時に実行」で登録するとよいと教えていただいた. これで大分スムーズに使えるようになった.
6月3日_
会社のVAIO-BX(VGN-BX4AAPS)に64bit Windows 7を入れた. まず秀丸とJapanistyamyをインストール. レジストリエディターとかシステムのプロパティのウィンドウに対してはyamyが効いてない感じだった. 何か別の理由か勘違いかもしれないのでまたちゃんとチェックする.
6月1日_
TAKESAKOさんに『大人の科学マガジン』のテルミンの号をいただいた. 休みの日に遊んでみよう. 改造記事を野尻抱介が書いてる!

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