gcc-4.7のAdaの変更点

公式Release NoteのAdaのところが相変わらず手抜きなので、いつものようにメモっときます。
今回はなぜかAdaCoreが本気出してて、gccがStage 3(バグ修正のみのフェイズ)に入ってもガンガンコミットされてたりしてたので、最後まで追いきれてないです。ウォッチャーするのも根性がいるのです。
なお相変わらず、Bugzillaは放置されてます。AdaCoreはgccのBugzilla使ってないのでしょうか?
D言語(のように開発が他で活発に行われているフロントエンド)がgccリポジトリに入っても同じような状況になるんでしょうか?gccの開発体制とか全くわかりません!コミュ障です!

Ada2012対応

use all typeが部分的に使えます。
といっても変数を初期化する式でだけです。(V : T := X;のXでだけ。V := X;のXはダメでした。)
早く全面的に使いたいですねー。

aliased引数が使えます。

aspectのみの機能も実装されてきています……って書くと分かりづらいですね。
Ada2012のaspectC++0xで言う属性です。__attribute__(())です。Ada2005までは全部pragmaで後置していたのですが、宣言と一緒に書けるようにしたものです。
構文自体は4.6から受け付けるようにはなっていたのですが、既にpragmaとして存在する機能のみの対応でした。
4.7では、aspectの形でしか存在しない新機能にも対応しています……ってことですハイ。

Implicit_Dereferenceが作れるようになりました。
C++で言うと追加データやデストラクタも持てる参照型です。あるいは暗黙のキャスト演算子をひとつだけ持てるstruct。
(以前試したときは関数呼び出しの返値でのみ解釈されてましたが、オブジェクト単体でも解釈されるようになってます。やったー。)

range-based for(違)も実装されました。
Ada.Iterator_Interfacesもちゃんとあります。
展開されたコードを見ると冗長だなあ……と思いますが、Iteratorの型がForward_Iterator'Class(要するにサイズ不定の型)でFirstもNextも仮想関数で、Referenceの返値もデストラクタを持ってることを考えますと短くなりようもないですしね。
(-O3でコンパイルして、ループごとに仮想関数×1、非仮想関数×2の呼び出しが追加で必要。そんなもん?)

operator [] (違)も実装されました。
規格では右辺値ならConstant_Referenceが呼ばれるみたいなことが書いてある気がするのですがConstant_Reference/Referenceの呼び分けはコンテナ自体がconstant(in)かどうかしか見てないようです。
現在のAda.Containersの実装ではConstant_ReferenceとReferenceに差が無いので関係無いです。
俺々コンテナを使うときはReferenceが呼ばれることによるパフォーマンス悪化に注意、でしょうか。
(以前試したときはConstant_Referenceも「参照型」を返す関数のみ受け付けてましたが、Iterator_Elementの型をそのまま返す関数も受け付けてくれるようになってます。)
あと2引数以上の時の挙動はバグっぽいです。

QueueとかMultiway_Treeやなんかのコンテナも実装されました。

Synchronous_Barriersやなんかの同期オブジェクトも実装されました。

Subpoolも実装されました。
libcで言えばmalloc_zoneみたいなやつです。
もちろんmallocで取ってきたメモリを独自管理してますので、malloc_create_zoneやHeapCreateを直接使うのと比べて二重管理ry

その他改善点

Controlled型(C++風に言うとデストラクタのある型)がリンクリストを作らなくなってます。
以前は同一スコープのControlled型はリンクリストでつながってて、スコープを抜けるときはリストを辿って終了処理が行われていました。
つまりオブジェクト毎にポインタ2つ分の余計な領域が取られていたわけですが、これが無くなりました。
でもリンクリスト自体が無くなったわけではなく、スコープごとにFinalization_Masterなるものが作られるようになって、これがリンクリストを持っています。要するに侵入型リンクリストが外付け型になっただけ。
なので依然としてBoehm GCを使ってもControlled型は自動では解放されません。

Unbounded_Stringのデータが参照カウンタで共有されるようになりました。
(以前試したときはデフォルト無効だったのですが、デフォルト有効になってました。めでたしめでたし。何度も書いてますようにネイティブで可変長文字列を持つ言語と比べるとやっぱり見劣りしますが、とりあえずstd::stringを見上げることはなくなりました。)

