Note

サイト
最近のコメント
 

2012-01-27

[] 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のローカルコミット実装を熱烈希望します!!

2011-12-16

[] 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

ビルドツールの仕事

最近クロスリファレンスツール(gnatfindとかocamlspotなど)はビルドツール(gnatmakeとかocamlbuildなど)に内蔵されているべきと思うようになってきました。

だってビルドツールはソースの位置とか中間ファイル(大抵クロスリファレンスに必要な情報も持ってる)の位置とか依存関係も含めて全部知ってるじゃないですか、ねえ。

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

camlspottercamlspotter 2011/12/18 08:17 逆にビルドツールはコードの中までは知らないので、その方向で進むとビルド、コンパイラ、各種ツールがすべて一体となった統合開発環境という糞へと進むだけだと思います。GHC はコンパイラとビルダが一体になってますけど Haskell を離れるとあまり役に立たない。我慢して別ツールにしつつ、緩い連携を探ったほうがよいと思います。
ocamlspot のデータファイルは ocamlc が呼ばれたときのコマンドラインを記憶しているので、ビルドシステムからの情報をそれなりに引き継いでることになります。知りたいファイルのファイルシステムでの位置を求めることが可能です。

ytqwertyytqwerty 2011/12/18 18:58 えー、統合開発環境いいじゃないですか。
エディタはともかく、最低限ビルドツールとコンパイラは一体になっているべきと思います。依存関係を割り出すためにヘッダをパースして、各ファイル毎に別プロセスが立ち上がって同じヘッダをパースして、って無駄ですよ……。

2011-11-21 週末は雨だった

[] Delphi文字列並の効率を実現できるようにUnbounded_Stringを改造したい

何度も書いてますが、言語組み込みの文字列とライブラリ提供の文字列では効率に天と地の差があります。C++みたいになるべく自然に使えるように努力している言語ですら、その差は到底埋められていません。ましてやAdaのUnbounded_Stringなんて(略)ですよ。

Delphiの文字列のスゴイところと、どうすればおんなじ風にできるかなー、ってのをダラダラやってた話です。あ、ここでいうDelphiの文字列というのは2007以前のAnsiStringのことです。2009以降については正直静的言語なのにコードページを実行時に持つ必要は無いと……どうでもいい。

実はD言語の文字列のほうがもっとスゴイのですが、あのスゴさはGC前提なので省略。

Delphi2〜Delphi2007のAnsiStringの構造

「参照カウンタ、長さ、文字列本体、(C互換用のゼロ終端←以後省略)」というメモリブロックへの「ポインタ」です。

まあこの構造自体はstd::stringでも同じでしょう。0xでは参照カウンタでの共有は許されなくなったんでしたっけ?

で、早速Adaには難点が。Adaでは(C++で言う)デストラクタを実現するためには、Ada.Finalization.Controlledを継承しなければいけないのですが、そうすると当然VMTが必要になるわけです。つまりポインタ2つの組になってしまうわけです。若干メモリの無駄

DelphiC++Ada
×

レジスタに入る

AnsiStringの実体はポインタ1つだけです。ということはレジスタに入ります。

std::string(C++03以前)も、実体はポインタ1つだけにできますが、レジスタに入りません。メンバ関数にthis(ポインタを1個だけ持つクラスへの更にポインタ)を渡さないといけないからですね。*1

もちろん上記のようにAdaでは最低でもポインタ2つ必要になりますので、レジスタには入りません。

DelphiC++Ada
××

文字列定数

AnsiStringの定数は、静的に確保されてます。参照カウンタには-1が入ってます。コンパイラ最初から文字列の構造を把握してるからこそですね。

一方、std::stringもUnbounded_Stringも、コンストラクタ/To_Unbounded_Stringに定数文字列を渡されても、動的にメモリを確保して中身をコピーする必要があります。C/C++もAdaも文字列定数は単なる文字型の配列であり、参照カウンタも長さデータも持ってないからです。

そのため、ちょっと構造を変えましょう。

「参照カウンタ、長さ、文字列本体」→「参照カウンタ、長さ、文字列本体へのポインタ、(文字列本体)」

動的に確保する時は、「文字列本体」まで一緒に確保して、「文字列本体へのポインタ」メンバには「文字列本体」(要するにすぐ次のアドレス)を入れます。

静的に確保する時は、「文字列本体」は確保せずに、「文字列本体へのポインタ」メンバには実定数へのポインタをそのまま持たせます。

……実はこれだけだとダメです。

