Island Life

2010/06/22

gitメモ

gitにもだいぶ慣れてきた。 仕事のプロジェクトでは最近Gerritが 導入され、パッチを手元のトピックブランチから直接提出できるようになったので、 gitのワークフローで 書いたフローよりも簡単なフローで回している。

  • トピックブランチ work-FOOで作業。
  • パッチ作成は別ブランチwrap-FOOにて。詳細なコミットログclog-FOO.txtを書いといて
    git reset --hard version1.0
    git merge --squash work-FOO
    git commit -F clog-FOO.txt
    テスト
    git push gerrit HEAD:refs/for/version1.0
    
    これでパッチがレビューに回る。

最近良く使うのはgit stash。 例えばトピックブランチwork-FOOで作業中、wrap-BARでのパッチが差し戻されて fixしなくちゃならなくなったら:

git stash                  # 作業中の変更をコミットせずに一時退避
git checkout work-BAR
...作業、コミット...
git checkout wrap-BAR
...パッチ作成、gerritにpush...
git checkout work-FOO
git stash pop              # 変更中の状態に戻す

いるべきブランチとは別のブランチで作業を進めちゃった時なども git stashしてブランチを移動後git stash popで変更だけ移せる。便利。

Tags: Programming, git

2010/06/21

言語Xは速い、というのは

昨日のエントリへのリンクが拡散しているようなのでちょっと補足めいた雑談。

定量的な速度の話になった時点で、純粋な言語そのものではなく 処理系込みの話になる、っていうのはいちいち断らなくてもいい…よね? 「実際にそういうことができる処理系が今存在しているか」っていうのは 言語選択に影響を与えるファクタであろうと思う。

普通に書いたLispと普通に書いたCならCの方がずっと速い。 Lisp版の方が安全で、簡潔で、汎用的だろうけれど。 違いが出るのはそっからどこまで追い込めるかという伸びしろの部分なんだけど、 それを重視すべきかどうかというのはプロジェクトに依存する。 コンパイラを出し抜いてまで性能を稼ぐ必要のある案件っていうのは 全体の割合では減ってきてはいるだろうと思う (もっとも、サーバサイドの スケーラビリティが重視される流れになって、「性能が足りなければ マシンを増やせばいいじゃない」とか「2年待てばチップが速くなるよ」 なんて能天気に構えていられなくなってきてはいると思うけど)。

個人的にはCommon Lispの処理系はこの先もっと頑張らないとまずいんじゃないか と思ってる。といっても普段使ってる処理系はAllegroだけだし、それも 限界まで使い込んでるわけじゃないので外してるかもしれないけど。

  • inter-proceduralな、あるいはプログラム全体を見ての最適化はもう C/C++では普通だけれど、Common Lispの場合、関数単位でのダイナミックな 置き換えがいつでも可能という前提があるため、ちょっとこの分野で遅れを 取っているのではなかろうか。インライン展開程度ならマクロで、 inter-proceduralな情報を利用することで可能になる型チェックの 省略などについては人手で型宣言をつけてやることで補償できるけれど、 ソフトウェアの規模が増大すれば、自動化しないと破綻するだろうし。
  • Lisp族というくくりで言えば、Stalinがwhole program analysisの 強さは証明しちゃってるのだけど、コンパイル時間が増大するのは大問題。 C++もそれでつまづいてる。ここはひとつ、動的言語の誇りにかけて、 全プログラム解析とインクリメンタルな関数の置き換えを両立させて 欲しいものだ。例えば関数を動的に置き換えると関数毎に閉じた最適化に リセットされるけど、そのままほっとくと勝手に解析が進んで次第に 最適化の範囲が広がってくとか。
  • 型というかメタ情報に対するサポートってのはもっとあっていいと 思うんだよね。Allegro CLだとfixnumをメモリアドレスと見なして 生メモリに直接触れるので(正確には8byte alignedなアドレスを3bitシフトした 値をfixnumで扱うんだけど、fixnumのタグが000なので ネイティブコードレベルではint64_t*で書いてるのと同じコードになる)、 例えばそれを使ってshared memoryをmmapした領域に 自分でメモリアロケータを書けたりするんだけど、 Lisp的にはアドレスとfixnumの区別がつかないんで、 void*とintptr_tの区別があるCよりも状況が悪い。 がっつり型検査しろとは言わないけど、「ここはaligned pointerだよ」、とか 「ここはaligned pointer同士の差分だよ」とか宣言しといたら ある程度自動的に矛盾箇所 (差分だけ使ってmemrefしてるとか) は 検出できるだろう。少なくともそういう検証ルーチンをコンパイラに組み込める フックがあったらいいなあ。自分でcode walker書けばできるんだけどさ。