protected型がread-write lockしてくれるようになってます。
今まではprotected procedureもprotected functionも同じ排他制御がなされてたのですが、protected procedureはwrite lock、protected functionはread lockされるようになりました。
(ここでfunctionは副作用なしというスタイルを心がけて云々と言えないのが辛いところです。Ada2012ではfunctionもin out引数を持てるようになって、副作用のあるfunctionの利用が加速しています。世の中が関数型に傾倒しているというのに、昔っからfunctionの引数がin固定だったりpackageがpureであることを明示できたりしてて素地があったはずのAdaは見事に逆行してます。)

他のフロントエンドから投げられたも受け止められるようになっています。
System.Exceptions.Foreign_Exceptionにマップされるようです。
これでミックスランゲージも大丈夫!
(以前、他のフロントエンドから投げられた例外はwhen othersでは受け止められないみたいなことを書きましたが、一年持たずに嘘記事になりました、めでたしめでたし。)

Formal_コンテナが追加されています。
Bounded_コンテナの実行時エラーチェック強化版?

MKS単位系付きの書式化出力ルーチン群が追加されています。
よくわかりません!

他にも新pragmaとかいっぱいありそうですが把握できてません!

Debianのmingw32-ocamlをDarwin上でビルドしてみた

Debianにはyumというパッケージシステムがありまして、これがmingw32をターゲットにしたクロスコンパイラを取り揃えているのです。すごい。binutilsgccは元々クロス開発ができるように作られていますので、各自でクロスコンパイラを野良ビルドするのも楽勝*1です、が、yumはmingw32-ocamlなんてパッケージまで持ってます。ocamlのビルドプロセスはクロスコンパイルなんて想定されてないわけで、これはすごい。
というわけで(最近ようやくLeopardからバージョンアップして環境再構築中の)Snow Leopard上でビルドしてみましたらなんかできたっぽいのでアップロードしてみます。yumのパッチをそのまま使って、yumがやっているであろうことを手動でちまちまコマンド打って、エラーが出たらちまちま直して、あとディレクトリ構成は好みに合わせて弄りました。

http://panathenaia.halfmoon.jp/alang/darwin/ のmingw32-ocaml.dmgに置いてますので試したい方はどうぞ。
今、ビルドできたやったー、でこれ書いてますので、本当に動くかろくに試してません。動かなかったら教えていただければ幸いです。

インストール先は/usr/localになります。

  1. mingw32クロスコンパイル環境の作り方(復習)
    1. gccに必要なのでgmp、mpfr、mpcをインストール。これはMacPortsやHomebrewを使ってOK。
    2. MinGW-w64のWIN32 Downloadsから適当なビルド済みアーカイブを取ってきて、/usr/local/i686-w64-mingw32/includeと/usr/local/i686-w64-mingw32/libに配置。
    3. bintuilsをconfigure --target=i686-w64-mingw32してインストール。
    4. gccをconfigure --target=i686-w64-mingw32してインストール。
  2. mingw32-ocamlのビルド
    1. yumのディレクトリからmingw32-ocaml_3.12.0+debian3.tar.gzをダウンロード。
    2. 最新の3.12.1を使いたいので本家からocaml-3.12.1.tar.bz2をダウンロード。
    3. mingw32-ocaml.dmgの中のinstall.txtに書いてあるコマンドを上から順に実行していってください。
    4. または、(私の環境とほぼ同じって自信?のある人は)mingw32-ocaml.dmgにあるビルド済みのファイルを使ってみてください。

とりあえずi686-w64-mingw32-ocamloptを動かすのに最低限必要なものは↓だけみたいですので、/usr/localを汚したくない人は↓だけシンボリックリンクでもOKです。全部他に置きたいって人は同梱してあるパッチから更に書き換えてくださいませ。

/usr/local/
  i686-w64-mingw32/
    bin/
      ocamlrun
      flexlink
    lib/
      flexdll/
        flexdll_mingw.o
      ocaml/
        OCamlのランタイムライブラリ

特にDebianならではやDarwinならではって部分はありませんでしたので、ホストがPOSIXでさえあれば同じ手順でクロスコンパイラが作れそう。(最近流行りらしいARMターゲットのOCamlロスコンパイラは全く見てません)

