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のaspectはC++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をターゲットにしたクロスコンパイラを取り揃えているのです。すごい。binutilsやgccは元々クロス開発ができるように作られていますので、各自でクロスコンパイラを野良ビルドするのも楽勝*1です、が、yumはmingw32-ocamlなんてパッケージまで持ってます。ocamlのビルドプロセスはクロスコンパイルなんて想定されてないわけで、これはすごい。
というわけで(最近ようやくLeopardからバージョンアップして環境再構築中の)Snow Leopard上でビルドしてみましたらなんかできたっぽいのでアップロードしてみます。yumのパッチをそのまま使って、yumがやっているであろうことを手動でちまちまコマンド打って、エラーが出たらちまちま直して、あとディレクトリ構成は好みに合わせて弄りました。
http://panathenaia.halfmoon.jp/alang/darwin/ のmingw32-ocaml.dmgに置いてますので試したい方はどうぞ。
今、ビルドできたやったー、でこれ書いてますので、本当に動くかろくに試してません。動かなかったら教えていただければ幸いです。
インストール先は/usr/localになります。
とりあえず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を動かしたほうがよっぽど楽ですねそうですね。
デフォルトのテキストエディタを設定するメモ
なんでもかんでも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プロパティひとつで各種ゴタゴタを良きに計らってくれる点でした。
具体的には↓な運用。
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では……。