「言語よりは環境の問題」と言えばそうなのかもしれないけれど、 マクロで低レベルコードを吐かせたり、なんていう話になってくると 言語と処理系と開発環境は相互に支えあってて切り離しては語れないよなあ、 とも思う。処理系と独立した言語仕様があること自体は、 プログラムの正当性の検証という見地から重要なんだけどね。

Tags: Programming, Lisp

2010/06/20

「Cで書くコードの方がCommon Lispで書くより速いって人がいたら、それは彼のCの技量が高すぎるってことだね。」

Valvallowさんの『Lisp は「C 言語並みに速い」「C 言語より速い場合がある」』 を読んで、 故Eric Naggumの c.l.lへのポスト を思い出した。

一年以上前にCommon Lispで書いためっちゃCPUヘビーな関数を、 ものは試しと最適化してみたら、一回の呼び出し当たりダメダメな623µsから 快適な4.7µsに縮んだよ。

全く同等の機能を持つCの関数は呼び出しあたり92µsかかっていたんだけど、 みんなそこまで最適化して満足してたんだ。 Common LispだとCでやるよりもずっと素早く色々なアルゴリズムを試せるから、 Cでひとつ最適化を試してる間に全く別の方法やアプローチを試してみることが できる。こういうことはほんとに良くあるんだ。 つまり、Cプログラマが最適な実装を見つけ出すよりも速く、 良いCommon Lispプログラマは最適なアルゴリズムかつ最適な実装を見つけることが 出来るってこと。

Cで考える連中はCが速いって思ってる。 でもそれは、彼らがCommon Lispが遅いって思っている以上に真実からは遠いんだ。 本当に速いCコードを書くのはとてつもなく難しいんだぜ。 下手するとコンパイラの吐くコードを眺めて、 結局自分でアセンブリを書くハメになったりする。 俺もCPUから最後の血の一滴まで絞り出すようなコードを書いたことはあるけれど、 Allegro CLのインストラクションレベルプロファイラや ハック可能なLAP (ありがとう、Duane!)、それからコンパイラマクロによる コード変換 (これはCLの標準機能だ) を使わずにやるのは難しかっただろうな。 Cプログラマはこういう道具を持ってないんだよ。

それでもCで書くコードの方がCommon Lispで書くより速いって人がいたら、 それは彼のCの技量が高すぎるってことだね。

(DuaneはFranz Inc.のDuane Rettig。LAPはLisp Assembly Program。 まあインラインアセンブリみたいなもの。)

このポストは1999年のものだけれど、 マクロと低レベル機能の組み合わせで、色々がアルゴリズムが効果的に 試せるってのは今でも当てはまる。 ただ、今の時代だと、Allegro CLにも不満はあるなあ。 キャッシュミスの影響とか簡単に見られるようになってくれると有難いんだけどなあ。

Tags: Programming, Lisp

2010/06/20

テストの最適化

テストの話を出したのでついでに。

今関わってるプロジェクトでは、標準のテストを流すのに25分くらいかかる。 一応もっと速い簡易テストもあるんだけれど、カバレッジが低くて 自分が触っている範囲にはあまり有効でない。 わりと、障害時対策とか高負荷時のバイパスとかそういうあたりを 触ることが多いので、正常系のテストはあんまり役に立たないのだ。

とは言っても、テストが重い理由は他にもあって、単体テストと結合テストが ちゃんと分けられてないんで、ちょっとしたことを試すのにも依存する モジュールがぞろぞろついてきたりしている。このへんうまくスタブを書いて 単体テストだけに絞ればもっとずっと良くなると思うんだけど、 なかなかそこまで手が回らない。