ところで、どうせwine入れるならWindows用のocamlopt.exeを動かしたほうがよっぽど楽ですねそうですね。

*1:ただしホストがWindowsの場合を除く。もちろんターゲットがWindowsでホストがWindows以外なのは楽勝。

デフォルトのテキストエディタを設定するメモ

なんでもかんでもTextEditが開いてうざい、私はTextWranglerを使いたいんだ、という話。
もちろん拡張子ごとに「このアプリケーションで開く」の「すべてを変更...」ボタンを押しまくればいいのですが、テキストエディタで扱う拡張子なんて無数にあるわけで流石に切りがないですからね。

前提知識としてUTIについてざっと書きますと、OSXでは拡張子→UTI→アプリケーションって具合に関連付けが管理されています。以前はクリエーターコードだのなんだの複雑だったのですが、無くなってしまいました。便利だったのですけどねクリエーターコード……。で、「このアプリケーションで開く」はUTIをすっ飛ばしていきなり拡張子→アプリケーションの関連付けを行うわけです。そうではなくて、UTI→アプリケーションの設定を変えることができれば、無数にある拡張子ごとにちまちました設定をしなくて済みます。詳細は検索してください。

問題はこの設定を行う標準添付のUIやコマンドがないという一点のみで、それ用のアプリケーションが必要になります。今まで使っていたRCDefaultAppの最終更新日が流石に昔になってきて不安だったので、今回はdutiを使うことにしました。

まず、(「このアプリケーションで開く」を設定していないファイルを使って)関連するUTIを調べます。

$ mdls  -name kMDItemContentTypeTree てきとうなテキストファイル
kMDItemContentTypeTree = (
    "com.apple.traditional-mac-plain-text",
    "public.plain-text",
    "public.text",
    "public.data",
    "public.item",
    "public.content"
)

↑の各UTIのデフォルトアプリケーションを設定すればいいわけです。あ、UTIは継承関係を持ってまして、端折りますと、このファイルはcom.apple.traditional-mac-plain-textではあるのですが、開く他の各種操作にはこれ全部の設定が絡んでくるってことです。

まず現在のデフォルトアプリケーションを調べましょう。適当にそれっぽいpublic.textから。

$ duti -d public.text
com.apple.TextEdit

出ました憎きTextEdit。この設定を書き換えてしまいましょう。

$ duti -s com.barebones.textwrangler public.data editor
$ duti -s com.barebones.textwrangler public.text editor

com.apple.TextEditはTextEditのバンドル名、com.barebones.textwranglerはTextWranglerのバンドル名、editorはdutiのマニュアルから。editor以外にもいくつかあるみたいです。

適当なファイルを色々開いてみて、大体いい感じになるまで上記手順を繰り返せば完了です。全部やらなくてもpublic.dataとpublic.textだけでほとんど間に合うと思います。関連付けなんて大雑把でいいですよね。

あと、時々、tarアーカイブなんかを展開して出てきた拡張子なしのファイル(READMEだのINSTALLだの)が「Unix 実行ファイル」(UTIはpublic.unix-executable)と推論されていることがあるのですが、これはなぜか実行可能属性が付いてしまっていることが原因ですので、chmod -xしてやればpublic.dataになります。うっかりダブルクリックするとshで実行されてしまって危ないのですよこれが……。

svn:externals代替計画

個人的なソースコードsubversionに全部突っ込んでまして、それなりに便利に使ってたのですが、時代の流れと共にいろいろ不都合が生じてきました。具体的には1.7系列にしたらsvkが動かなくなったのでオフラインコミットができなくなったり(将来的に本家で実装する予定だそうですがとりあえずsvkが動かないのは「今」なのです)、githubを使うようになって二重管理めんどかったり(公開用gitリポジトリの方はgit svn fetchしてmerge --squashしてcommitしてpushしてるだけなので手間はたいしたことないのですが、HDD上に同じソースコードが2つあるってのは何かと面倒を生じるのです)、あとまあいつの間にかbitbucketがプライベートリポジトリ作り放題になってたり最近私はWindowsを全く使ってない等の諸々の事情が合わさってsubversionを捨ててgitに完全移行する障壁が無くなってきた感じです。

