2017年7月ごろから衝動的にコンパイラを自作し始めました。3年経っても完成には程遠く、完全に自己満足の世界なのですが、誰かの為になることも書けるかも(?)知れないので日記・覚書をここにつけてみます。 LGTM貰えたら多少モチベ上がるかも。
Githubリポジトリ: Palanソースコード(C++)
C→Palanツール
リファレンス: Palanリファレンスv0.3
ヒストリ
プログラミング言語Palan開発日記 1年目
プログラミング言語Palan開発日記 2年目
[プログラミング言語Palan開発日記 3年目]
(https://qiita.com/tosyama/items/104ccfa7cada1bf801af)
日記
更新を楽にするため、上の方が最新です。時系列で読みたい人は下から読んでね
実装中
型情報のリファクタリング
4月12日 刈れてきたら
時間かかったけど、大夫問題刈り取れてきて、型設計の変更を少しづつ進めている。が、なんかなー。設計変更したらシンプルになるかと思っていたけど全然ならないな。むしろ複雑になっているのかも。どうしよう。でもやりたい事をするには前に進むしかないかな。
3月4日 Yak Shaving
ヤクの毛刈り。重要なことをやろうと思ったら、別の問題が発覚して、その問題に対処しようとしたら、また別の問題がでて対処していたら・・を繰り返してることを言うらしい。早くこの長い毛を刈り取りたい。
2月24日 リファクタリングのリファクタリングの・・・
次の機能をいれるために、型関連(主に固定配列)のリファクタリングをやっていて、allocとfree部のリファクタリングは目論見どおりできた気がするんだが、Deep Copyの造りの部分が上手くリファクタリングできず、更にDeep Copyのリファクタリング始めてる。リファクタリングの階層深くなってるがいつ上まで戻れるんかこれ。
2月11日 コツコツ
ずっとリファクタリングしてるがなかなか終わりが見えない。要素数が異なると、処理も異なるメモリアロケーションの関数を生成していたのを、関数名は違うが処理は全く同じコードを出力するように修正中。ようやく半分ぐらいか。
これまでのテストはオールグリーンを保ち、壊さないように根本的な造りを新しい設計に徐々に差し替えていくのだ。コツコツやるしかない。
1月17日 何回目かの型設計変更
結局、型関連のリファクタリングをしている。もう何回目だろう。これまで配列の型は[2]int32と[3]int32では別の型として扱っていて、アロケーションも別になっていた。そうではなくて変数に付く型ごとに初期化に必要な情報(上ではサイズ、2とか3とか)とかを持って、共通の型情報として[]int32をもつのが適切と考えて大規模リファクタリング中。そうしないと動的にサイズを変えるようなコンテナの実装には進めない。そうすべきだとは初期のころからわかってたはずなんだが、残念ながら自分には最初から一発でいい設計できる能力はないようだ。その代わり上手く設計変更にリファクタリングで対応することで乗り切る。
12月27日 v0.4
ようやく構造体メンバーや配列要素へのオブジェクト配置の細かい実装漏れが無くなってきたのでPalan v0.4としてFixしたい。次は宣言時に要素数を変えられる配列の実装かなー。っと思って勢いで進めて見たら、早々に大きな設計上の壁にぶつかり、絶望してブランチごと削除した。これは大規模リファクタリングが待っている予感だな orz。
あと直接メモリ配置の前置子#
にしてたけど$
に変えようかな。要素数を記憶する配列に#
を使いたくてこれが混在すると分からなくなりそう。$
は'Stack'って雰囲気あるよね?、、ね?、、ないかな(汗)。
12月6日 道のり
ようやくずっとやりたかった構造体のメモリに配列や構造体を直接配置することができるようになったかも。Cだと普通の仕様なんだけど、当初はJavaみたいに構造体や配列はかならずヒープ配置必須の考えで進めてたので、設計修正が大変で、ここまでくるのは遠い道のりだった。
ここ一週間は、あと少しのはずなんだけどAssertで引っかかっている部分をどう修正するかわからず、前回配列の所で同じようなことやったはずなのにソース眺めてもどうやったか思い出せないなーと全然進まなかった。今日ようやくわかった。3行追加に1週間以上かかった。
これが何が大きいのかというと、C言語の構造体をPalanの型に変換できるようになる。つまり8-9月に作ってたCパーサーを使って、Cのインクルードファイルを直接読み込んでPalanで使う道筋ができるというところ。まぁ、いまのペースだと、まだまださらに遠い道のりだなー。
11月21日 2の累乗の剰余
たぶんこんな感じ。理論はあまり理解できてない。
2の累乗かどうかは (X>0 && !(X & (X-1)) で判定できる。何乗かは n = log2(X)で取得。
割り算 D = X/2^n | 余り M = X % 2^n | |
---|---|---|
符号なし整数 | D = X >> n | M = X & (n-1) |
符号あり整数 | D = X >> (64-n) D = D + X D = D >> n |
S: Xの符号が+のとき0, -のとき全ビット1 (CLTQで作れる) S = S >> (64-n) M = X + S M = M & (n-1) M = (M - S) |
割り切れるかの確認で % を使う場合は if (M==0)の判定を使うけど、そのような使い方が分かっている場合は、符号なしと同じ計算でも割り切れて0になるので単にビットシフトだけでも問題ない。気になるのは足し算でオーバーフローがあるかもしれないので、もしかしたらさらに考慮がいるのかもしれない。
3の乗算の最適化も、Lea や複数の加算など試してみたが、今の環境だとあまり変わらず、とりあえず保留とした。
結果、gcc最適化なしには勝つようになった。
palan | gcc -O0 | gcc -O1 | gcc -O2 | |
---|---|---|---|---|
Collatz数列(10^6) | 3.85s→0.61s | 0.86s | 0.39s | 0.37s |
11月17日 負の割り算の余り
2の累乗のときビットシフトや論理積に変換するだけでしょっと甘く考えていたが、なかなか進まない。どのフェーズで変換かけるかとか微妙に設計が悪くて入れられないとか、負数の時どうするのとか。あと、意外とまとまった情報が見つからないない。
剰余演算に至っては、数学とC言語では剰余の定義が違うらしく混乱した。
具体的には数学だと余りは負にならないらしい。Cの場合は割られる数と同じ符号。そこまで考えた事なかったな。最適化でも、この辺り正確にやろうとするとなかなか情報がない。出来た段階でまとめてみるか。誰の役に立つか知らんけど。
11月12日 collatz
配列のオブジェクトメンバーを直接配置できるようにする変更は一旦できた。配列表現の代入あたりが動くけど、まだまだ出来が悪くて遅いと思う。今度は構造体のなかに直接オブジェクトを配置するのをやろうかなーっと思ってたけど、ふと【プログラミング言語速度比較】Collatz数列ベンチマークを言語別比較しよー!を試してしまった。
結果、またしてもgcc -O0に惨敗 orz。gcc -O0 は 0.86秒に対して、palanは3.85秒。gccの最適化なしには何としても常勝したいのでこちらに取り組むことにする。アセンブリを見たけどあれだな、collatz数列では 2の除算と3の掛け算等、ビットシフトとかに変換しやすい計算なので、そこの最適化がもろに効いてくる。ここはやってなかったのと興味もあるのでやってみよう。
palan | gcc -O0 | gcc -O1 | gcc -O2 | |
---|---|---|---|---|
フェボナッチ(40) | 0.76s | 0.96s | 0.96s | 0.59s |
Nクイーン(13) | 1.83s | 2.78s | 1.35s | 1.19s |
レイトレース(単精度) | 2.65s | 2.85s | 1.82s | 0.58s |
Collatz数列(10^6) | 3.85s | 0.86s | 0.39s | 0.37s |
11月2日 core
gdbでスタックトレース見ようとしたら、何故かcoreが出力できなくなって困った。開発に使っているVPSの障害情報が出てたから、再起動されて何かの設定が戻ったのかな。いろいろ見てると core_patternがapportにパイプされていた。上書きしようと試みる。リダイレクションにsudoの権限つけないといけないからbash経由で動かす。
sudo bash -c 'echo core > /proc/sys/kernel/core_pattern'
再起動したら設定戻るので、ここに覚え書きしとく。
開発の方は配列のオブジェクトメンバーを直接配置できるように実装中。とても地味でつくるのも面倒だが、パフォーマンス向上やメンバー配列を切り出せるようになるなど重要な機能である気がしてきた。
[3]#[2]int32 arr = [1,2][3,4][5,6]; // メンバーのサイズも含めて1回のmalloc
[3][2]int32 arr2; // <=だと、4回のmalloc。
[3,2]int32 arr3; // いままでの記法。これも1回のmalloc。メモリ配置は同じ。
9月24日 文字列化マクロ
忙しくてほぼ進捗ないが文字列化マクロの実装まで完了している。プリプロセッサとしては組み込みマクロ以外は機能的には盛り込めた。ここで一旦切り上げてpalanに戻ろう。gtestはインストール済みの前提でcmake組んでいるが、やはりプロジェクトに組み込んだ方がいいのだろうかとは迷ってるが、とりあえずこのまま。
8月24日 こんどこそstdio.h
先日できたと思っていたが、GitHub Actionでコケてしまって全然できてなかった。開発環境の古いUbuntuとCIの新しいUbuntuではインクルードファイルと通るパスが違うみたい。CIのエラーだけだとどこが悪いかわからず、結局一時的にVPSにUbutntu20.04環境を立ち上げデバッグしてみたところ、普通にバグだらけ。そんなこんなで何とか修正、こんどこそようやくstdio.hプリプロセス通った。あとは文字列化のマクロ # ができてないのでそこをやらないといけない。
8月13日 stdio.h
トークン結合を関数型マクロのところにのみ実装して、ようやくstdio.hから続く23ファイルのヘッダーファイルのプリプロセスに成功した。
やっと前に進んだ気持ちになった。でもプリプロセスにおいてはまだ実装してないディレクティブがある。地味に進めるぞ。
8月6日 再帰下降パーサ
プリプロセッサのifの条件文の判定のロジックを再帰下降パーサーで実装した。何となく知っていたがbisonばかり使ってたのでまともに実装したのは初めてかもしれない。
印象としては思ったより簡単で単純作業感がある。これは慣れると、かなり簡単かもしれん。でも単純作業は苦痛なんだよね。
これでようやくstudio.hから関連付くヘッダー読めるようになった。っと思いきや識別子の結合が残ってて不完全だった。マクロ展開前に結合しないといけないのか。どうにかなるだろうとあまり考えてなかったけど、これはなかなか厄介そうだぞ。
7月28日 プリプロセッサ
C言語をPalanに変換する時に#defineはconstに変換したいので、まじめにプリプロセッサ実装してみている。しかし、単なるトークンの置換で済むかと思いきや#ifがあるおかげでマクロ展開しながら定数式のパースもしないとダメなのね。
これは面倒。
いろいろ実装ソースみたけど、あまり気にいるコードがなく結局独自に作っている。このままだとCコンパイラが出来きてしまいそう。セルフホストなど高尚なことは考えてないので、正規表現等も使ってわかりやすく作りたい。
7月6日 形(かた)
いつの間にか、5年目突入してしまった。。まぁこのまま書こう。
Cパース用に新たなプロジェクトc2paを立ち上げて開発に必要な設定を一通り行った。 CMake/GTest/Github Actions(CI)/Coveralls連携/カバレッジのローカル取得ツールなどが使える。時間はかかったが、これは早くやればやるほど後の開発が楽になるし、こういう形(かた)は一度できてしまうと次に応用が効く(はず)。
cmakeも少し慣れたかな。いろいろ泥臭い書き方しないといけないなーとは感じつつも、依存関係を自動的に判断してくれるところは便利かも。
6月28日 CMakeとGTest
先日のGitHub Actionsの移行をやってみて、時代にあわせて使うツールは変えていかなくてはと改めて実感。今気になっているのはMakefileそのまま使っているのと、テストに使っているCatch2。やはりクラスプラットフォームが楽になるようにCMakeにした方がいいのか。また、Catch2もいけてない新バージョンに移行するよりも、この際GoogleTestに移行すべきと考えてしまう。
全然触ったことないので、今のプロジェクトでいきなり移行はハードルが高い。そこでcパーサーのプロジェクトを新たに立ち上げてそこで試してみることにした。
将来的に、CのインクルードファイルからPalanのコードを生成するツールがないと、C言語ライブラリ使う際、いちいち構造体の定義等を手で書くのが負担になって使いたくなくなるのが目で見えてるので、その準備の意味もある。
CMakeを試したところ、CMake独自のやり方を覚えることが多く、正直今のところ便利さは実感できていない。が、クロスプラットフォーム化するときに大いに役立つんだろうな。
6月22日 GitHub Actions
CIで使っていたtravis-ci.orgがついに使えなくなってしまった。travis-ci.comにデータは移行できたけど、無料で使うにはOSS用のプラン申請が必要。travis-ciで続けてもいいけど、この際GitHub Actionsに移った方が筋がよいと考えたのでそちらに移行。
Coverallsとの連携が大変かと思ったけど、CoverallsもGithub Actions専用のアプリを用意してくれてたのでlcovを実行してその結果をアプリに渡すだけで上手くいった。
6月16日 ひさびさ
ちょっと実装と日記のモチベーション落ちてしまったが、開発を停止したわけではないので進捗を書いておく。
#
演算子を使って構造体をスタックに確保できる様になった。でも配列とか構造体のメンバーは出来ないのでこれから。
スタックに確保した構造体は所有権の受け渡しができないので、ちょっとIN/OUTがわかりづらいという問題があった。
そこで、第一引数が書き込み可能な参照だった場合は、クラスのメソッドのような呼び出しができる様にした。これで見やすくなった気がする。
これらを使ってレイトレースのサンプル書き換えて、ようやくgccの最適化なしに勝てる速度になった。
type A {
int64 x;
int64 y;
};
#A a; // スタック上に構造体を配置
[3,4] -> a;
a.addx(5); // メソッドライクな呼び出し
func addx(@!A a, int64 z)
{
a.x + z -> a.x;
}
次はスタック上の配列とか、メンバーを間接でなく直接確保するとかかな。その前に構造体宣言時にメンバーの型をいちいち一つづつ宣言するのもやめたい。
利便性が高い文法実装が後回しになってる気もするが、こういう基本部分は後から追加は厳しそうなので頑張るか。
3月23日 文法・パーサ見直し
スタック上の構造体サポートにあたり、使いやすい文法追加しないとなぁと思い、ちょっと追加してshift-reduceコンフリクトに悩む。まぁ、コンフリクトは嫌だけど動くは動きそうだなぁとは思ったが、ふと、そういえば去年配列宣言の文法見直したので、LALR(1)でも型と普通の変数の記述の区別付くようになったんじゃないかと気づく。
去年の日記見るとわかるが、ずいぶん複雑なことしたので、シンプルにできるはずなので全面的に見直し。また、所有権移動の->>
の演算子とかも、1この変数限定にした方が使う側もわかりよいと思いなおしたので、そのあたりいろいろ思いつつパーサの記述全面見直し中。
3月8日 パフォーマンス見込み
まだ完全ではないがスタック上に構造体の確保とメンバアクセス、引数渡しができたのでプログラムをスタック構造体に修正して測りなおしてみた。測りなおしたら、なぜかgccが速くなってる。。tccと間違ってたかもしれない。
palan | gcc -O0 | gcc -O1 | gcc -O2 | |
---|---|---|---|---|
フェボナッチ(40) | 0.76s | 0.96s | 0.96s | 0.59s |
Nクイーン(13) | 1.83s | 2.78s | 1.35s | 1.19s |
レイトレース(単精度) | 4.6s⇒2.65s | 2.85s | 1.82s | 0.58s |
ようやくO0に速度的に勝つようになった。だけど、実はもやもやしている。C言語の方は構造体を返すときに戻り値の値渡しで返している。Palanは今回の修正で引数で出力用の変数のアドレス渡せざるを得なくなったが、どれが出力の引数なのかわからず見づらい。コード上はC言語の方がわかりやすいので、全然勝った気がしない。
かといって構造体の戻り値の値渡しを実装すればいいかというところでも、C言語のABIに違和感がありもやもやしている。もう少し整理したら書こう。
3月4日 区分の定義
なかなかスタック上の構造体の実装に入れない。宣言してスタック上に確保ぐらいまではできているのだが、引数への受け渡しとか代入とかも検討し始めると、既存の設計の問題点によってできないことが明らかになり、そのリファクタリングばかり行っている。設計の問題といっても作りが悪いというよりも、enumに定義するような区分の整理がちゃんとできてなかったなという印象。最初からちゃんとできていれば苦労はしないが、最初にこの区分の定義をちゃんとするには、最初からすべてが頭にできてないとムリだろうな。
今はちょっと他の勉強に時間を使っているので亀の歩みでなかなか進まないが、着実に設計はよくなっていると思う。
2月9日 使い方
型管理のソースを眺めてたら、型の管理のデータ構造を変えるというよりはそこにいれるデータの考え方とか使い方を変えると上手くいくような気がしてきたので、そのようにリファクタリング。型を変えるわけではない微妙な修正なので逆に修正漏れがないかの確認が難しい。
何故かデータ構造に不要なメンバーがでてきたので削除。少しシンプルになったのかな。でも、これで正解という確信はない。
まぁ、なんとか使い方を変えれたので、スタックに構造体を確保する目途はついたかも。
1月23日 型管理沼
高速なコードが書けるよう、構造体のメモリをスタック上に確保できるようにしようとしている。簡単な修正でできるような気が何となくしてたんだが、いやいやどうして、なかなか難しい。C関数に渡すポインタのポインタの型辺りで破綻してるかも。また型かー。
プリミティブ型とか配列とかヒープに確保したときの違いとかを今の設計の持ち方でいい具合に管理できる気がしない。どうかするとシンプルにいけそうな気もするが思いつかない。プリミティブ型と、構造体のようなオブジェクト型を一緒のように見せかけるのが複雑にする原因と思う。
この泥沼を抜け出すアイデア出てこーい。
1月12日 単精度浮動小数点計算
単精度浮動小数点計算をサポートした。無駄な倍精度変換がなくなり、レイトレースのサンプルプログラムのパフォーマンスが大幅改善。あとは構造体をスタックに確保できるようになれば、gcc -O0レベルになるはず。でも最下位のコアの部分の改修になるので大変そう。
palan | gcc -O0 | gcc -O1 | gcc -O2 | |
---|---|---|---|---|
フェボナッチ(40) | 0.76s | 0.96s | 0.96s | 0.59s |
Nクイーン(13) | 1.83s | 2.78s | 1.35s | 1.19s |
レイトレース(単精度) | 6.1s ⇒ 4.6s | 3.2s | 2.0s | 0.6s |
1月5日 丑年
日記更新のモチベがわかず放っておいたら、年が明けてたよ。去年に引き続き今年もおみくじは凶で絶好調。開発速度はゆっくりだけど、今は単精度浮動小数点の計算をサポート中。これができたらレイトレのパフォーマンスがかなり良くなるはず。
11月15日 ニューノーマル
うーん。退屈なテストを書くフェーズになったら急に開発速度が落ちてしまった。去年までは、こういうときには通勤時とかにiPhoneで暇つぶしに書けたんだが、コロナ禍でほぼ通勤することがなくなったりプロジェクトが変わったりして環境がすっかり変わってしまった。まぁ、それでものんびり続けてテストは書くには書けた。
ただ今度は、単体テストのフレームワークはCatch2を使っているんだが、Catch2のインストール方法に破壊的な変更が入ったみたいでCIが動かない。Catch2はシングルヘッダーインクルードでシンプルに使えるのが一番の強みで採用してたのに、面倒になってるっぽい。自分としてはCatch2を使う理由がなくなるほどの改悪に感じる。Catch2はどうして強みを放棄してしまったのか。
10月6日 型設計考慮漏れ
細かなバグを直していると、結局は設計考慮漏れに行きつくことが多い。今回は型チェックが上手くいってないが、ちゃんと直そうとすると今の設計だとダメじゃんパターンになってしまった。型の情報の持ち方は相変わらず難しくて正解がわからない。
具体的には、「データをディープコピーする場合」と「アドレスのコピーの場合」とで、型チェックは区別しなければならなかったがしてなかった。型のチェックが甘かったのでリードオンリーと書き込み可も考慮すべきだなと思って修正してたら、データをディープコピーする場合は、ReadOnly変数→Write可変数はOKなんだけど、アドレスのみコピーの場合は、ReadOnly変数→Write可変数はNGになって逆。気付かなかった。どう設計すべきなんだろうか。
9月25日 こまこま
レイトレースのプログラム実装中に見つけた細々したバグや未実装を直している。いろいろ実装を見て理解しなおさないといけないので、結構時間かかる。まぁ、こういう借金は早めになくすに限る。神は細部に宿るのだ。
9月15日 結論
メモリプールはやめとこう。将来的にスタックに構造体確保できるようにしよう。その方が、メモリ確保/解放の時間0にできるし今回の目的では筋がいい。malloc1億回+free1億回で1秒強しかかからないglibcは、もともとかなり速い。それでもメモリプールで爆速にしたかったらif文を1つも使わないぐらいの勢いの実装がいるが、汎用的にするとどうしてもif文が必要で、回避する方法は思いつかなかった。
コーディングスタイルとかでmalloc/free減らせる手法がないかとも思ったが、今回のレイトレースのように再帰するケースは厳しい。中途半端な3倍速程度のプール実装するよりは、苦労してでもスタックをサポートすべきだろう・・・という結論。
まぁ、今回のレイトレースのプログラムが計算量/速度に対して特にシビアなんだよな。通常のプログラムでは今の構造体の実装でも、十分実用的な速さなのではと思う。
palan | gcc -O0 | gcc -O1 | gcc -O2 | |
---|---|---|---|---|
フェボナッチ(40) | 0.76s | 0.96s | 0.96s | 0.59s |
Nクイーン(13) | 1.83s | 2.78s | 1.35s | 1.19s |
レイトレース(単精度) | 6.1s | 3.2s | 2.0s | 0.6s |
gcc -O0に常勝をまずは地道に目指す。
9月11日 試算
勝手な妄想は楽しいが、頭で考えていることと現実は違うというのが世の常。採用前に簡単なコードで実測して検証。
今回のレイトレースで呼ばれたmalloc/freeを計測すると各1億2933万回!。お試しコードだと、今の環境だと1.6秒がその処理に使われている見込み。ここは予測に近い。ただ、今考えているオブジェクトプールでalloc/freeを各1億回すと0.45秒はかかりそう。大きく改善はするけど、1.1秒短縮してレイトレースは3.8秒。つまり、gcc最適化なしの3.2秒に届かない試算でなんか悔しい。作戦見直しが必要か。
なんだかんだいってもmalloc/freeって速いね。プール使ったら10倍は速くなるかと思ってたけど、3.5倍程度しか速くできない。小さいメモリ領域は戻さずプールしてるみたいだね。
9月10日 構想
オブジェクトプールというかメモリプールについてあれこれ考えてみている。
- 基本的な考え: 解放されたものfreeせずに型ごとの専用スタックにPushして、次回確保要求が来た時にスタックにあれば単純にそれをPopして返す。スタック空なら普通のmallocを呼ぶ。できるだけシンプル・高速に。
- スタックの容量は16個とか決め打ち(設定でかえられる)にしといて、それ以上の場合はそのまま単純にfreeすることで、使われないオブジェクトが多量にメモリに残ることを防ぐ。あくまで確保/解放が頻繁な型の緩衝用。
- 将来的にはマルチスレッドの考慮の必要あるだろうな。マルチスレッド勉強せねば。。ロックするかtrhead localにするか。mutexは重そう。
スピンロックがよさそう。でも、スピンロックよりもさらに軽くできないか。ロック取得に失敗したら普通のmalloc/freeをするとかだとよさそう。マルチスレッド用とシングルスレッド用に分けてもいいかもしれないが、軽いのができれば共通でもいい。→ 9/11 試しにatomic_exchangeを最小限使っても通常のmallocより数倍遅くなる。ロックって重いんだね。thread local storage(TLS)でスレッド個別にスタックを持つ以外の選択肢はなさそう。でもアセンブリでTLSどうやるんだ。。 - とりあえずはC言語で書いて、オブジェクトファイルをリンクしたほうがいいのかな。palanではその関数を呼び出すコードを出力するのがとりあえずはいいか。
- スタック等はmallocで確保して、アドレスは型ごとのグローバル変数(or TLS)に入れとく。→ グローバル変数まだ実装してなかった。。
9月9日 倍精度版
定数のバクを踏んでしまって修正に時間とられたけど、レイトレースの倍精度版を作成。パフォーマンスは6.3秒→4.9秒になった。1.4秒は倍精度⇔単精度変換分のようだ。単精度の計算なんて昨今使わんだろうと思って倍精度だけにしてたけど、ここまで遅くなると単精度の計算もちゃんとサポートしたいね。
のこり1.7秒ぐらいはベクトル計算用(x,y,z)の構造体のmalloc/freeかなぁ。特にfreeが遅いんだよな。これ何とかしないとなぁ。Javaとかはヒープに基本確保するので同じと思ってこの実装にしてたが。使い物にならないってことはないけど、遅いのはがっかりするよね。やはり構造体をスタックに確保して関数とのやり取りできるようにするのがいいのかなー。でも、あまりスタック消費したくないなー。他にいい方法ないかねー?
例えば構造体ではなくて、3つのfloat変数をまとめて扱ってるだけって概念のものにできないかな?
・・・1時間後
オブジェクトプールだな。生成/廃棄が頻繁になる構造体用にオブジェクトプールを楽に扱える方法があればいい気がしてきた。スタック消費しないし。ちょっと考えてみるかな。
9月6日 惨敗
ようやくできた。refractとreflectのスペルミスを見つけるのに苦労した。
最初にコンパイラのバグを疑ってしまうので、なかなかデバッグが大変。
パフォーマンスは6.3秒ぐらい。gccの最適化なしに比べても2倍ぐらい遅い。惨敗orz。
凄く遅くなる心当たりは2つ。このレイトレースはすべて単精度浮動小数点数のプログラムなんだが、palanで浮動小数点の計算するときは倍精度に昇格している。つまり計算のところで単精度⇔倍精度の変換が常に発生しているという点。もう一点は、今のpalanでは構造体はスタックに持たずに全部ヒープに持っている。構造体をローカル変数として使うときもmalloc/freeが多くコールされてしまう点。
とりあえず倍精度版を作ってみて速くなるかみてみようかな。
9月4日 できた...てなかった
んん? なんか変な屈折ができた。。こりゃ失敗。
レジスタ退避の悪い箇所は、到達不可ブロックが扱いきれてなかったのが原因だった。
return直後ブロック閉じる前のところに到達不可の処理が生成されていたが、レジスタ退避の計算ロジックではそれは想定外。到達不可ブロックの消去は後でいいやと思っていたらダメだったね。消去消去。
9月2日 コメントアウトデバッグ
原因とは無関係のところで落ちてるっぽいので、gdbのスタックトレースには頼れない。アセンブリを調べるしかないが、調べる箇所が多すぎ。そこで落ちる現象をキープしつつダミー値に代替してコメントアウトできそうな処理をコメントアウトしていく。そうするとどの関数にバグがありそうか検討がついてくる。
printfデバッグならよく聞くが、コメントアウトデバッグって名前あるのかな。
結局、屈折のベクトルの計算の関数が怪しい。何回見ても原因がわかんなかったが、バグありと確信してみると見つかるもんだ。
仮引数がレジスタに割り当てられていたんだが、レジスタ退避のコードを出力する位置がおかしい。落ちる原因はなんとか特定。コメントアウトするとレジスタの割り付け方も変わり再現性が微妙で発見に苦労した。
でもこのレジスタ退避のロジックは結構複雑なところ。悪いところ見つけて直すのには時間かかりそう。
9月1日 折
何とかごまかしながら既存文法でlibjpegをコールできるようにしたり、単精度少数点の扱いができていないとこ追加したりとかして、あと20行程度のガラスの屈折のコード書いたらゴールってところで、まさかのセグフォ。どこかでメモリ壊して無関係のところで落ちてるっぽい。コンパイラが出力したコードが悪いんだろうが。
↓みたいに影、反射がいい感じで出力できて、あと一息思ったのに。。。どこから調査すればいいんだこれ。心が折れる~。
8月21日 libjpeg
いやー、レイトレの前で詰まるとは。すぐに結果見るために元とするC版ではjpegで出力する様にしてるんだが、palanでlibjpeg使うのに一苦労。構造体でかくて定義が大変。加えてポインタのアドレスのみコピーの文法なくてエラー処理書けなくて困る。メジャーなライブラリ使うのにまだ文法足りてないか。
やはりヘッダー直接読めるようにできたらいいな。
8月18日 レイトレース
動的配列実装のモチベがいまいちわかないので、浮動小数点数のベンチマーク用にレイトレースを書いてみることにした。
参考にするのは、tinyraytracer。256行でできてる。
C++で書かれているが、テンプレートとか使われててpalanに変換しづらいので一旦Cにポーティングした。C版のパフォーマンスは最適化なしで3.2秒、-O1で2.0秒、-O2で0.6秒。
palanどこまでいけるかな。最適化なしぐらいまでは、ついていって欲しいんだが。
実はレイトレースのプログラムは大昔に技標社の本見ながらMacで書いたことはある。そのときはプログラムがへぼかったのもあると思うが、屈折までやったらレンダリングは数時間レベルだったと思う。今は1秒かからんなんてコンピュータ本当に速くなったなー。
8月15日 動的配列
次は動的配列を実装してみたい。といっても可変な配列ではなくて宣言時の要素数に式を指定できるが、固定長の配列。いままでは定数で固定のみの実装としていた。
int64 size = 15;
[size]int64 dyarray; // 動的にサイスが変わる。型としては[?]int64型
できるような土台にしていたつもりだったけど、いざ実装してみようとなると、足りないものや設計が合わないところが多すぎて、しばらくはソースコードみながら設計を考えることになりそう。
8月13日 潜んでる?
うーん、絶対パグが潜んでるパターンだな。coverallsでカバレッジ見ると通ってない処理があるのでちょっと調べてみたら、どうもローカルでは通っている処理らしく、CI環境のみ通ってない。この調査相変わらずむずいな。今までのケースだとreturn の書き忘れか何かなんだけど。++/--の実装はできて、テストケースもオールグリーンなんだけどなぁ。
これは厄介。
・・1時間後
バグじゃなかった。Assertion内で確認のため関数を使ってたのが原因だった。CIではNDEBUGをつけてるから、Assertionが消えて呼ばれなくなっただけだった。この紛らわしい動作はなくしたいが、このAssertionも結構助かってるんで消したくない。とりあえず保留。
8月6日 ++
間接参照の文法を追加し、ようやくインクリメントが動き始めた。テストとエラー処理追加中。今までサンプルでa+1->a;
と書いてたのがa++;
となり入力の楽さを実感。この簡単な書式のおかげで、C系のforはあんな形になったんかねーなどと想像を巡らせてみてる。
7月27日 オブジェクト型・プリミティブ型
考え中のインクリメントの実装だが、更新する変数が間接参照だった場合は内部で下記のように展開することにした。
// a[x]++; を展開
{
@!int32 _temp = a[x]; // a[x]のアドレスを取得して、int32の書き込み可能参照とする。
_temp = _temp + 1; // _tempの参照先がプリミティブの場合、自動的に間接参照する。
}
ここの初期化と間接参照も文法作ってないので、やはりここから作ることになるかー。オブジェクト型(配列・構造体)の変数の場合は、実体はもともと参照先を指しているアドレスなので、何も考えずにそのまま使えばいいが、プリミティブの場合はアドレスをとったり、逆にアドレス先の参照にしたり、まぁまぁ特別扱いしないといけない。
Cっぽくプリミティブ型とオブジェクト型はシンプルに全く違う文法にしてもいいが、言語をつくる立場としては、同じように使わせたい欲求がどうしても湧き上がってくる。Javaとかもそのあたり混乱のもとになってたな。Rubyとかは割り切ってすべてオブジェクトにしてるがパフォーマンスが犠牲となるジレンマがある。プリミティブ型とオブジェクト型の取り扱いをどうするかは言語作成者にとっては避けられない悩みではないかな。
7月24日 ++
インクリメント演算子(++
)の実装を考えている。whileしかない現状、++がないとループ書くのが少し面倒なんで。前から決めていたが、文法はC言語とは違い、式ではなく文の扱いとしたい。
Pythonの+=
もそんな感じではないのではないかな。++も後置のみ限定する。式にすると実装の難易度も高くなるし、式のなかで使うと読むほうも頭使う。単体で使うこともが一番多いしわかりやすいとの結論。
ここまで文法を限定していも、実装は面倒そう。代入と加算は実装済みなので、単純に i++;
をi + 1 -> i
に変換すればよさそうな気がするが、配列メンバとか絡むとそうはいかない。
例えば、a[getInd()]++
をa[getInd()] + 1 -> a[getInd()];
に展開してしまうとgetInd()が2回呼ばれてしまうので、プログラマの期待と異なるからNG。内部でポインタ系の機能を新たに追加しないと、実装できないかもしれない。
7月22日 I'm back.
4か月間openだったCoverallsのissueに、travis CIのUbuntuのバージョンをtrustyからxenialにしたら直ったよとのコメントが付いた。試してみるとたしかにエラーがでなくなって正常にカバレッジアップロードできるようになったよ! フロントエンドでファイルのソートがおかしくなるバクもいつの間にか修正してくれてる(長い間放置ごめんのコメントついてた)。さっそくcodecovからcoverallsに舞い戻った。coveralls頑張ってほしい。
7月19日 v0.3リリース
英語リファレンスができたので、ようやくv0.3リリース
構造体、C関数呼び出し改良、レジスタ割付けによる最適化が主な成果。パフォーマンスは、
palan | gcc -O0 | gcc -O1 | gcc -O2 | |
---|---|---|---|---|
フェボナッチ(40) | 0.76s | 0.96s | 0.96s | 0.59s |
Nクイーン(13) | 1.83s | 2.78s | 1.35s | 1.19s |
と、まぁ、gcc -O1には負けるが実用速度になってきたのではないかな?浮動小数点はまだ計測してないし、最適化してないので、これからの課題。
他の公開している自作Cコンパイラも気になったので、計測して比較してみたが、最適化しているものでもgcc -O0より遅いか同等程度のものばかりだったので、palanは結構健闘してるのではないかな?
今のところ-O2レベルは目指す予定はない。O2ぐらいになるとどうもアルゴリズムレベルの最適化の効果が大きくなってきてるみたい。逆にいうと、プログラマのスキルがあればコーディングで同等の速度が出せるということ。コンパイラの仕事としては、プログラマが手を出せないアセンブリレベルの最適化をしっかりやるべきかなと感じている。
v0.4ではもう少しコーディングしやすい文法を増やそうかな。forとか++とか。
覚書
- 引数と戻り値が同じ時 戻り値側の型はvarで省略できるようにする。
- 数値計算などパフォーマンス上げる為にfreeをしないモードを入れてもいいかもしれない。
- 名前付きブロック。宣言した時点でメモリ確保してしまうので、子のブロックで親のブロックの変数を宣言できる方がいい。ループ内の振る舞いは要検討。
- 関数呼び出し時のデーブコピーは呼び出し元でやってるが、リードオンリーの際リファレンスを使うかどうかを呼び出し元で選べるから呼び出し先でやってもいいかも、と、一瞬思ったが、引数確定後に元の値が変わる恐れがあるのでやっぱり今まで通りでいい。
- 継承は便利なもことも多いが、子の型が確定しているときに子のデータにダイレクトにアクセスしたい時が面倒。共用体のように型変換せずとも気軽にアクセスできる仕組みがあればいいと思う。
- 引数にコピーを指示している関数にmoveで渡せてもいい。用途はsetterとか。そもそも引数宣言にmoveの指定は無くていいのかも。fopen/closeは強制できたほうがいいか。
- インポートするモジュールをいちいち指定するのは面倒。パースの際に未解決識別子を認識して、それに応じたモジュールを自動でインポートできれば嬉しいかも。