ToDo:
ここ最近、SADをコンパイル時の最適化レベルを上げてコンパイラのバグを 踏んづける事例が何例か報告されていますが、i386系で GCC使う場合 -O1と -O2ってそんなん差が出ます? 多分、Intel Fortranを使う方が全然はやいと思うのですが... (昔からGCCの-O2は怪しい環境が多いのです)
あと、数値計算だと内部的にいろんなコードが使っている 線形代数や FFTのコードを最適化済みの(例えば、SSE/Multi Threadに 最適化した)ライブラリに置き換えた方が、早くなりそうな気がします。 おまけに、外部化すれば保守が簡単に...
特に、線形代数ライブラリーに関しては、BLAS/LAPACKという デファクトスタンダードな APIがある上に、有償ですが ベンダーが最適化済みのものを提供しています
FFTに関しては、fftwかな?
以下の三つのファイルがコンパイル出来ないのでSADのベンチマークは 取れていませんが、フリーのコンパイラでは LLVM-GCCが期待大です (小さなプログラムでは Intelコンパイラに近い性能が出ます)
よく考えれば当たり前の話ですが、シェルの検索パスとして使える パス名を構成する文字には":"は含まれません
所謂、Bourne shell /bin/shの検索パスは、環境変数PATHに ":"を区切り文字として列挙されるので、":"を含んだパスは 使えません。「バックスラッシュエスケープ出来る」と思う人も 居るかもしれませんが、PATH変数としては"\\"は ただの文字リテラルなのでエスケープ出来ません。
これが、/bin/cshだとシェル変数pathは空白区切りで 設定するので、csh自身がコマンドを探す場合は問題なく 使えてしまうのですが、コマンド検索が/bin/shや system call 経由になった瞬間に誤作動しますので、要注意です。
似たような例に、makeのソースやターゲット名に":"を 含むファイル名が使えないと言う罠もありますね
SADScriptの関数は、内部エラーに関して itfmessage_()関数経由で エラーを通知するものがあります。 これらのエラー通知は、関数の返り値ではなく Messageとして送出され、 Messageが送出された事実はCheck[]関数でトラップすることが 出来るのですが、発生したMessageとその内容を知る術が実はありません Orz コマンドラインからの実行に対しては、MessageList[]関数がありますが Messageの名前だけで付属のパラメータは記録されていません。
つまり、ユーザー定義の関数で、例外が起こり得る関数を呼び出す際に Check[]で例外をトラップし、処理可能な例外は関数内で処理し、 処理できない例外に対しては関数の外側に例外を転送する処理が書けません。
仕方ないので、Add\$Message[]関数に細工して\$MessageLastに 最後に送出されたメッセージを記録するようにしました。
が、Check[]関数では例外を検知しますが、例外発生時点で式の評価が 止まるわけでは無いので、真面目に例外処理を書こうとすると面倒ですね。
Try[]ブロック構文でも、新設すべきかな?
C++の例外機構を知っている人間が勘違いしやすいものに、Catch[]関数が あるのですが、これはThrow[]関数による式評価からの非局所脱出を捕まえる 構文で、例外処理ではない罠。 これって純粋関数とReturn[]で記述できそうな気がしたけど、 純粋関数では Return[]使えないのね... Orz
つまり、f[arg_] := body;は、f := With[{arg = #1}, body];へ 書き換え可能では無いと...
Execve[]へのフロントエンドとして Execvpe[]と Execvp[]を実装しました。
Execvp[file_, {argv___}, options___]は、 execvp(3)相当でファイル名と引数列を渡すと 必要ならPATH変数で指定された検索パスから外部コマンドを 探し出し、現状の環境変数を引き継いで Execve[]します。
Execvpe[file_, {argv___}, {env___}, options___]は、 Execvp[]に環境envを渡せるようにしたものです。
なんで、execvpe(3)が無いんだろう... (execve(2)は、PATHの自動検索は行わないのがポイント)
前々から、やろうとしていた BidirectionalPipe[]の High Level定義への 置き換えを実施しました。 これで、Fortranと Cの間の文字列受け渡し(null終端忘れてるとか...) などの問題や、execlp(3)を使っているのに、コマンド名以外の引数を 渡せない問題が解消されます。
新しいプロトタイプは、以下の通り。
BidirectionalPipe[file_String, {argv___String}, Options___]
オプションとしては、Execve[]へのオプション(ForceCloseOnExec, CloseStd) や、子プロセスのIDを受け取るための PID:>varオプション、 環境を渡すための Environments->{env___String}オプションが使えます。 デフォルトでは、呼び出し時点の環境全体が子プロセスへ渡されます。
OpenRead["!ls -l /usr/include"]相当のことを BidirectionalPipe[]で書くと
{in, out} = BidirectionalPipe["/bin/ls", {"ls", "-l", "/usr/include"}, PID:>pid]; l = Table[If[s = Read[in, String]; s === EndOfFile, Break[], s], {Infinity}]; Close[in]; Close[out]; Wait4[pid];
と、なります。 注意すべきは、閉じるべきファイル記述子が送信/受信用の二つあること、 子プロセスの最後を看取ってやる必要があることです。 通信終了後に子プロセスが自発的に死なない場合やエラー等で 強制切断するときは、Kill[]でシグナルを送って親プロセスから 明示的に殺す必要があります。 (パイプを閉じれば、子プロセス側では 読み込みに対しては EOF、 書き込みに対しては SIGPIPEが発生するので、例外処理が正しく 実装されている場合は read/write発行時には死んでくれる筈ですが、 内部で無限ループしたりすると親プロセスが死ぬまで残留することに なるので、停止が期待できない場合にはやはり明示的に殺す必要があります。) また、Kill[]で明示的に殺した場合も Wait[]かWait4[]で 最後を看取らないとプロセステーブル上のリソースは開放されないので unix初心者は要注意。
私の書いた、β-φ解析ツールが結果を出さないという苦情が舞い込む。
エラーコードを見ると、不正な入力データが原因、 エラーコードぐらい確認してほしいものである。 (なんのためにマニュアル書いたんだか Orz)
で、不正な入力データの正体は、入力に使われている LINX/Yファイルそのもの。 LINX/Yを読めるようにしろと要求されたときに渡されたサンプルと微妙に違う のが原因...だからインターフェースの仕様を文書でくれと言ったのに... Orz
通信で使うデータ構造の標準化を求めると「我々がスタンダードだ」とか 言っているし...
きちんと定義していないから解釈の相違で互換の無いファイルが作られ、 いちいちそれに対応するコストが発生していることはどう考えているのだろうか
コントロール室の計算機環境でdry-runを実施するという流れで、 やっと、その手の話が話題になるようになりました。
開発環境と運転環境が異なるので、 Software Configuration Management (SCM)が必要だと言う指摘に対しては、 確かに必要だと同意してくれたのですが、CVSの問題点やそれに対する 現代的な SCMシステムの利点を説明したが...新しいツールを覚えるのは 面倒と言うことでCVSで管理するつもりらしい
確かに、CVSも欠点を知った上できちんと運用出来るなら 問題無いのですが...関係者全員がその辺を理解しているとは 思えない Orz
SPACエンジンとトラッキングコード間の境界APIを定義し、明確に分離すれば 新しいエンジンの組み込みや試験が簡単になる。
calc.yに関しては、LLARパーサーフレームワークがもはや保守不能のため... bisonの出力を awk等で Fortranコードに加工するという発想は、 bisonの改良と共に出力形式が変化していく現状では無理がある。
gfortranなんかで SADをコンパイルしていると警告される構文として、 次のようなものがあります。
integer :: i do i=1,5 write(*,*)'before if: i=',i if(i .eq. 3)then goto 100 endif write(*,*)'after if: i=',i 100 enddo
何をやってるかといえば、DOループの途中で条件に依存してループの中身を GOTOで飛び越しています。
内容的には、END DO文に付いたラベルが、DOループの中なのか 外なのかが分かりにくいと言うものです。 少なくとも、END DO文を使うということは Fortran90を仮定することなので (多くの Fortran77コンパイラは拡張としてサポートしますが)、 GOTOの代わりに CYCLE文で、次の繰り返しに入る方がすっきりしたコードになります。
修正すべき箇所は何か所あるのかな... Orz
正解は、61箇所
SADInspectで、定義が長いシンボルを選択したときに 表示が更新されないことがあるので、調査していたのだが... Fortran I/Oのバッファリングが諸悪の根源のようです Orz
ネットワークからのストリーム受信を、非同期に実行するために Tkのハンドラから受信ルーチンを起動して n = SelectUnit[lun, 0]と Read[lun, {n * Byte}]の組み合わせで Non-blocking uni-byte stream inputを行っているのだが、 Read[]中に Fortran I/Oのバッファがリフィルされ kernel側のバッファが空かつ Fortran I/Oバッファが空でない 状態になると受信が停止します。 (kernel側バッファが空なので、ハンドラがトリガーされない & SelectUnit[]は 0を返すので読むべき量が分からない)
原因判明
openoffice.orgには icu 3.6のソースが組み込まれており 自前でコンパイルして使っているのだが、portsが configure時にCPPFLAGSに渡している -I/usr/local/includeが自前のヘッダーへの パスよりも先に使われているために、 コンパイル時に ports/devel/icuでインストールされている icu 3.8.1のヘッダーが使用され icu 3.6のライブラリーとの リンクに失敗していました。
本来は、Makefile側を修正して-I/usr/local/includeの 優先順位を下げるべき所だが、面倒なので/usr/local/include/layoutと /usr/local/include/unicodeを一時的に名前を変えてごまかすことに
character(len=32) :: fn fn='/dev/null' write(*,*)'Open10' open(10,file=fn,status='UNKNOWN') write(*,*)'Open11' open(11,file=fn,status='UNKNOWN') write(*,*)'Open12' open(12,file=fn,status='UNKNOWN') end
コンパイラ | OPEN動作 |
g95 0.91 20080220 | すべて成功 |
g77 3.4.6 | すべて成功 |
gfortran 4.2.4 20080305(prerelease) | open(11,...)で失敗 |
gfortran 4.3.1 20080306(prerelease) | open(11,...)で失敗 |
Intel Fortran 7.1 | open(11,...)で失敗 |
Intel Fortran 8.1 | すべて成功 |
character(len=32) :: fn fn='/tmp/fortran.txt' write(*,*)'Open10' open(10,file=fn,status='UNKNOWN') write(*,*)'Open11' open(11,file=fn,status='UNKNOWN') write(*,*)'Open12' open(12,file=fn,status='UNKNOWN') read(12,'(a)')fn write(*,*)fn read(11,'(a)')fn write(*,*)fn end
コンパイラ | OPEN動作 | READ時のファイルポインタ |
g95 0.91 20080220 | open(11,...)で失敗 | N/A |
g77 3.4.6 | すべて成功 | 独立 |
gfortran 4.2.4 20080305(prerelease) | open(11,...)で失敗 | N/A |
gfortran 4.3.1 20080306(prerelease) | open(11,...)で失敗 | N/A |
Intel Fortran 7.1 | open(11,...)で失敗 | N/A |
Intel Fortran 8.1 | すべて成功 | 独立 |
調査の範囲では、dup2(2)で上書きするための Logical Unit Numberを 確保するのに/dev/nullを多重OPENするという戦略は移植性が無い。
Intel Fortran 7.1から 8.1への挙動の変化が謎ではあるものの、 g77とことなり Fortran95規格ベースで実装されている gfortran/g95の挙動を 見る限り「同一ファイルへの多重OPENは禁止」は Fortran仕様的に何らかの根拠があると思われる。
integer :: i write(*,*)'Open10' open(10,status='SCRATCH') write(*,*)'Open11' open(11,status='SCRATCH') write(*,*)'Open12' open(12,status='SCRATCH') do i=1,1000 write(11,*)'New' enddo do i=1,10000000 write(10,*)'Test' enddo end
コンパイラ | SCRATCHファイルの可視性 | SIGINT時の動作 |
g95 0.91 20080220 | 不可視 | N/A |
g77 3.4.6 | 可視 | すべてのファイルが消える |
gfortran 4.2.4 20080305(prerelease) | 不可視 | N/A |
gfortran 4.3.1 20080306(prerelease) | 不可視 | N/A |
Intel Fortran 7.1 | 可視 | すべてのファイルが残る |
Intel Fortran 8.1 | 可視 | SIGINT時書き込み中のファイルが残る |
調査の範囲では、dup2(2)で上書きするための Logical Unit Numberを 確保するのにSCRATCHファイルをOPENするという戦略では、 プロセス中断時に一時ファイルが残留する可能性がある。
つまり、一時ファイルがファイルシステムに残留する可能性や 多重OPEN禁止の実装まで考慮すると... OPEN文にmkstemp(3)とunlink(2)を組み合わせて 自前で実装するしか一般解が無いと言うことに...
どうやら、gfortranに関しては/dev/null(正確にはisatty(3)が真となるもの
stat(2)の結果、!S_ISREG()が真となるもの[訂正2008/03/18])
に対しては、unbufferedspecial_file[訂正2008/03/18]な
取扱いになり、regular fileと異なる振る舞いをする。
従って、dup2(2)で上書きするための Logic Unit Numberを 確保するのに/dev/nullを OPENする代わりに regular fileを OPENすると挙動が違うことに...
このために、mkstemp(3)とunlink(2)を組み合わせたitopenbuf_の 再実装はうまく動きませんでした。
パス名を新たに生成可能かつ、
isatty(3)が真になる!S_ISREG()が真になる[訂正2008/03/18]というと
使えそうなのは名前付きパイプかな?
とすると、mkdtemp(3) + mkfifo(2) + unlink(2) + rmdir(2)か...
これまでの分析に基づいて、多重OPENの禁止処理にかからずに dup2(2)での上書き用の Logical Unit Numberを確保するコードを 実際に実装してみる。
有効にするには、sad.confへ以下を追加すること
USE_NEW_FORTRAN_OPEN_SIM=YES
動作は、/tmpに openbuf.で始まる 0700な一時 directoryを作って(mkdtemp(2))、 その中に名前付きパイプfifoを作成し(mkfifo(2))、 OPEN文で開いた後、 unlink(2)と rmdir(2)で後始末する。 一時ファイルの race conditionに対する安全性は、 mkdtemp(2)が予測不能な名前で ownerしかアクセスできない directoryを作ることに依存しています。
itfopenread_G77.fで実装されているOPEN文での DISP修飾のエミュレーションも unlink(2)で再実装する。 system経由の実装だと、/bin/shと/bin/rmを実行するために fork(2)とexecve(2)が2回づつ呼び出されるという パフォーマンス面での不利もあるが、 /bin/shを通過するためにシェルが解釈するメタ文字を含む パス名を正しく扱えない問題がありました。 (好き好んで、そんな変態的なパス名を一時ファイルに 使う上位層はいないはずですが...)
Fortran I/O側でのバッファ管理の形態は、OPEN文が実行された時点での fileの種類に依存し、実際のI/O時にfile descriptorがどんな 実体と結びついているかは考慮されていない。 つまり、外部から FNUM + dup2(2)で上書きするという使い方は、 保証外の使用法である。(当たり前と言えば、当たり前ですよね)
バッファの管理形態としては、次の3種に分類できる。
種別 | 条件 | read(2)の呼び出しパターン |
unbuffered | isatty(3)が真 | 要求された読み込み量だけ read(2)を発行する |
special file | S_ISREGが偽 | バッファを埋めるために、read(2)を一度だけ呼び出す |
regular file | S_ISREGが真 | バッファが埋まるまで、read(2)を繰り返し呼び出す |
つまり、SADInspectで起きてる問題は、special file型としてOPEN された Logical Unit Number(LUN)へ socketを結びつけてネットワークから uni-byte streamを受け取る際に、SelectUnit[](ioctl(FIONREAD))で kernel buffer側に溜まっているデータ量を確認し Read[, {n*Byte}]で 溜まっているデータだけを受け取ろうとするが、ioctl(FIONREAD)発行後 read(2)システムコール呼び出しまでに新たに受信されたデータが、 Fortran I/O側のバッファにコピーされ、Read[]で指定した以上の データが kernel bufferから取り出されるために、適切な受信が 出来ない現象であると説明できます。
Fortran I/Oを使う限り、Fortran I/O側のバッファに溜まっている データ量を拾う手段が無いことには解決しません。 また、Fortran I/Oの具体的な実装はコンパイラ依存なので、 言語仕様にそうした手段が組み込まれない限りコードの移植性は望めません。
従って、これを解決するには、unbufferedにするしかありません。 具体的には、itopenbuf_内で仮想端末を確保してOPEN文に渡せばよい。 ただし、FGETCを用いた現状の uni-byte stream読み込みの実装では 著しいパフォーマンス劣化が予測されます。
仮想端末デバイスを一時ファイルにした itopenbuf_を実装。
Revision 1482は、USE_NEW_FORTRAN_OPEN_SIM=YESな状態でコンパイルした itopenbuf_はposix_openpt(3),grantpt(3),ptsname(3),revoke(2)を使った 仮想端末デバイスの確保を行いOPEN文でptyを開いたのち、 そのfile descriptorを/dev/nullで置き換えて、 仮想端末をクローズする。
以前のmkfifo(2)ベースの動作に戻すには、 COPTへ-DUSE_ITOPENBUF_MKFIFOを追加こと。
仮想端末版 itopenbuf()で作った Logical Unit Numberへ結びつけた pipe(2)での動作を検証してみました。
コンパイラ | Pipe[]へのRead[]の動作 |
g95 0.91 20080220 | unbuffered |
g77 3.4.6 | buffered |
gfortran 4.2.4 20080305(prerelease) | unbuffered |
gfortran 4.3.1 20080306(prerelease) | unbuffered |
Intel Fortran 7.1 | unbuffered |
Intel Fortran 8.1 | buffered |
Intel Fortranに関しては、最新の Version 10は持ってないので判断を 保留しますが...g77に関しては「窓から投げ捨てろ」ということですね
SADで RFSWとRADをONにしたTrackingを 実行するとTraclkingの中で実現する平衡分布が z方向へずれるという謎現象があるのですが、ヒントを得ました。
どんなヒントかといえば、トラッキングのために KEKBOpticsをロード(自動的にCALCが走る)した後、 事前にファイルに保存してある粒子分布を読み出し TrackParticles[]へ突っ込んだ場合(1)と、 その場で粒子分布を生成して TrackParticles[]へ突っ込んだ場合(2)で、 粒子分布の重心運動が異なるというものです。 (1)で事前に用意した粒子分布自体は、(2)で生成した 分布をファイルに書き出したものなわけで... File I/Oが破滅的にイカレてない限り粒子分布は 同じものになります。念のため、TrackParticles[]直前で 擬似乱数列も初期化しているので、プログラム的には まったく同じ出力を返すべきところですが、z方向に 30mm謎のドリフトをしてくれます。
注目すべきは、Opticsロードから TrackParticles[]開始までの 粒子分布生成の方法しか違わない点で、分布生成に Emittance[]の吐き出す座標変換行列を使っているので、 TrackParticles[]の直前に Emittance[]を差し込んだ所、 謎のドリフトが消失...トラッキングルーチンで Emittance[]が書き出す大域変数を参照している ようです。そんな仕様、聞いてないよ Orz
NORADにした場合でも、謎のドリフトは発生する。
先日の日記で書いてた calc.y回りの保守を実施。
新しいコードでは、src/yylex_.cへ
を実装し、calc.yからyaccにてcalc.hとcalc.cを生成し そのまま使います。
yaccの出力を Cコンパイラに渡すために、calc.yのアクションは FortranからCへ書き換えて有ります。
calc.yの文法に関しては、 ";",")",ID型トークンによる exprの終端を 受け入れる(YYACCEPTする)構文に変更しています。 これは、calc.yが解析する構文が、部分構文で次のトークンが 構文定義に含まれないために、SADの MAINレベル的には正常な構文に対して 構文解析器が必ずsyntax errorで終了するという問題への修正です。
Revision 1500から USE_NEW_FORTRAN_OPEN_SIM=YESな状態が デフォルトになりました。
古い実装を使いたい場合は、USE_OLD_FORTRAN_OPEN_SIM=YESを sad.confに追加してください。
なんか、SAPC系のフラグが乱立しているので、状況を整理してみる
なお、Space Charge関連と思われるtspac.fにある tspac_ルーチンは、呼び出されない。
ざっと眺めた感じでは、次の問題がありそう
ジュネーブに久々に雪が降りました。 市内の方は、芝生とかがうっすらと雪化粧をしている程度だったのですが、 CERNに近づくにつれて雪の厚みが増してゆく...
CERNは、一面銀世界でした。
昔のMAIN Trunk(1.0.8.23.1bとか)に入っていた Christopher K. Allenの3D RMS Beam Envelope Simulationを 拡張モジュール形式に移植する作業を始めたのだが... DynamicCall[]の第二引数の取扱いが腐ってました。Orz
誰からも苦情が来なかったことからすると...誰も使って無いのかな?
Christopher K. Allenの3D RMS Beam Envelope Simulation内で使われている Matrix Operatorの最適化といか再実装...
ざっと見たけど、MatrixFunctions.nは Fortran/C使いだった SAD初心者が書くコードですね。(私も、最初は似たようなコードを 書いた記憶が...) 行列演算が、ForループとPart演算子で記述されている。 これは書き直せば、絶対早くなると確信して作業開始。
結果、MatrixInnerProd2($L^2$ノルムな行列の内積)は、 リスト操作に直したらエラー処理込みで 3行に圧縮出来る上に、 速度は 17倍になった。また、MatrixNorm2[A]が MatrixInnerProd2[A,A]で実装されているので、 Plus@@(Flatten[{A}]^2)に変えてみると約 30倍早くなる。 (倍率は、Random[6, 6]で作った 6x6の乱数行列に対するベンチマーク)
これらを使っているMatrixExpとか MatrixLogも 4〜5倍に高速化した。
ちなみに、MarixNorm2[A](すべての要素の2乗和)の実装だが、 Plus@@Flatten[{A^2}]よりもPlus@@(Flatten[{A}]^2)の方が 何割か早いのは、二重リストへの Power[]演算子よりも リニアなリストへの Power[]演算子の方が、ループ段数が少なく 結果を格納する一時オブジェクトの確保にかかるコストが少ないためと 思われる。
SADで速いコードを書こうとすると、後ろで動作している インタープリターの挙動を意識しなければならない点は、 初心者向きでは無いと思う。Mathematicaと言うか、Lisp的な言語なので 世に普及している手続き型言語とプログラムの組立てかたが異なるが、 関数言語的な思考が出来れば結構書きやすいのだが...
実行効率を求めるとバイトコンパイルや最適化をやらない インタープリターは、他の関数言語の実装に比べると不利だと思う (純粋関数言語なHaskellとかでは、ソースコードをコンパイルして 最適化するので、ものによっては Cで書くより速く動くらしぃ)
MatrixCommutatorとかは、行列積オペレータ(.)が使われており 小手先の改良を行う余地はなさそうなので、Cで実装する方向で... src/sim/sad_api.hとかに必要なプロトタイプを実装する
CERNの4月の一般公開(LHC運転前の最後一般公開になる予定)を 前に飾り付けが行われていますが、なんかLHCのマグネットを Meyrinの街中に飾っている模様。
Meyrinのバス通り沿いのロータリーには、Quadrupole Magnetが出現
また、CERNの Receptionから Rue de Meyrinを渡った向かい側の 球体状の建物の前には、Dipole Magnetが出現
Mapに渡すために二つの行列の積を返す純関数を素直に
(#2.#1)&
と書いて、MapThreadに食わせてみたとこと、なぜか結果がおかしなことに...
演算結果を見る限り行列の要素毎の積になってるので、どうやら
(#2 #1)&
と解釈さているようです。何故... Orz
なんと、スロット演算子#に右結合するオペランドは実数らしい! つまり、最初の例では、(, #2., #1, ), &と 5個のトークンに分解されるようです。
対処としては、
((#2).#1)&
と書くか
(#2 . #1)&
のように明示的にwhitespaceを挿入すれば良いわけだが、 スロット演算子の意味付けから整数以外のものと結合する意味は 無いので、もう少し構文定義が改良できんものかねぇ
もっとも、SADの実装では構文定義から構文解析器を生成しているのではなく 直接構文解析器を書き下しているので、副作用が怖くて修正できん。 (この前の~演算子への修正で、予期しない構文定義への副作用が 発生していたのも、これが原因かと)
No | 入力 | SADの解釈結果 |
1 | (#1.6)& | (#&) |
2 | (#2.5e2)& | (#25&) |
3 | (#1.5.#2)& | ...#でプロンプト |
4 | (#-1)& | (-1+#)& |
6 | (#0)& | (#0&) |
5 | (#0.5)& | segmentation fault |
f = (#0)&;
を定義して、f[]とかf[0]とかやってみる。 なにが返ってくるのだろう...わくわく
って、なにこの味気の無い出力
In[1]:= f = (#0)& Out[1]:= (#0&) In[2]:= f[] ???General::slot: Undefined Slot #0 in (#0&)[] ???General::abort: Aborted: f[] ^ ???-FFS-Error-?Undefined command or element: F[] In[2]:= f[0] ???General::slot: Undefined Slot #0 in (#0&)[0] ???General::abort: Aborted: f[0] ^ ???-FFS-Error-?Undefined command or element: F[0] In[2]:=
General::Slotで適用時にエラーにするくらいなら、 純関数オブジェクト生成時に構文エラーにすべきでない?
2005年にChristopher K. Allenが SAD V1.0.8.x向けに書いた 3D RMS Beam Envelope Simulationを Extension module形式にして移植しました。 移植に当たっては、MatrixFunctions.nをすべてCで再実装することで オリジナル版に比べ 例題の 実行速度は約5倍に高速化されています。
使い方とか中身の原理に関しては、 http://www-linac.kek.jp/seminar/allen.htmlを参照
Scheff.n内の ScheffDecoup[], ScheffGenerator2[]を C moduleで再実装。 さらに、ScheffGenerator2[]を使った StepSigmaMatrix[]を StepSigmaMatrix2[]という名前で C moduleで再実装。 (もちろん、内部的なScheffGenerator2[]の呼び出しは C module内の 内部API経由)
これで、Revision 1569の実装に対して約 6倍に高速化(オリジナルからだと約 33倍)
SADScriptでのマトリクス演算が意外と遅いので(内部的に二重リストと Fortran配列間を相互変換するオーバーヘッドが大きいと思われる)、 ScheffPropElem[]も C moduleで実装すればもう少し速くなると 思われるが、可変刻みドライバーコードは変更容易性のために SADScriptのままにしておいた方が良いと思われるので、 手を付けずに残しておく。
先日、スロットの罠で、スロット演算子#が 実数へ右結合するという書き方をしたが、どうやら本当に演算子らしいです。 { 2 3 } &は、二つの実数間に暗黙の積演算子が存在すると見做され {Times[2,3]}& -> ({6}&)と簡約されます。
それに対して、{ # 3 } &は、直感的には{Times[#, 3]}&と 解釈されそうな所なのですが、({#3}&)と簡約されます。
つまり、スロット演算子#は字句解析レベルでは独立したトークンであり、 構文解析レベルで右結合な演算子なのです。 つまり、字句解析レベルでは、#2という第2スロットを示す表現は、 スロット演算子#と実数リテラル2の2トークンだったのです。
さらに、スロット演算子の優先順位は暗黙の積演算子より高いことが分かります。
すると、次の疑問が沸き起こります。 スロット演算子と右結合したオペランドが実数リテラル以外の場合、何が起こる?
In[1]:= f = {# a}& Out[1]:= ({#a}&) In[2]:= a = 1 Out[2]:= 1 In[3]:= g = {# a}& Out[3]:= ({#a}&) In[4]:= f[1,2,3] ???FFS::undef: Undefined element in ({#a}&)[1,2,3] ???General::abort: Aborted: f[1,2,3] ^ ???-FFS-Error-?Undefined command or element: F[1 In[4]:= g[1,2,3] ???FFS::undef: Undefined element in ({#a}&)[1,2,3] ???General::abort: Aborted: g[1,2,3] ^ ???-FFS-Error-?Undefined command or element: G[1 In[4]:=
なんと、シンボルaと右結合するではありませんか!(fの拘束結果) これだけ見ると、純関数の参照するスロット番号を変数としてパラメータ化 出来そうなのですが、SADの実装ではそうは問屋が下ろしません。 不思議なことに実数1に拘束されたシンボルaに 右結合したスロット演算子の簡約結果が奇妙なことになっています(gの拘束結果)
つまり、スロット演算子#は右結合かつ Hold属性を 持っているようです。
ただ、fやgを作用させた結果は、何とも味気ないものになっていまが、 発生しているエラーはUndefined Slotでは無いのでまだまな謎は尽きません。
こうなってくると、スロット演算子##の動作です。 {## 2}&や{## a}&は予想に違わず ({##2}&)や({##a}&)に簡約されます。
さて、トークンレベルではどうなっているのでしょう?
{ # # }&の評価結果は、なんと({#}&)になります。 解釈に悩みますが、この挙動から
ことが分かります。 スロット演算子は、右結合かつオペランドが無い場合は暗黙のうちに 1をオペランドとするので、これまでの見てきた動作との 一貫性を優先するなら({#(#1)}&)のように評価されるべきところ だと思われます。少なくとも、勝手にトークンが失われるのは反則でしょう。
ここで、二つ目の#をカッコで括ったさらに怪しい構文を試してみます。
In[1]:= f1 = { # (#) } & Out[1]:= ({##}&) In[2]:= f2 = { ## }& Out[2]:= ({##}&) In[3]:= f1@@Range[10] ???General::wrongtype: Argument must be Number or symbol in ({##}&)[1,2,3,4,5,6,7,8,9,10] ???General::abort: Aborted: f1@@Range[10] ^ ???-FFS-Error-?Undefined command or element: F1@@RANGE[10] In[3]:= f2@@Range[10] Out[3]:= {1,2,3,4,5,6,7,8,9,10} In[4]:= f1===f2 Out[4]:= 0 In[5]:=
すごく謎な挙動です、f2に拘束している純関数は、 すべての引数のリストを返す演算子で普通に動作していますが。 無理やりスロット演算子にスロット演算子を結合させた純関数(f1)は まともに動きません。
SameQは、f1とf2の違いを認識しているようですが、 表示上区別が付きません。もしやと思い、ToString[]を試してみると...
In[1]:= f1 = { # (#) }& Out[1]:= ({##}&) In[2]:= f2 = { ## }& Out[2]:= ({##}&) In[3]:= ToString[f1] Out[3]:= "({##}&)" In[4]:= ToString[f2] Out[4]:= "({##}&)"
これは、もはや笑うしか有りません。 つまり、環境と結合の無い純関数の定義を ToStringで シリアライズしたものをToExpressionで元通りに復元不能と 言うことです。
もうそれほど速くはならないと思うがベンチマーク目的で、 ScheffPropElem[]とCompStepSize[]のC versionを試しに書いてみたところ...
なんかさらに2倍速になるんですけど Orz
やっぱり、SADインタープリターが致命的に遅いのかな...
KEKBで使ってる SADScriptで書いた Optics Correctionの計算とかも Cで書き直すと10倍ぐらい速くなりそうな気がしてきた
とすると、欲しくなるのは
あたりですね。
前者に関しては、例えばC++上に SADインタプリター内のオブジェクトと 操作を表現するクラスを作って、変換時に型情報を追跡して 効率よくC++のコードへマップ出来れば、そこそこの性能は出そうです。
後者に関しては、バックエンドVMに LLVMとかが使えそうです。 上層で、関数言語的な簡約と最適化を行った後、LLVM向けの バイトコードを吐き出して、LLVMがローカルなマシンコードへ 最適化を行いつつ変換するみたいな実装とか...
どちらにしろ、SADScriptのメンテナンス性の良い構文解析器が必要な気がし ます。まずは、野菜作りですかね?(Yet Another SAD Interpreter)
昔の日記に書いていた LLVM-GCCで SADがコンパイル出来ない件だが、どうやら llvm本体のバグか llvm-gfortranが複素数に対する累乗演算子を正しいバイトコードへ 変換出来ていないのが原因のようです。
状況としては、a**bにて aまたはbがCOMPLEX型の ときに発生する模様。exp()とかlog()はCOMPLEX型に対しても 正常に動くようなので、a**bをexp(b * log(a))へ書き直せば コンパイルは可能になりましたが、inimem_内でSegmentation Fault するようです。ilistrootを用いてSAD内のヒープ管理領域を初期化する 際に発生しているので、ilistを格納しているcommon blockの状態が おかしなことになっている模様。
リンカーが下記のような警告を出すので、アライメントも怪しい...
/usr/bin/ld: Warning: alignment 16 of symbol `ffs_' in libsad.a(tffs.o) is smaller than 32 in libsad.a(tffs.o) /usr/bin/ld: Warning: alignment 16 of symbol `itparm_' in libsad.a(tpara.o) is smaller than 32 in libsad.a(tpara.o) /usr/bin/ld: Warning: alignment 16 of symbol `mtfcommon_' in libsad.a(tfetok.o) is smaller than 32 in libsad.a(tfetok.o) /usr/bin/ld: Warning: alignment 16 of symbol `rbufcom_' in libsad.a(tfreadbuf.o) is smaller than 32 in libsad.a(tfreadbuf.o) /usr/bin/ld: Warning: alignment 16 of symbol `tffvp_' in libsad.a(track.o) is smaller than 32 in libsad.a(track.o) /usr/bin/ld: Warning: alignment 16 of symbol `tok_' in libsad.a(tfetok.o) is smaller than 32 in libsad.a(tfetok.o)
SAD本体のコードは20年選手なので、紆余曲折を経て謎コードに なっている部分を理解するには、古いコードを発掘して変更内容を 追跡することが必要になったりしますが、関連する変更箇所を探しながら 時間方向に遡る作業は CVS向きでは無いのでCVS repositoryを コピーしてきて調査用の Subversion repositoryを生成してみる。
ふむ、CVS repository上最古のコードは 1995年なのか... その時点で呼び出し元と呼び出し先で引数の数と型が 整合していない例を見たことがあるが、そこから遡る追跡調査は 至難の技かな
「ソースがドキュメントだ」とか「ソースにすべて書いてある」とか 言う人もいますが、下記の理由によりコードやデータ構造を理解するには インクリメンタルな変更差分から変更意図を類推する作業が必要です。
コメントを読む限り、 src/inc/MACPHYS.incの echargには、 昔は elementary chargeがC単位で入っていたようなのですが、 途中からエネルギーの単位がJからeVに変更され echargが1になっているが、参照コードによっては 1.602176487x 10^-19 Cであることを期待しているものが 見受けられる。
同じ物理定数が異なる名前で、複数箇所で定義されている。 光速cや真空の透磁率$\mu_0$のように定義値に なってるものはまだ良いが、観測量なものに関しては複数箇所で 定義するのは保守性の悪化を招く。 (kg(シリコン単結晶の真球を使ってアボガドロ数を定義値にして定める)か J(ホール効果等からプランク定数hを定義値にして定める)を定義値に するって話は出てるけど、置き換えまだ〜)
事実、収録元か収録時期が異なる観測量な物理定数が存在する。 また、こうした定数は 2003年から更新されていない。
27日の日記でLLVM-GCCを使ってコンパイルしたSADだが、 common block回りの不具合原因が判明。
ここで問題です、次の Fortranコードを llvm-gfrotranで コンパイルした場合、どんな結果が出力されるでしょう?
implicit none integer*4 i logical*4 flags(64),flag1,flag2,flag3 common /tflags/flags equivalence (flag2,flags(2)) equivalence (flag3,flags(3)) do i=1,64 flags(i)=.false. enddo write(*,*)'Clear:' write(*,*)'flags(1)=',flags(1) write(*,*)'flag1=',flag1 write(*,*)'flags(2)=',flags(2) write(*,*)'flag2=',flag2 write(*,*)'flags(3)=',flags(3) write(*,*)'flag3=',flag3 flag2=.true. write(*,*)'After: flag2=.true.' write(*,*)'flags(1)=',flags(1) write(*,*)'flag1=',flag1 write(*,*)'flags(2)=',flags(2) write(*,*)'flag2=',flag2 write(*,*)'flags(3)=',flags(3) write(*,*)'flag3=',flag3 do i=1,64 flags(i)=.false. enddo write(*,*)'Clear:' write(*,*)'flags(1)=',flags(1) write(*,*)'flag1=',flag1 write(*,*)'flags(2)=',flags(2) write(*,*)'flag2=',flag2 write(*,*)'flags(3)=',flags(3) write(*,*)'flag3=',flag3 flags(1)=.true. write(*,*)'After: flags(1)=.true.' write(*,*)'flags(1)=',flags(1) write(*,*)'flag1=',flag1 write(*,*)'flags(2)=',flags(2) write(*,*)'flag2=',flag2 write(*,*)'flags(3)=',flags(3) write(*,*)'flag3=',flag3 flag1=.false. write(*,*)'After: flag1=.false.' write(*,*)'flags(1)=',flags(1) write(*,*)'flag1=',flag1 write(*,*)'flags(2)=',flags(2) write(*,*)'flag2=',flag2 write(*,*)'flags(3)=',flags(3) write(*,*)'flag3=',flag3 end
Subversion resporitory revision 48818での結果は、以下の通り。
Clear: flags(1)= F flag1= F flags(2)= F flag2= F flags(3)= F flag3= F After: flag2=.true. flags(1)= T flag1= F flags(2)= F flag2= T flags(3)= F flag3= T Clear: flags(1)= F flag1= F flags(2)= F flag2= F flags(3)= F flag3= F After: flags(1)=.true. flags(1)= T flag1= F flags(2)= F flag2= T flags(3)= F flag3= T After: flag1=.false. flags(1)= T flag1= F flags(2)= F flag2= T flags(3)= F flag3= T
要するに、EQUIVALENCE文に配列を添字付きで与えた場合、 添字の部分が無視されているようです。
バグ 1971でも EQUIVALENCEの不具合が報告されている模様です。
SADは、common blockとEQUIVALENCEを多用してるので このバグが直らない限り、LLVM-GCCを使うのは絶望的です。
SADの CVS repositoryから調査用に変換生成した Subversion repositoryを あさってみると...
といったことが、分かりました。
開発形態は、少数精鋭による典型的な伽藍モデルのようです。
あとは、CVS repository導入直後から現代までのコード行数の変遷でも 調べて見るべきか?
_ Y氏 [バグ1971は直ったようですね。]
3月最終日曜日となる本日午前1時からサマータイムへ突入しました。
現在の日本との時差は7時間です。
街中の一部の時計は、サマータイム突入前の時刻を指しているものが 見受けられる所を見ると、切り替えは自動ではない模様。
前にスロットの罠で報告した(#0.5)&が segmentation faultになる件だが、原因が分かったので修正した。
実は、スロット関数を使った表現Slot[0.5]&では、問題なく 受け入れられるのだが、この違いは、スロット演算子をsrc/tfeexpr.f経由で スロット関数に変換する過程で不正な参照を持ったスロット関数を 生成していることが原因でした。 また、表示はスロット演算子形式になるので、ToString[]して ToExpression[]すると問題が発生する罠。
詳細は、Ticket-11を 参照のこと。
前にスロットの罠で報告した(#1.5.#2)&が ...#でプロンプト待ちになる件だが、原因が分かりました。
tfetok()の返すトークンを追跡したところ、#の次のトークンは 1で始まるので数値リテラルをトークン化する eval1()が 呼び出されるが、このサブルーチン何を思ったかトークン中に 2個目の.が出現するとそのままリターンしてしまいます。 したがって、#の次のトークンが謎文字列1.5.#2になることに...
eval1()は.で開始する実数リテラルをサポートするようなので、 この2個目の.を検知してアボートするロジックは、 RepeatedNull(..),Repeated(...)演算子を 見つけた際に実数リテラルとしてトークン化せずに上位ロジックに 制御を戻すためのものと思われますが、.の連続性を 検査していないので誤爆している模様です。
現在の実装の挙動はおかしいので仕様レベルの修正をRevision 1658で入れました。
具体的には、連続する.(つまり..)を見つけたら その直前で実数リテラルが終了するものと解釈します。 したがって、..で開始する文字列は実数リテラル部の長さが0と なるのでm=0を返して終了。 .を含む実数リテラルに.が継続した形2.5.は、 2.5までが実数リテラルとして解釈され、続く.は次のトークンの 開始文字になります。 また、.を含まない実数リテラルに..が継続した形12..は、 実数リテラル12と継続文字列..に分割されます。 なお、.の後に[^.eEdD0-9]が継続する場合は、.までが 実数リテラルとして解釈されるので、(2.A)は(, 2., A, )とトークン化され、Times[2., A]と解釈されます。
SADには正規の文法定義な存在していないので、バグと言うのも微妙なのですが、 標準的な表現には影響しないはずです。
ただし、ToString[]がコンソールへのエコーバックで使われる短縮形では、 各リテラルの短縮表現と後続リテラルの短縮表現を結合した際に生じる 文法的な曖昧性を考慮しないので、変換ToExpression[ToString[#]]&が 恒等変換で無いのは注意が必要です。
たとえば、(#2.0.#3)&はDot[#2.0, #3]&と解釈されますが、 ToString[]の結果は短縮表現((#2.#3)&)となり、 これをToExpression[]で解釈するとTimes[#2., #3]&と解釈されて しまいます。
1995/09/13(CVS repositoryの初期化)から2008/03/26までの 各コミッターによるCVS MAIN trunkへのコミット数の推移 (branchへのコミットは除外)
Acct. | 1995 | 1996 | 1997 | 1998 | 1999 | 2000 | 2001 | 2002 | 2003 | 2004 | 2005 | 2006 | 2007 | 2008 | Total |
A | 75 | 81 | 250 | 449 | 293 | 228 | 224 | 164 | 60 | 111 | 61 | 31 | 64 | 29 | 2120 |
B | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 53 | 85 | 40 | 26 | 53 | 103 | 75 | 435 |
C | 0 | 0 | 49 | 148 | 118 | 71 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 387 |
D | 23 | 26 | 22 | 2 | 7 | 5 | 3 | 1 | 0 | 0 | 6 | 0 | 0 | 0 | 95 |
E | 0 | 2 | 13 | 5 | 16 | 22 | 2 | 25 | 5 | 1 | 0 | 0 | 0 | 0 | 91 |
F | 0 | 0 | 0 | 0 | 0 | 0 | 4 | 6 | 0 | 0 | 0 | 12 | 0 | 0 | 22 |
G | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 5 | 0 | 0 | 6 |
H | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 4 | 0 | 0 | 0 | 1 | 6 |
I | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 5 | 0 | 0 | 0 | 5 |
Total | 98 | 109 | 334 | 604 | 434 | 326 | 234 | 249 | 152 | 156 | 98 | 101 | 167 | 105 | 3167 |
カテゴリー: Admin | Emacs | EPICS | Fortran | FreeBSD | GCC | hgsubversion | IPv6 | KEKB | LHC | Lisp | LLVM | MADX | Ryzen | SAD | samba | tDiary | unix | WWW | YaSAI | お仕事 | イベント | 出張 | 宴会 | 数学 | 艦これ | 買いもの | 追記 | 雑記
_ Y氏 [LLVMはiPhoneで使われているようですね。早く日本でも発売されないかなぁ...]