という日記はともかく、今回はsvn:externalsの真似の話です。
各種分散型vcsも外部リポジトリを取り込む機能は提供してますが、subversionが便利だったのは、svn:externalsプロパティひとつで各種ゴタゴタを良きに計らってくれる点でした。

  1. svn:externalsでもpartial checkoutできる。
  2. 同じリポジトリ内での相対パスを取り込める。
  3. svkを併用すると、ローカルでの変更をローカルに伝播できる。

具体的には↓な運用。

lib1/
  source/
    lib.ads
app1/
  extlib/ svn:externalsに../../lib/source lib1を設定

この状態で全体をチェックアウトすると↓になります。

lib1/
  source/
    lib.ads
app1/
  extlib/
    lib1/
      lib.ads

lib1/source/lib.adsを更新してupdateするとapp1/extlib/lib1/lib.adsに反映されますし、逆も可。svkを使ってますと全部オフラインでできます(できました)。push先の指定なんかも不要で操作ミスを誘発するような要素もなくて、単純明快です。で、lib1以下だけ、app1以下だけをチェックアウトしても上手く動きます。

これをgitで再現したいわけです。

操作が煩雑になるのはまあ仕方ない。リポジトリを細かく分けないといけないのも仕方ない。app1にlib1を取り込むのはsubmoduleでできます。後はpartial checkout。ここまで前置き。

やりたいこととしては、lib1のmasterはlib1に必要なもの全部入りで、それとは別に外部からlib1を利用するため用に、lib1のsourceのみブランチを用意しよう、と。
http://progit.org/book/ja/ch6-7.html で説明されている例では、ブランチをmasterのサブディレクトリとして取り込み、ブランチ側の更新をmasterでマージしていますが、この逆ができたらいいなと。

とりあえずmaster作ります。

$ mkdir subtree && cd subtree && git init # 実験用リポジトリ
Initialized empty Git repository in ~/subtree/.git/
$ mkdir source
$ edit source/lib.ads # ソースを追加(editはTextWranglerを起動するコマンド)
$ edit manual.txt # ソースコード以外のものも追加しておく
$ git add source/lib.ads 
$ git add manual.txt 
$ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#	new file:   manual.txt
#	new file:   source/lib.ads
#
$ git commit -m "master initial commit"
[master (root-commit) 52b8a83] master initial commit
 2 files changed, 2 insertions(+), 0 deletions(-)
 create mode 100644 manual.txt
 create mode 100644 source/lib.ads

空ブランチを作ります。作り方はgithub:pagesから。

$ git symbolic-ref HEAD refs/heads/sourceonly # 新しいブランチの名前
$ rm .git/index
$ git clean -fdx # (.gitディレクトリ以外の)ファイルを全部手動で消してもいい
Removing manual.txt
Removing source/
$ git status
# On branch sourceonly
#
# Initial commit
#
nothing to commit (create/copy files and use "git add" to track)
$ git branch
  master # sourceonlyブランチは出てこない。initial commitがまだ無いから?

この新しいブランチに、masterのsourceディレクトリをそのままコミットします。

$ git cat-file -p master # masterブランチが指しているコミットオブジェクトを調べる
tree b9231259976ca289eba4c430902a912d9bb12af7 # このtreeが本体だな
author yt <...> 1327636245 +0900
committer yt <...> 1327636245 +0900

master initial commit
$ git cat-file -p b9231259976ca289eba4c430902a912d9bb12af7 # treeの中を調べる
100644 blob ba0c3d41594a6bf8c9c45b8ea750426abc4789c2	manual.txt
040000 tree 493e1a42635421242b367f4793bc1cbd92cbcbef	source # 目的のディレクトリ発見
$ git read-tree 493e1a42635421242b367f4793bc1cbd92cbcbef # sourceディレクトリをそのままindexにする
$ ls # ワーキングコピーには反映されてない
$ git status
# On branch sourceonly
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#	new file:   lib.ads
#
# Changes not staged for commit:
#   (use "git add/rm <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#	deleted:    lib.ads # ワーキングコピーがないためなんか言われてますが無視します
#
$ git commit -m "initial source only"
[sourceonly (root-commit) 84cb94a] initial source only
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 lib.ads
$ git branch
  master
