* ''a22''に関する雑記#1 -(by [[K]], 2021.03.29) ** 2022.03.29(火) -なんか調子が出ないなと思ったら、「そうか[[a22_memo01]]を書いてないからだ!」と思いつきました。ということで開始。 -今日はacl2ライブラリに追加中の、AMemAlc2というメモリアロケータのテストをしました。これはメモリ効率が良いうえに高速なアロケータなのですが、個別に開放することができません(サイズ変更などもできません)。だからあとでまとめてポイするようなメモリの使い方をする場合に重宝します。 -個別にfreeしないのなら、メモリのアロケート処理は簡単になります。まず適当に大きなメモリブロックを持ってきて、それをアロケート要求が来るたびに切り取って渡せばいいだけだからです。そしてまとめて開放するときには、その大きなメモリブロックをfreeしてしまえばそれで終わります。 [テストプログラム: AMemAlc1(普通のmalloc相当)で確保した場合] AMemAlc0_report0: n=255 total=43456 // 実際の消費メモリ AMemAlc0_report1: n=255 total=32640 // アラインのためのパディングを除いたもの [テストプログラム: AMemAlc2で確保した場合] AMemAlc0_report0: n=3 total=32784 // 実際の消費メモリ AMemAlc0_report1: n=3 total=32656 // アラインのためのパディングを除いたもの -このテストでは、AMemAlc2を使ったほうがメモリブロック数(n)が少なくて、実際の消費メモリも抑えられています。 --report1の結果は、AMemAlc2のほうが16バイト多いですが、この16バイトは3つのメモリブロックを後でまとめて開放するための管理情報です。 -お気に入りのライブラリ --[AClean]メモリの開放やオブジェクトのdeinitをかなり楽にしてくれるやつ。 --[AMemAlc0]最も基本的なメモリアロケータ。サイズ指定を無視していつも同じサイズのメモリブロックを提供する。高速。 --[AMemAlc1]AMemAlc0を28個たばねて、mallocのような感覚で使えるメモリアロケータ。サイズに合ったメモリブロックを提供する。 --[AMemAlc2]個別の開放を許さない代わりに、メモリの利用効率を改善したもの。 -あと、今日はデバッグレベルを2にするとacl2ライブラリがまともに動かなくなってしまうというバグを直しました(デバッグレベルは[[a22_memman05]]の(5)で説明しています)。 -デバッグレベル2だとオブジェクトのチェック処理が有効になるのですが、チェック処理をするためのオブジェクトがあって、自分自身のチェックをしようとして無限ループになってしまうという問題がありました。少々苦労しましたが、なんとか直せたと思います。 ** 2022.03.31(水) -C言語の場合、確実に参照しない引数が末尾にある場合、それはスタックに積む必要はない(レジスタに代入する必要はない)。でも今はこれを表現する手段がないので、不必要な引数も適当な値を積まなければいけない。HLXではこれをなんとかできたら、わずかな優位性になるかもしれない。 ** 2022.04.02(土) -今までC++のオブジェクトの自動開放(スコープを抜けるとデストラクタがよばれるやつ)がうらやましくてそのことだけを考えてACleanクラスを作ったのですが、これだけではガーベージコレクションには勝てないと気づきました。・・・ガーベージコレクションはcopying GC機能を有しているものがあって、つまりメモリコンパクションができるので、メモリのフラグメンテーションが発生しないわけです。おおなるほど、それはうらやましい。 -上記の(2022.03.29)に書いたAMemAlc2をうまく使えば、連続したメモリが解放されるようになるので、そもそもメモリのフラグメンテーションは発生しにくくなります。しかしそれでも、メモリの使い方によっては、「全く発生しない」ということにはできません。たとえばオンメモリのデータベースがあって、データの挿入と削除を繰り返せば、徐々にフラグメンテーションは発生するでしょう。 -ガーベージコレクションを実装せずに、同等の機能を実現するにはどうしたらいいかを考えました。それは結局、copying GC的なことを自前でやるしかなさそうだと思いました。たとえばオンメモリデータベースクラスを作ったとしたら、データベースオブジェクトを複製するメンバ関数を作るべきで、それを使うことで古いデータベースから新しいデータベースを作ることができて、そのあとで古いほうを消してしまえばいいわけです。 -このときメモリアロケータをわければ、新しいほうが最初から断片化してしまう問題は生じないでしょう。 -ちなみにacl2ライブラリでは、標準のAMemAlc1の上にAMemAlc2を載せて、そのAMemAlc2の上にAMemAlc1を載せる、みたいなことができます。こうすることで、メモリの確保と解放が自由にできて(これは最上位のAMemAlc1の機能によるもの)、しかもその使用範囲は連続的かつ限定的で(これはAMemAlc2によるもの)、完全に使い終わったらAMemAlc2の機能で一括解放が可能です。 ** 2022.04.03(日) -HLX-003cをacl2ライブラリに対応させて、コンパイルが通るようになって、テストランしてみたら、よしよし動く。・・・それで試しにデバッグレベルを2にしてみたら、a=1;すら内部エラーになってしまう。・・・え?なんということ! -これ、もし本当にバグを見つけてくれているのだとしたら、すごく役立っていることになるのだけど、一方で、そんな初歩的なミス(=デバッグレベルを上げた程度で検出できるバグ)をしていたなんて、我ながら情けない・・・。今はまだバグ検出ルーチンのバグなんじゃないかと思っています。 -ちなみにデバッグレベル0でも、行数の多いプログラムを実行すると落ちるので、やはりどこかおかしいのは間違いないです。acl2対応の時にミスったのかなあ・・・。 ** 2022.04.04(月) -本当にバグでした。ただし、間違っていたのはHLXではなくて、acl2の中身でしたが。まあとにかく、バグを早く見つけて直せたので良かったです。 -さて、改良を始めると止まらなくなるので、まずはいったんこのあたりで公開かなあ。 ** 2022.04.08(金) -C言語は仕様が単純で作りやすい言語だと思うけれど、しかしそれでも簡単に作れるというほど簡単でもないなとふと思いました。ではどんなサブセット仕様ならいいのでしょうか。C言語みたいに一通りのことはできるけど、でもC言語よりも単純なのがいいです。 -以下、思いついたことを書いてみました。 --[1]char/short配列をint配列に変換したり、int配列をchar/short配列に変換する組み込み関数があれば、言語としてはint配列だけをサポートすればよくなる? --[2]ポインタはintに入れることにする。構造体もポインタの形でしか変数に代入できないことにする。メンバへのアクセスもできるが、メンバはintしかないとする。 --[3]floatやdoubleをあえてサポートせずに、double*だけサポートするとする。・・・これで、言語としてはintだけサポートすればいいことになる。 -うーん、これで言語を構成することはできるだろうけど、これはちょっとみっともない言語になりそうです。やっぱり型はめんどうでもちゃんとサポートしたほうがよさそうだなあ。 ~ -acl2のバージョン02dです。 --http://k.osask.jp/files/acl02d_win.zip ** 2022.04.11(月) -セキュアなプログラミングを支援するとしたら、どんな機能が言語にあると嬉しいでしょうか。・・・結局は間違いを見つけてくれる機能じゃないかなと私は思います。 -間違いを見つける機能のうち、コンパイル時に見つける方法と実行時に見つける方法の2通りがあると思うのですが、私はまずコンパイル時の検出を頑張ろうとは思いません。なぜなら、正確な判定はかなり難しいからです。このコードは絶対におかしい、絶対におかしくない、の2つだけではなくて、おかしいかもしれないけどおかしくないかもしれない、という判定結果になることがよくありそうで、それを握りつぶして報告しないのならチェックで見つけられるセキュリティホールなんてごくわずかでしょう。かといって疑わしいものを全部警告として報告していたら、それはそれでうっとうしいだけです。結局警告を無視する習慣が身につくだけでしょう。 -一方で、実行時に見つける方法ならずっと簡単です。実行してみないとわからないというところは残念ですが、しかし誤判定はほとんどなくせます。 -でも実行時検出はあまり人気がありません。それはおそらく実行速度の低下を伴うからです。・・・でもそんなのはデバッグレベルが1以上の時に実行時チェックのコードを入れればいいだけで、デバッグレベルが0ならチェックコードを入れないようにすればいいだけなんです。 -そうなると、何を支援すればいいでしょう。デバッグチェックのためのコードを自動挿入してくれればいい気がします。つまり、プログラマが #if (ADBGLV >= 1) ... #endif とか書かなくても、ある程度のチェックを全部自動でやってくれるようなそんな言語です。 --初期化済みフラグを準備すれば、未初期化変数や未初期化メモリをリードしたらエラーにできるはずです。 --関数ポインタを使った呼び出しで、適切な値かどうかをCALL前にチェックする仕組みもその気になれば作れます。 --不正ポインタのチェック(含む:配列のアクセス範囲チェック) --リードオンリーやライトオンリーの変数属性の設定 ** 2022.04.13(水) -私は、定期的にポインタのセキュリティチェック機構を作りたくてたまらなくなります。 -でもそのたびに、「そんなの作ってもとてもじゃないけど使う気にならない」ものができそうになります。 -使う気にならないのは、ポインタに関する演算がマクロだらけになってすごく読みにくいからです。 -でもなあ、こう何度も作りたくなって中途半端にしていると、気が散ってしょうがないのです。・・・じゃあやっぱり作ったほうがいいのかなあ。 -使いにくいということに関しては、HLXみたいな自作言語でサポートしてしまえば、普通の記述で書けるようにできるはずなので、そういう意味では本質じゃないはずなのです。 ** 2022.04.19(火) -C++では、たとえば行列クラスを作ったとして、Matrix a, b;があったら、aとbは別々のメモリに割り当てられます。一方で、Javaとかの場合だと、aやbの実体はポインタになっていて、a = b = c; とかすると、aもbもcも同じメモリを指すことになります。 -私は先日までは、Javaのやり方が好きではありませんでした。だってどれか一つを書き換えてしまったら、他も変わってしまうからです。Javでは、それがいやなら、cloneすればいいということになっています。 -でも今の私はむしろJava方式が好きです。どれが一つを書き換えれば他に影響してしまう恐れがあるJava方式ですが、それなら書き換えなきゃいいと思うのです。たとえば、a+=b;みたいなことをしたら、aの指している先の内容を上書きすることはせず(だって他で使っているかもしれないから)、a+bの結果をヒープメモリ上に作って、そのポインタをaに入れればいいと思うのです。そうすると旧aの結果がメモリリークになる可能性はあります。でもそれはCleanクラスがあとで何とかしてくれるので、気にしなくていいと思うのです。 -C++では、普通に代入すると結構重たいコピーが走ることになります。これを避けるために、所有権の移動という方法を使う代入も選べます。これはムーブというのですが、ムーブで代入した場合、代入元は値がなくなってしまいます。・・・一方でJava方式ならコピーはポインタのコピーなのでこれ以上はないというほど高速です。そしてコピー元が壊れるとかそういうことはありません。これはこれで悪くない長所だと思うようになりました。 * 2022.05.10(火) ** 2022.05.10(火) -今はUFCSのトランスコンパイラを書いてみています。 int abc(char *s, int len) { long l; } -を入力すると、 abc, s, len, l の型をおおざっぱに認識できるところまではできました。プログラム中の変数の型が分からないと、関数の候補を絞り込めないので、まずはそこをやっているというわけです。 * 2022.05.11(水) ** 2022.05.11(水) -UFCSのトランスコンパイラがもう少し進んでこうなりました。 int i; // これでiはint型だと認識される. int int_fnc(int i, int j); // これで関数int_fncはintを返すと認識される. i.fnc(0).fnc(1).fnc(2); -この最後の行は、 int_fnc ( int_fnc ( int_fnc ( i , 0 ) , 1 ) , 2 ); -に変換できるようになりました。 -あとはinclude処理を書けば、たぶん実用段階へ移行です。 * こめんと欄 #comment