仮に引数の型をchar const *やaccess constant Stringにしたとしても、ローカル変数(定数)へのポインタを渡されたりすると、そのまま使うわけにいかないですよね。呼び出し元の関数を抜けると消えちゃいますから。

というわけで安全に文字列定数を作るためには、引数で渡された定数が本当に定数であることの保証が必要になります。D言語ならimmutableって便利なものがあるのですが、C++やAdaにはありません。

ただAdaのaccess型(ポインタ)はスコープに紐付けられた型ですので、ライブラリレベル(一番外)のaccess型を使えば、関数を抜けるなどで消えたりはしないことだけは保証できます。それでも、Adaのaccess constantはC++のconst *と同じで、実際に指しているものは変数かもしれません。中身が書き換わる恐れだけは無くせません。

DelphiC++Ada
××

引数渡し

AnsiStringは参照カウンタで管理されていますが、引数渡しの時は参照カウンタが増減されません。

実引数は関数呼び出しよりも寿命が長いことが確実だからです。

C++やAdaでこれを避けるには参照渡しにするしかなくて、そうするとポインタのポインタになってしまって、無意味な逆参照が1段入ってしまいます。引数として渡ってくるわけですからどんなにコンパイラが賢くてもひとつの関数内だけの最適化では取り除けません。LTOなら話は別。

まあAdaは特に、継承を使っている型は明示しなくても問答無用で参照渡しになってしまうわけですが。

DelphiC++Ada
××

世の中にはStringPieceなんかもありますが、折角の参照カウンタが活用されないため、あんまり意味無いかなあ。(リンク先の例だと中でCopyToStringするのも呼び出し元で一時的なstd::stringを作って中では参照カウンタを増やすだけってのも大して変わらないはず?out->append(dir);をout->assign(dir);に変えたら……と思って試してるのですが……続きは↓↓)

返値

返値も今までの話と似たようなものです。

AnsiStringはレジスタで返せて、その際参照カウントも増減しません。

std::stringはレジスタでは返せずに(暗黙の引数が必要になる)、コピーコンストラクタと一時変数のデストラクタも走ります。(0xならムーブコンストラクタと一時変数のデストラクタ)

Unbounded_Stringもレジスタでは返せずに(暗黙の略)、やっぱりAssignと一時変数のFinalizeが走ります。

もちろん定数を返すとわかっているなら参照返しにすればよいのですが、std::stringやUnbounded_Stringは上記のように定数であっても作成時点で中身全部コピーしたりしますので参照返しにして参照カウンタの操作を省略できたやったー、なんてやっても虚しいだけです。最終的に単なるポインタのレジスタ間コピーだけになるAnsiStringだからこそ活きる最適化ですよね。

DelphiC++Ada
××

連結

文字列を一気に3つ連結したりする時も、AnsiStringならコンパイラが知ってますからまとめて連結してくれたりします。(ランタイムには_LStrCat3とかある)

std::stringやUnbounded_Stringの連結は、演算子オーバーロードで実現されいるわけですが、「A & B & Cを一括で処理するオーバーロード」は定義できませんので、ちまちま連結していくしかないです。

ただ少しだけあがくことは出来て、「A & B」の時点で領域を多めにとっておいて、「& C」がそこに入るなら再アロケート無し、みたいな頑張り方はできます。もちろん「& C」されなれば無駄な領域ですのでふつーの発想だとやらないと思います。後はRopeにするぐらいかなあ。

DelphiC++Ada
××

というわけで結論

Unbounded_Stringをいろいろ弄ってたんですがあまり改善できそうにないです。

要は失敗談でした。

[][] 4.7がStage 3に入ったので試してみた

  • 2012対応状況
    • use all type → 「変数 := 式;」の形の式の部分でのみOK。
    • Implicit_Dereference → 関数呼び出しの返値でのみOK。
    • range-based for(違) → OK、ありえんぐらい酷いコードが出てくるけど動く。Ada.Iterator_Interfacesもちゃんとある
    • operator () (違) → OK、Constant_Referenceが呼ばれないような気もするけど。
    • 後は4.6のときと同じ。
  • その他気になる改善
    • protected型がちゃんとread-write lockしてくれるようになってます
    • Controlled型がリンクリストを作らなくなってます。*2
    • Unbounded_Stringに参照カウンタの実装も追加されたみたいです。*3でもデフォルトでは無効っぽい。

[][] 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では……。

*1:ただしC++Builderには__declspec(delphireturn)なるものがありましてレジスタに入れることができます。