* sourceonly

めでたくsourceonlyブランチができました。
続けてマージのテスト。

$ git checkout master # masterに戻って
Switched to branch 'master'
$ edit source/lib.ads # 何がしかの変更を加えます
$ git status
# On branch master
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#	modified:   source/lib.ads
#
no changes added to commit (use "git add" and/or "git commit -a")
$ git add source/lib.ads
$ git commit -m "change in master"
[master fbdd687] change in master
 1 files changed, 3 insertions(+), 1 deletions(-)
$ git checkout sourceonly # sourceonlyブランチに切り替え
Switched to branch 'sourceonly'
$ ls
lib.ads
$ git merge -s subtree master # サブツリーマージ
Auto-merging lib.ads
CONFLICT (add/add): Merge conflict in lib.ads
Automatic merge failed; fix conflicts and then commit the result.
$ git status
# On branch sourceonly
# Unmerged paths:
#   (use "git add/rm <file>..." as appropriate to mark resolution)
#
#	both added:         lib.ads
#
no changes added to commit (use "git add" and/or "git commit -a")

あれ、なんかコンフリクトしました……?なんででしょう?

$ git reset --hard # というわけでやり直し
HEAD is now at 84cb94a initial source only
$ git merge -s subtree -Xtheirs master # とにかくmasterを使え
Auto-merging lib.ads
Merge made by the 'subtree' strategy.
 lib.ads |    4 +++-
 1 files changed, 3 insertions(+), 1 deletions(-)
$ git diff master # git diffには-sオプションが無いぞ……
# 大量のログ省略
$ edit lib.ads # しょうがないので目視確認、ちゃんとmasterの内容になってそう
$ git log
commit 5f079753cadb727a813556de9b29f2a2851b0552
Merge: 84cb94a fbdd687
Author: yt <...>
Date:   Fri Jan 27 13:30:36 2012 +0900

    Merge branch 'master' into sourceonly

commit fbdd68745883a47099ad386a1a40d2514ad3dca2
Author: yt <...>
Date:   Fri Jan 27 13:20:02 2012 +0900

    change in master

commit 84cb94a7ea53233bc89374413980181694fd0b6c
Author: yt <...>
Date:   Fri Jan 27 12:53:00 2012 +0900

    initial source only

commit 52b8a833a00339de319d8dbd2037fd41e521e34f
Author: yt <...>
Date:   Fri Jan 27 12:50:45 2012 +0900

    master initial commit

後はこのsouceonlyブランチを使う側でサブモジュールとして取り込めばおしまいです。
めでたしめでたし……煩雑ですよねー。
subversionのローカルコミット実装を熱烈希望します!!

ビルドツールの仕事

最近、クロスリファレンスツール(gnatfindとかocamlspotなど)はビルドツール(gnatmakeとかocamlbuildなど)に内蔵されているべきと思うようになってきました。
だってビルドツールはソースの位置とか中間ファイル(大抵クロスリファレンスに必要な情報も持ってる)の位置とか依存関係も含めて全部知ってるじゃないですか、ねえ。

ocamlbuildのメモ

参考URL: http://brion.inria.fr/gallium/index.php/Using_an_external_library

酷評されているocamlbuildを使ってみましたので忘れないうちにメモ。

その1

main.mlがあったとします。

$ ocamlbuild main.native

_buildという作業ディレクトリが作られて、なんやかんややった末に_build/main.nativeができて、カレントディレクトリにmain.nativeのシンボリックリンクができます。

簡単ですね!

  • 疑問点1: このmain.nativeというファイル名は、多くの場合このままでは嬉しくないと思われます。変える方法はあるのでしょうか?
  • 疑問点2: このままですと_buildディレクトリを消したときに実行ファイルも残りません。ユースケース次第ですがこれが嬉しくないこともあるはず。シンボリックリンクではなくて実体のコピーにする方法はないのでしょうか。
  • 疑問点3: _buildディレクトリにmain.mlのコピーができてますが、ハードリンクじゃなくて単なるコピーみたいです。main.nativeはシンボリックリンクなのに何故ソースコードはコピーする……。
  • 疑問点4: しれっとmain.cmoもできてますが、私はネイティブコード生成を指示したのであってバイトコードは別にいらんです。ビルド時間がかかってしょうがないのでこれやめさせることはできないものでしょうか……。