テストの「効率の悪さ」というのもカバレッジ分析と同じ原理で 測れるかなあ。テスト実行中に極端に重複して実行されている パスがあったら、多分そこが改善の余地があるってことだろう。 ああ、でもループ内で実行される関数はそれだけ回数が多く出てきちゃうから、 そのぶんの効果はさっぴかないとならないか。何にせよ、「ここは無駄なテストを しているよ」と指摘してくれるツールがあると便利だろうなあ。

(ちなみにAllegro CLにはカバレッジツールがついてるんだけど、 フラグを有効にしてリコンパイルしないとならないので まだうまく使えてない。(declare (optimize (speed 3) (debug 0)))な コードが大量にあるので結構面倒なのだ。)

Tag: Programming

2010/06/18

コードに対するテストも要るんじゃないかなあ

From RSpec のすごいところ:

結局、テストコードは何をもとにして書くのか、ということだよね。Test::Unit を使ってた頃は、テストコードはソースコードに対して書くものだと思ってた。でもそれは間違いで、テストコードは仕様に対して書くものということなんだ。

TDDで最初に書くテストは仕様に対するテストと考えて良いと思うのだけれど、 実装が済んだ後で (もしくは、実装がある程度具体化した後で) 改めてその実装に対するテストを書く ことは必要なんじゃないだろうか。

というのは、特定の条件を満たさないと通らないコードパス、っていうのは ほとんどのアルゴリズムにあって、 そういうパスを通すためのテストデータは実装が具体化してからでないと作れないから。 経験上、アルゴリズム内のパスが切り替わる境界条件付近にバグが潜んでることが 多いので、境界条件をつつくテストは仕様のテストよりずっと重視している。 (例えば浮動小数点数絡みで、計算結果に桁上げが生じて54bit目が 丸められる条件を突く、とか。外から見えない内部のテーブルがいっぱいになって 次のデータ追加でテーブルの拡張が起きる条件を突く、とか。)

実装が変わったら改めて境界条件を突っつくデータを用意しないとならない。 最適化すると大抵場合分けが増えるのでテストすべき境界条件も複雑になる。 最近はカバレッジ分析みたいなツール使って ある程度自動化するものかな? 私はロートルなんでいまだに手でデータを用意してるけれど。

そんで、そういうテストを書く場合、入力セットも期待すべき出力も 複雑になることが多いので、assert_equalをずらずら並べるテストというのは すぐに限界に達する。assert_equal並べるのは楽だからつい自分も書いちゃうけど。 どんな入力に対しても結果が満たすべき条件、というのを検査するテストルーチンを 書いて、それに実装の境界を突っつく色々な入力を与えるというのが良いと思っている。 簡単な目安として、assert_equalのexpectedに定数を書いてたらたぶんそれは 良くないテスト。(自分もそういうテストをたくさん書いてるんで、自戒込みで。)

たまに、テストしたい機能と、条件検査に使ってる機能が対でバグってて、 テストを通っちゃうこともあるので、別の処理系や言語で計算したexpectedを入れる ことはある。その場合はexpectedを計算するコードをコメントに書いたり しているけど、もっとうまい方法はあるかなあ。

(追記2010/06/20 10:56:49 UTC): From http://twitter.com/tyt/statuses/16608793012

http://tinyurl.com/2b86bkq 最適化のコードのテストは最適化されてないバージョンのコードを使えばいいのではと思った。入力の型以外はほぼ自動でできそうな感じ。(一回そうやってテストしたことあるけどテストはすぐ終ったし最適化後に仕様変更は無かったなあ。)

そうだなあ。 問題は、最適化されてないバージョンをテストで走らせられる 状態で持っとかないとならないってことなんだけど (ただ最適化前の状態を取っておくのではなく、最適化後にもコードは進化するので その後の変更を最適化前のコードにも統合してゆく必要がある)、 回避できるケースもある。

  • 納品したらおしまい、という仕事で、最終ステージでががっと最適化する場合。 コンソールゲームとか。イベント向けの一発仕事とか。
  • 仕様が明確で安定している場合。ランタイムオプションやコンパイルフラグで 常に最適化のあり/なしを切り替えられるようにしておける。 Gaucheでも-fno-inlineとか持ってるので、最適化オプションあり/なしでの 結果をダンプしといて比較することは自動化できそうだ。

でもやっぱり、最適化後のコードのパスを全部カバーするようなテストセットは 改めて作る必要があるよなあ。

Tag: Programming

More entries ...