*2:上では余計なデータはVMTだけみたいに書いてますが、実はgcc 4.6までは前後へのリンクも持ってました。

*3:上ではあたかもUnbounded_Stringが参照カウンタで実装されてるように書いてますが、gcc 4.6までは毎回コピーされてました。

2011-11-10

RSSアンテナ作りました

http://panathenaia.halfmoon.jp/birdeyes/

やっぱり何番煎じかわからないのですが……。

愛用させていただいていたI knowが終了してしまって困ったことは、「複数のRSSをまとめた結果をRSSとして再出力できるWeb上のRSSリーダーがない」ということでした。(ダッシュボード上のRSSリーダーで更新チェックしてます)

で、Yahoo Pipes存在を教えていただいて、しばらくそれを使っていました。こいつはGUIプログラミングでRSSを加工できるかなり楽しいツールなのですが、ふつーのRSSリーダーのようにサイトをひとつ追加しようと思えば、Fetch Feedを配置してUnionにつないで……と、まあ、めんどいわけです。そのせいですっかり購読しているサイトが少なくなってしまいました。

次に、無いなら自分で設置しよう、ということでCGIアンテナを探したわけです。その中ではRNAが私の要件を満たしてるっぽいです。

一応I knowでできてRNAでできないことはありまして、RNAはサイトごとに、最新記事だけを表示するか、各記事をばらして表示するかの設定ができません。各サイトの最新記事だけをまとめたRSS、各サイトの各記事を全部含めたRSSは作れます。その設定を混ぜることができません。I knowではできたんですよね。

例えば個人のブログサイトなんかは、更新されていることがわかれば充分ですので最新記事だけでよくて、むしろ過去の記事は混ざってほしくないわけです。

逆に、ニュースサイトなんかは、タイトルを見て開くかどうかを決めますので、全タイトル出てきて欲しくて、ブログとは逆に出元のニュースサイトがどこかは意識しないわけで、バラバラに時系列順に並んで欲しいわけです。

というわけでその機能付きの俺アンテナです。

あとI knowでできたことと言えば、RSSを公開していないサイトもdiff取ってRSSに載せてくれたのですが、はてなアンテナのRSSを取り込めば実現できますので実装していません。負荷ははてなに負わせる作戦。

あとインポート/エクスポート形式は、ふつーOPMLと思うのですが、たまたまI knowのデータをLIRSでしか保存してなかったのでLIRSのみ対応してます。

続きを読む

2011-10-01 おぼえてない

--

2011-09-27

[] libxml2は更に酷かったという話

先述の循環参照への対応実装しました。これでもうopaque typeが変な使われ方をしていても大丈夫!ええ、めんどくさかったですが。

でも、libxml2には更にその先があったのです……。

xmlregexp.h

/* 宣言A */
#include <tree.h>
/* 宣言Bを使う */

tree.h

/* 宣言B */
#include <xmlregexp.h>
/* 宣言Aを使う */

……。(あ、インクルードガードは省略してるだけでちゃんと書かれてます)

さあ、窓から物を投げる例のAAを探してこないと……。

これやられると本当にもうどうしようもないので、やらないでくださいね!

2011-09-21

[] Exceptional C++を焚き付けにする話

引きずって申し訳ありませんが、先のranhaさんの発表を聞いて以来、Exceptional C++を読み返しています

で、116ページでとんでもない文を見つけてしまいました。

先行宣言で十分な場合は、決してヘッダをインクルードしないこと。

とんでもない!この一文だけでこれは禁書です、燃やしましょう!

……ええ、ええ、説明しますとも。

私がCのヘッダファイルトランスレータを作っていることは前に書いたとおりです。変換できるライブラリも割と順調に増えて、そろそろ標準Cライブラリはカバーできるのではないかってあたりは視野に入ってきました。最終目標windows.hはまだまだ遠いですが。

で、今libxml2対応しようとして「どうしようか、これ……」ってなってるところです。具体的には次のようなパターン

one.h

struct T;
typedef struct T *t_ptr;

two.h

#include "one.h"
struct T { t_ptr mem; };

one.hでは、struct Tをopaque typeとして宣言して、そのポインタだけを使ってます。全く問題ありません。

two.hでは、struct Tに実体を与えて、そこで自身のポインタをメンバに持ってます。全く問題ありません。

問題は、two.hがone.hを#includeしているため、必ずone.hがtwo.hより先に来ることです。