その2

実行ファイルを複数作りたいのでディレクトリを分けました。

exe1/
  main.ml
exe2/
  main.ml
source/
  shared.ml

exe1、exe2それぞれのディレクトリでビルドする算段。
ただ、このままではエラーになります。

$ ocamlbuild -I ../source main.native
Failure: Included or excluded directories must be implicit (not "../source").
Compilation unsuccessful after building 0 targets (0 cached) in 00:00:00.

フルパスにしても同じで、どうやら全てのソースファイルがカレントディレクトリ以下に必要な模様。
なのでリンク。

exe1/
  main.ml
  source -> ../source
exe2/
  main.ml
  source -> ../source
source/
  shared.ml
$ ocamlbuild -I source main.native

めでたしめでたし。

  • 疑問点5: ここでリンクの名前を_sourceみたいにすると無視されてしまいます。ビルドディレクトリも_buildですし、どうやらアンダースコアで始まる名前は無視対象のようです。無視を解除して_sourceという名前を使うにはどうしたらよいでしょうか。
  • 疑問点6: そもそもカレントディレクトリの外にあるソースを使うには、これが本当に正しい方法なのでしょうか?絶対もっといい方法ありますよね!

その3

外部ライブラリを使いたいと思います。
ocamlが初めから持っているBigarrayやUnixやなんかは-tag use_bigarrayで充分なのですが、野良ライブラリの場合は位置を教えてあげる必要があります。なのでmyocamlbuild.mlを書きます。

open Ocamlbuild_plugin;;

dispatch begin function
| After_rules ->
	 ocaml_lib ~extern:true ~dir:("../lib") "gmp";
	 ocaml_lib ~extern:true ~dir:("../lib") "mpfr";
	 ocaml_lib ~extern:true ~dir:("../lib") "unicode";
	 tag_file "main.ml" [
	 	"use_gmp";
		"use_mpfr";
		"use_unicode"];
	 tag_file "main.native" [
	 	"use_bigarray";
	 	"use_gmp";
	 	"use_mpfr";
	 	"use_unicode";
	 	"use_unix"]
| _ ->
	()
end;;

配置としては、main.mlと同じところにlibというディレクトリを作って、野良ライブラリ自身のmakefileでもってlibにライブラリを配置させます。ocamlbuildからは配置済みのライブラリを使おうという戦略です。私はOCamlのライブラリ付属のmakefileは全く信用していないのですが、ocamlmklibやなんかはもっと信用していません。(この例で使っているGMPラッパーなんかは私自身が作ったライブラリですので、そのmakefileは私が書いたものです。なので、私の環境限定で上手く動作することは確実なのでそれを使います。これを読んでいる人は、ライブラリのmakefile(私のものも含めて)も信用しないようにお願いします。必ず自分でビルドしましょう。)

ocaml_libのオプションですが、~extern:trueは、ocamlbuildにビルドさせないために必要です。このパラメータを省略しますと、親切に全部ビルドしようとしてくれやがりますがソースが見つからずにエラーになります。~dirが"../lib"になってますが、これは_buildからの相対パスなのでこうなります。
tag_fileは、_tagsというファイルを作りたくないのでここに書いてるだけです。ひとつのビルドツールのために複数ファイル書きたくないです。

後は、このlibディレクトリ自身が依存関係解析に巻き込まれないように、無視するようオプションをつければOKです。

$ ocamlbuild -X lib -I source main.native
  • 疑問点7: Gmpモジュールが使われていたら自動でuse_gmp扱いにする、みたいな挙動はできないのでしょうか?
  • 疑問点8: ライブラリ間の依存関係は書けないのでしょうか。この例では、実はBigarrayはUnicodeライブラリが使っているだけで、main.mlにBigarrayの文字は出てきません。
  • 疑問点9: -build-dirで_buildディレクトリの名前を変えられますが、そこで階層ごと変えられると_buildからの相対パスは破綻します。カレントディレクトリからの相対パスに直してくれるぐらいしてもいいと思いませんか……。(解決方法: Sys.getcwd () ^ "/lib")

ocamlbuildは標準で入っている素晴らしいツールです。少なくともocamldepを駆使したmakefileを書くよりは良いので、俺ビルドツールを作るよりも先に試してみましょう。俺ビルドツールが散乱してもいいことは何もありませんからね!*1

*1:ちなみにYTの俺ビルドツールはhttps://github.com/ytomino/ocamlmakeにあります。バカですね!

std::stringはもっと速いはず?

http://d.hatena.ne.jp/shinichiro_h/20100823#1282563465の例は参照カウンタを活用すればstd::stringはもっと輝けるはずだ、clear();append(dir);ではなくてassign(dir);すればもっと速いんじゃないか、という実験。

パターン1

void JoinFilePathStr(string const& dir, string const& base, string* out)
    out->assign(dir);
}

void JoinFilePathSp(StringPiece const& dir, StringPiece const& base, string* out) {
    dir.CopyToString(out);
}
Str(const char*, const char*) 0.665250
Str(string, const char*) 0.236847
Str(const char*, string) 0.308445
Str(string, string) 0.011704
Sp(const char*, const char*) 0.174883
Sp(string, const char*) 0.136190
Sp(const char*, string) 0.139241
Sp(string, string) 0.109931

Str(string, string)が他とケタ違いに速い。これはOK。逆に、このパターンでstd::stringのほうが速いことで、実装が参照カウンタを使っていることも確認できます。(basic_stringのソース見たら参照カウンタを使わない条件とかあって複雑でしたので)

パターン2

void JoinFilePathStr(string const& dir, string const& base, string* out)
{
    out->assign(dir);
    out->push_back('/');
}

void JoinFilePathSp(StringPiece const& dir, StringPiece const& base, string* out) {
    dir.CopyToString(out);
    out->push_back('/');
}
Str(const char*, const char*) 0.837721
Str(string, const char*) 0.512278
Str(const char*, string) 0.499792
Str(string, string) 0.279397
Sp(const char*, const char*) 0.202806
Sp(string, const char*) 0.156834
Sp(const char*, string) 0.161091
Sp(string, string) 0.132171

1文字push_backしてるだけ。再アロケーションが必要になるのでStr(string, string)が遅くなるのはわかります。問題はSp(string, string)で、あまり変わってません。ここで逆転が発生するのはおかしい。この速度差は純粋にstd::stringのせいであってStringPieceあまり関係ないんじゃないか……。
どうやら(gcc 4.0の)basic_stringはC文字列からのassign時に余分な領域を取っているらしいです。JoinFilePathSpのほうはそれを利用しているから再アロケーションが発生していない。逆に実データが共有状態ですと、領域が仮に余っていても、常に共有解除するから遅いのではないかと。で、Str(string, string)がSp(string, string)の2倍遅いのは、共有解除とpush_backで2回コピーが起きてるのではないか?と予想できます。共有解除と連結を一気に行うようにしてしまえば、同じぐらいの速度になるはず。要するに、この例でStringPieceが速いのはStringPieceの手柄ではなくて、単にbasic_stringの実装がタコなせいではないでしょうか……。

パターン3

void JoinFilePathStr(string const& dir, string const& base, string* out)
{
    out->assign(dir);
    out->reserve(dir.size());
}

void JoinFilePathSp(StringPiece const& dir, StringPiece const& base, string* out) {
    dir.CopyToString(out);
    out->reserve(dir.size());
}
Str(const char*, const char*) 0.820524
Str(string, const char*) 0.498222
Str(const char*, string) 0.498238
Str(string, string) 0.273923
Sp(const char*, const char*) 0.185341
Sp(string, const char*) 0.156204
Sp(const char*, string) 0.147269
Sp(string, string) 0.121723

ソースを見るかぎりpush_backはreserve(size() + 1);してるだけでしたので、reserveによる共有解除だけにしてみました。
先の予想はハズレです。同じ長さへのreserveだけでも遅い。なんだこれは。長さが変わらないので純粋に共有解除だけ、つまりアロケーションとコピーが各1回必要で、CopyToStringと同じ=Sp(string, string)と同じになるはず、と信じたかったのですが……。
一方、Sp(string, string)に変化はありません。最初から共有していない状態で、長さも変わらないからreserveは何もしません。当然ですね。
というわけで予想2。reserveの実装が極端に酷い。
……ソース見てるんですが、reserve→_M_cloneと呼ばれてアロケート(_S_create)もコピー(_M_copy)も1回ずつしかやってませんね……なんでしょう??