今まで変換に成功した他のライブラリにもこの手のパターンはあったのですが、幸いにもtwo.hがone.hを#includeしてはいなかったため、#includeの順番を調整することで回避できました。これは直接#includeが書かれてしまっていて順番を入れ替えることができません。

……ええ、ええ、意味不明ですね。

C言語では、opaque typeとその実体は依存関係なく別々に宣言可能です。これははっきり言って珍しい機能であり、C言語特有の超便利な機能です。もっと言えばC言語がモジュールを持たないからこそ実現できている機能です。

まともなモジュールがある言語でこれを表現するには……循環参照が必須ですよね、ですよね。

んでもってまともなモジュールがある言語はフツー公開部分の循環参照が禁止されてますよね、ますよね。

どうしろと!?

D言語は確か循環参照できたよなー(うろ覚え)……とかそういう問題ではなくて。

まともなモジュールがある言語では、struct Tの宣言箇所を1箇所に絞らないといけないわけですよ。

まりopaque typeの宣言と実体の宣言が別ヘッダにあることを検出して、内部的に循環参照に置換しないといけないわけです。

(実体の宣言をtwo.hからone.hに移動するというのはダメです。実体はtwo.hで宣言されている他の何かを使ってるかもですし、opaque typeを宣言してるのはone.hだけじゃなくもっとあるかもしれませんし)

one.d

import two;
alias struct_T* t_ptr;

two.d

import one;
struct struct_T { t_ptr mem; }

Adaの場合はもっと面倒です。普通には循環参照できないので、どっちかにlimitedを付けないといけません。

limitedを付けた参照先はaccess型(ポインタ)経由でしか扱えなくなりますので、one→twoとtwo→oneのどっちに付けるか判定が必要です。

更にAdaはCのようなやわらか型言語とは異なりvery very strong-typingな言語ですので、複数ヶ所でaccess型(ポインタ)を宣言するとそれぞれ別の型になってしまいます。

これを避けるために私のトランスレータではポインタ型の宣言は元の型と同じ場所に固めることにしています。

つまり本当であればstruct T *やstruct T **に相当する宣言はtwoに入ります。で、ポインタのtypedefみたいなのは、「元の型と同じ場所(two)で宣言されてるポインタ型」の別名として定義するようにしてます。

limited withしますとこれが使えなくなります。ポインタと言えどもlimited with先の型を値として使おうとしていることには変わらないからです。

つまり元の型に対するポインタ型を2箇所で宣言する羽目になり、strong-typingな言語ではこれが結構痛いわけです。

one.ads

limited with two;
package one is
   type t_ptr is access all two.struct_T;
-- subtype t_ptr is two.struct_T_ptr; ← エラーになる
end one;

two.ads

with one;
package two is
   type struct_T is record mem : one.t_ptr; end record;
   type struct_T_ptr is access all struct_T; ← struct T*が使われているので自動生成
end two;

↑のone.t_ptrとtwo.struct_T_ptrに互換性はありません!

追記: twoからはoneは全部見えてるんだから全部one.t_ptrを使うようにしてしまえば宜しいのでした。あほか私は……。

この時点でもう実装をあきらめたいレベルなわけですが、これにインライン関数でもくっついてて、struct T::memに触られてたりすると……。

このトランスレータは今のところAdaしかサポートしてませんが、最終的は汎用にしたいわけです。循環参照ができない言語ですと(Dなんかが例外であってほとんどの言語がそうでしょう)……one.h内で使われている分はvoid *扱いにでもするしかないですよねもう。

というわけで、先行宣言を別のヘッダファイルに泣き別れにするのはやめてください、

先行宣言は同じファイル内だけで完結させてください、お願いします!

(理由: 他の言語に移植できなくなるからC言語を滅ぼす日が遠くなる)

っていうかostreamの宣言がclass直からtemplateになったために<iofwd>ができたってすぐ前のページで書いてるんですから、自前の型も後でtemplateにするかもしれないわけですから、先行宣言をあちこちに書き散らかすなんて推奨するのはやめましょうよHerb大先生!これがありなら関数のプロトタイプだって使う場所で別個に書けば#include要らないよねやったーって話になるじゃないですかー。

2011-09-04

エラーハンドリング勉強会

の懇親会?みたいなのが無かったので(まあ私は聴講者その1に過ぎなかったのですが)、発表者のranhaさんと、nihaさんとeldeshさんを呼び出して(なんという不遜、なんという豪華メンバー)秋葉原で適当に話してきました。ありがとうございます

覚え書き。C++0xにはパターンマッチ可能な多相バリアントが追加される。

型。

std::exception_ptr var;

値をboxing。

try{
   throw 任意の型の任意の値;
}catch(...){
   var = std::current_exception();
}

追記。専用の関数が用意されているそうです。

var = std::make_exception_ptr(任意の型の任意の値);

値をパターンマッチで取り出す。

try{
  std::rethrow_exception(var);
}catch(int i){
   ...
}catch(std::string s){
   ...
}catch(...){
   ...
}

これはSMLでも使用実績のある由緒正しいテクニックらしい。

メモhttp://www.nminoru.jp/~nminoru/programming/stackoverflow_handling.htmlを穴が開くまで読む。

あとどうでもいいじまん。多相バリアントによる先頭要素を保証した異種リストhttps://github.com/ytomino/headmaster/blob/master/source/c_parser.ml で実際書いたことがありまして、冗長なことに目をつむれば便利なものでした。コンパイラを書くのに最適な言語は間違いなくO'Caml、私の中では。

wraith13wraith13 2011/09/05 10:37 値を boxing するところのコードですが、それ用の関数がちゃんと標準ライブラリで提供されていまして std::make_exception_ptr() を使えば一発ですよ! (^ρ^)

FlastFlast 2011/09/05 11:46 それ自体はそれほど難しくなくて、普通にC++03の範囲でも実装することができます。

ex) http://ideone.com/f2t27
要はboxingするときに完全に型が分かっていれば、必要な関数をtype erasure等で用意することができるわけです

h_sakuraih_sakurai 2011/09/05 12:32 また欠席してしまった。うぁぁあぁあ。まぁいいや

ytqwertyytqwerty 2011/09/05 14:40 >wraith13さん
もうバリアントとしての用途が想定されているとしか思えない\(^o^)/

>Flastさん
ああいえ、Boost.Anyもありますし、できるのは知ってたんですよ。
例外に使われるのが面白いなーと。

>h_sakuraiさん
会場でh_sakuraiさん探しましたよ!残念です。

2011-09-03

エラーハンドリング勉強会

明日http://partake.in/events/9874b92a-4cf0-4a20-a3fe-951239da5612に出かけること忘れるな俺。

あれ……↓の広告はなんでしょう?今までこんなのが入った記憶は無く。

catch(...)とかwhen othersとか

こういう「すべての例外を受け取る」機能って、本当にすべての例外を受け取れるとは限りませんよね……。もちろん実装次第ではあるのですが。

具体的には「他の言語ランタイムから投げられた例外」。g++とgcjとGNATで関数ポインタ交換しあって例外投げまくったらどうなるかとかそういう話です。pthread_cancelとかその手のも。あとGNATですとAbort_Signalという特殊な例外は明示的にwhen Standard'Abort_Signalって書くと受け取れるのですがwhen othersには引っかかりません。違うコンパイラですとBorland C++のlongjmpは抜ける途中の関数にあるオブジェクトのデストラクタは動かしてくれますが、当然catch(...)にはかかりません。

で、そういうことを考えると、よくある↓のイディオムが使えなくなります。

nanika_t allocate_and_initialize()
{
  nanika_t *p = new nanika_t;
  try{
    initialize(p);
    return p;
  }catch(...){
    delete p; /* initializeで例外発生 */
    throw;
  }
}

これだけですとauto_ptrやunique_ptrを使えと言われそうですが、メモリ管理に限らなければ、全部の例外を一度受けて巻き戻し処理を行って再度投げる形は、結構使われているはず。

try {
   /* なんかする */
} catch(...) { /* 実は全ての例外を取れないかもしれない */
  /* 巻き戻し */
  throw;
}

なんで同ランタイムの例外しか受け取れないかといいますと、知らない例外を受け取っても例外オブジェクトの解体方法なんかがわかりませんし、バックトレース情報なんかも更新できませんし。

finallyやデストラクタは、例外オブジェクトの方に触れる必要がありませんので、gccでしたらdwarf、WindowsでしたらSEHみたいな共通の枠組みに則っていれば例外を投げたランタイムを問わずハンドリングできます。

というわけでfinallyが使えれば↓でいいのですが、

bool completed = false;
try {
   /* なんかする */
   completed = true;
}finally{
  if(!completed){
    /* 巻き戻し */
  }
}

このcompletedって変数が嫌な感じです。break禁止な糞コーディング規約の元で無理やりフラグ立ててループを抜けているのと同じ感じがします。

というわけで↓みたいに書きたいなあ、という話でした。

try {
   /* なんかする */
} catch_all_and_rethrow {
  /* 巻き戻し */
}