パターン4

void JoinFilePathStr(string const& dir, string const& base, string* out)
{
    out->clear();
    out->append(dir);
}

void JoinFilePathSp(StringPiece const& dir, StringPiece const& base, string* out) {
    dir.CopyToString(out);
}
Str(const char*, const char*) 0.624683
Str(string, const char*) 0.298058
Str(const char*, string) 0.299669
Str(string, string) 0.055530
Sp(const char*, const char*) 0.178162
Sp(string, const char*) 0.135845
Sp(const char*, string) 0.138761
Sp(string, string) 0.106734

clear();append(dir);に戻してみました。これもアロケーションとコピーが1回ずつというのは同じですので、変わらないはず……と思いきやappendが速い!CopyToStringよりも速い!

謎すぎます。

ここから考えられるのは……ということで予想3。clear();は領域を開放しない。このベンチマークはjoined変数を使い回しているため、clear();append(dir);では再アロケーションが発生していない。つまりこのベンチマークは元々意味がないのでは?shinichiro.hさんごめんなさい。

パターン5

define BENCH(msg, expr) do {                                           \
        time_t start = clock();                                         \
        for (int i = 0; i < 1000000; i++) {                             \
            string joined;                                              \
            expr;                                                       \
        }                                                               \
        int elapsed = clock() - start;                                  \
        /*assert(!strcmp(joined.c_str(), "/tmp/hoge.c"));*/                 \
        printf("%s %f\n", msg, (double)elapsed / CLOCKS_PER_SEC);       \
    } while (0)

joinedをforループ内に移動して、毎回デストラクトされるように。

5-1 assign/CopyToStringのみ

Str(const char*, const char*) 0.678821
Str(string, const char*) 0.358226
Str(const char*, string) 0.359924
Str(string, string) 0.126315
Sp(const char*, const char*) 0.368007
Sp(string, const char*) 0.328823
Sp(const char*, string) 0.323669
Sp(string, string) 0.298406

5-2 push_back

Str(const char*, const char*) 1.079381
Str(string, const char*) 0.684841
Str(const char*, string) 0.679538
Str(string, string) 0.330640
Sp(const char*, const char*) 0.687556
Sp(string, const char*) 0.653732
Sp(const char*, string) 0.646298
Sp(string, string) 0.619009

5-3 reserve

Str(const char*, const char*) 1.063986
Str(string, const char*) 0.674232
Str(const char*, string) 0.663562
Str(string, string) 0.315969
Sp(const char*, const char*) 0.374639
Sp(string, const char*) 0.339220
Sp(const char*, string) 0.342732
Sp(string, string) 0.299357

5-4 clear();append(dir);

Str(const char*, const char*) 1.030788
Str(string, const char*) 0.639979
Str(const char*, string) 0.636695
Str(string, string) 0.286971
Sp(const char*, const char*) 0.367424
Sp(string, const char*) 0.329775
Sp(const char*, string) 0.324559
Sp(string, string) 0.297469

ちゃんとパスを連結するように戻すと

void JoinFilePathStr(string const& dir, string const& base, string* out)
{
    out->assign(dir);
    out->push_back('/');
    out->append(base);
}

void JoinFilePathSp(StringPiece const& dir, StringPiece const& base, string* out) {
    dir.CopyToString(out);
    out->push_back('/');
    base.AppendToString(out);
}
Str(const char*, const char*) 1.452504
Str(string, const char*) 0.998036
Str(const char*, string) 0.999712
Str(string, string) 0.642984
Sp(const char*, const char*) 0.898612
Sp(string, const char*) 0.857780
Sp(const char*, string) 0.849120
Sp(string, string) 0.825539

おお!予想通りの計測時間が出てきました!
見ての通り、いい勝負してます。

2つの引数で変換が発生するとstd::string不利ですが、これも予想通りですしね。元のshinichiro.hさんのベンチマーク程は大差がついていません。結論:暗黙の変換が発生するとしても、(仮に一時的にでも)共有状態を作り出せるならstd::stringを引数にしてOK。

ただしC++0xでは……。