常に再度投げるとわかっていれば、内部的にもcompletedみたいな変数は不要にできないかな……具体的には例外の種類を判別したりせずに一旦全部受けて、例外オブジェクトには一切手を付けないでそのまま_Unwind_Resume_or_Rethrowに任せたりでいけそうな。あ、でも/* 巻き戻し */の処理中に別の例外が発生したら、結局前の例外オブジェクトは解体せざるを得ませんので、お手上げですね……。

EgtraEgtra 2011/09/04 03:59 SEHならEXCEPTION_CONTINUE_SEARCH、D言語のscope(failure)、C++ならunique_ptrで削除子指定 + 成功時releaseあたりなんかどうでしょうか。

SaitoAtsushiSaitoAtsushi 2011/09/04 05:37 広告が入るようになってのははてなダイアリの側で変更があったようです。
http://d.hatena.ne.jp/hatenadiary/20110819/1313721142
設定ページで従来の挙動に戻すことは出来ます。

ytqwertyytqwerty 2011/09/04 10:58 >Egtraさん
ええ、scope(failure)は求めてるものそのものですね(try文みたいに書きたいというのは置いておいて)。unique_ptrは、finally効果のある機能がデストラクタしかないなら仕方ないのかな……unique_ptrでスコープガードを作る話は意外と先人が多くいてびっくりしました。

>SaitoAtsushiさん
ありがとうございます。消してみました。消せてるかな。

2011-08-20 家の前で交通事故がありました

[] Ada2012で追加される文字判定サブプログラム

http://www.ada-auth.org/standards/12aarm/html/AA-A-3-2.html

Is_Line_Terminator, Is_Mark, Is_Other_Format, Is_Punctuation_Connector, Is_Spaceが追加されます

Unicodeカテゴリともちょっと違うようですし(ていうか素直にIs_XXXをUnicodeカテゴリの全種類分並べるだけではなぜダメだったのか)、下の方の説明を読んでもいまいちピンと来ないのですが、{AI05-0185-1}のリンクをたどるとこの正体が明らかになります。

The current version of the GNAT compiler has defined the following implementation-defined packages;

http://www.ada-auth.org/cgi-bin/cvsweb.cgi/ai05s/ai05-0185-1.txt?rev=1.7

まじか……。

文字コードに対するAdaCoreのナンセンスさ加減はcomp.lang.adaでもある程度共有された認識だと信じていたのですが。

ということで、a-zchuni.adsを読むとこれらのサブプログラムの正体がわかります。

サブプログラム 対応するUnicodeカテゴリ
Is_Line_Terminator Zp(Paragraph_Separator), Zs(Space_Separator), 0a-0d(LF, VT, FF, CR)
Is_Mark Mn(Nonspacing_Mark), Mc(Spacing_Mark) *1
Is_Other(_Format) Cf(Format)
Is_Punctuation(_Connector) Pc(Connector_Punctuation) *2
Is_Space Zs(Space_Separator)

名前ぐらいUnicodeカテゴリを正確に反映しろよ……っていうか私Unicode詳しくないんですが、MnとMcだけを判定したくてMeを除きたいような用途ってなんかあるんでしょうか?詳しい人がいましたら教えて下さい、お願いします。

流し読みですが、Is_Punctuation_ConnectorのほうはRandy大先生が_Connectorを追加させた様子。a-zchuni.adsのIs_Punctuationのままですと、P系カテゴリ全部含むと誤解しちゃいますもんね。とりあえずその程度には元のa-zchuni.adsは酷い。

で、Handlingのサブプログラムは、Maps.Constantsのほうに対応する集合が定義されてるはずなんですが、まだ追加されてないです。

http://www.ada-auth.org/standards/12aarm/html/AA-A-4-6.html

要するに他のベンダーはこんなの実装せずに無視しろってことですよねわかります。

[] ところで

http://www.ada-auth.org/standards/12rm/html/RM-A.html

を眺めていて、Wide_やWide_Wide_バージョンが必要な大抵のpackageはAda.直下にWide_なんとかがあるのですが、StringsだけAda.Strings.Wide_なんとかの形になる(他と同じならAda.Wide_Strings.なんとかになるはず)のが気になって夜も眠れないのは私だけでしょうか?

*1:Me(Enclosing_Mark)は含まれない

*2Pd, Ps, Pe, Poは含まれない

 
カレンダー
2004 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2005 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2006 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2007 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2008 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2009 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2010 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2011 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2012 | 01 |