* acl1Tinyのドキュメント #1 -(by [[K]], 2024.09.20) ** (0) -このドキュメントを書いている時点での、acl1Tinyのファイル群をまとめました(3つのアーカイブの内容は同一です)。 --https://essen.osask.jp/files/acl1Tiny_20240923.7z (7.9KB) --https://essen.osask.jp/files/acl1Tiny_20240920.7z (8.0KB) --https://essen.osask.jp/files/acl1Tiny_20240920.zip (11.9KB) -つまり、以下のようなことが、たった8.0KBで実現できてしまうというわけです。 -つまり、以下のようなことが、たった7.9KBで実現できてしまうというわけです。 --[1]C言語でもメモリリークしないプログラムが書けるようになる。 --[2]C言語でもガーベージコレクションのある言語みたいにfreeを気にしない書き方ができる(しかもGC方式じゃないから速い)。 --[3]二重freeとかのバグを根絶できる。 --[4]C++のテンプレートみたいなことがC言語でもできる(C++には及ばないけどね)。 --[5]qsortより速いソートが使える。 --[6]malloc/freeの高速化もある。 -いやまあいろいろ及ばないところもあるかもしれないけど、8.0KBなので全部許せちゃう! -いやまあいろいろ及ばないところもあるかもしれないけど、7.9KBなので全部許せちゃう! -acl1Tinyの全体像は[[a24_programs]]で分かります、たぶん。 ** (1) メモリリーク検出 -acl1Tinyには、メモリリーク検出機能があります。コンパイルするときに ADbgLv を1以上の値で定義してください。そうするとメモリリーク検出機能が有効になります。 -ADbgLvを未定義にするか、0で定義しておけば、メモリリーク検出機能はコードから削除されます。したがってリリース時のオーバーヘッドはありません。 -簡単な実験をしてみます。もちろんADbgLvを1以上にしてコンパイルします。 #include <acl1Tiny.c> int main() { char *p = AM_alc(am0, 0, 1234); // char *p = malloc(1234); に相当. AM_dst(am0); // ←この記述で、am0は閉じられる(デストラクタ). このタイミングでリーク検出がなされる. return 0; } // am0 : デフォルトで用意されるメモリアロケータ. [実行結果] AM_dst(): error(n=1, total=1234) -このように、未解放のメモリブロックが実行時に検出されました。 -メモリアロケータからメモリアロケータを作ることができます。子供側のアロケータでリークを起こした場合、子供側のアロケータをdstすればリークを検出できるのはまあ当たり前です。しかし、子供側のアロケータのdstそのものを忘れる可能性があります。そういうときどうなるでしょうか。うまく検出できるでしょうか? #include <acl1Tiny.c> int main() { AM *child = AM1_opn(0, am0); // AM1型のメモリアロケータを、am0を親として作る. char *p = AM_alc(child, 0, 1234); // char *p = malloc(1234); に相当. AM_dst(am0); // ←この記述で、am0は閉じられる(デストラクタ). このタイミングでリーク検出がなされる. return 0; } [実行結果] AM_dst(): error(n=2, total=1314) -今回は2つのメモリブロックが未解放だと指摘されました。これは、am0からみれば、1234バイトのpだけではなく、80バイトのchildも未解放だということです。 -ということで、リークを見逃すことはありませんでした。 ** (2) 自動解放のテスト -acl1Tinyでは、自動解放をサポートしています。 自動解放について、詳しくはこちら。→[[a24_AMemMan]] #include <acl1Tiny.c> int main() { AM *child = AM1_opn(1, am0); // AM1型のメモリアロケータを、am0を親として作る. char *p = AM_alc(child, 1, 1234); // char *p = malloc(1234); に相当. AM_dst(am0); // ←この記述で、am0は閉じられる(デストラクタ). このタイミングでリーク検出がなされる. return 0; } [実行結果] (エラーがないので何も出力されません) -今回は自動解放フラグを1にして、childやpを作りました。こうすると、childは親のam0がreleaseを実行したタイミングでdstされるようになります。pはchildがreleaseを実行したタイミングでfreeされるようになります。 -そして、am0はdstされると自身をreleaseしますし、childもdstされれば自身をreleaseします。 -結局こういう流れになります。 am0.dst → am0.release → child.dst → child.release → p.free → (childのリーク検出) → child.free → (am0のリーク検出) -明示的なfreeを一切書かなくてもメモリリークしないなんて、まるでガーベージコレクションのある言語みたいです(笑)。 -なおこの自動解放の仕組みは Objective-C の Autorelease を真似て作っています。 -では、pの自動解放だけをOFFにしたらどうなるでしょうか。やってみましょう。 #include <acl1Tiny.c> int main() { AM *child = AM1_opn(1, am0); // AM1型のメモリアロケータを、am0を親として作る. char *p = AM_alc(child, 0, 1234); // char *p = malloc(1234); に相当. AM_dst(am0); // ←この記述で、am0は閉じられる(デストラクタ). このタイミングでリーク検出がなされる. return 0; } [実行結果] AM_dst(): error(n=1, total=1234) -このようにpが未解放であることを教えてくれます。先ほどの例(n=2, total=1314)とは異なり、n=1, total=1234になっています。 -ところで、このエラーはam0が出しているのでしょうか?それともchildが出しているのでしょうか?これだけでは分かりません。そこで、2つのアロケータに別々の名前を付けてみます。 #include <acl1Tiny.c> int main() { AM *child = AM1_opn(1, am0); // AM1型のメモリアロケータを、am0を親として作る. char *p = AM_alc(child, 0, 1234); // char *p = malloc(1234); に相当. AM_dbgSetNam(am0, "am0"); // アロケータにデバッグ用の名前を付ける. AM_dbgSetNam(child, "child"); AM_dst(am0); // ←この記述で、am0は閉じられる(デストラクタ). このタイミングでリーク検出がなされる. return 0; } [実行結果] AM_dst(child): error(n=1, total=1234) -このようにリークを検出したアロケータの名前が表示されるようになります。 -なお、 AM_dbgSetNam()は、ADbgLv=0のときは自動で空っぽの関数になるので最適化で完全に消えます。デバッグ後に消し忘れても邪魔にはなりません。 -この「オブジェクトの名前」の情報は、AM構造体の中には含まれていません。もちろんAM構造体の中に const char *dbgNam; というメンバ変数を入れておけば簡単だったのですが、そうすると ADbgLv=0 のときはオブジェクト1つに付きポインタ一つ分だけメモリが無駄になります。それは嫌だと思いました。また、#if~#endifを使って、デバッグモードのときだけこのメンバ変数が有効になるような実装も検討しましたが、それだとオブジェクトのサイズがリリースモードとデバッグモードとで違うことになり、それは「リリースモードでは再現するが、デバッグモードでは再現しないバグ」の原因になりえます。それじゃあせっかくのデバッグモードを使ったデバッグができなくなってしまいます。そんなわけで、オブジェクトの名前は全く別の場所に保存しています。 ** (3) AM1Dbgについて -malloc/freeの深刻なバグとしては、mallocで得たわけではないポインタでfreeしてしまったとか、freeを同じポインタに対して二回やってしまうとか、そういうのがあります。これをやらかすとヒープメモリを管理してるポインタのチェインが壊れて、再起不能なほどの状態になります。 -acl1の場合ですと、これに加えてfreeするときにmallocのときとは違うサイズを申告してしまうというバグもあり得ます(acl1はfree時にもサイズが必要なのです)。 -これをガードするには、mallocのたびにポインタとサイズを記録して、freeするときにはその記録の中に同じものがあるかどうかをチェックすればいいでしょう。ただし、これはそれ程軽い処理ではありません(少なくともfree1回につきハッシュテーブルを1回引くくらいの追加コストはかかる)。それゆえに、たとえデバッグモードであったとしても一律にすべてのアロケータにチェック機能を付けるのはためらわれます。 -ということで、チェックだけをするメモリアロケータ「AM1Dbg」を作りました。こいつはエラーチェックするだけで、実際のメモリ確保や解放は、すべて親のアロケータに丸投げします。こいつを怪しいところだけに入れれば最低限度の速度低下でデバッグできます。 #define ADgbLv 5 // 以下のプログラムは非常に危険なので必ずデバッグモードで実行する. #include <acl1Tiny.c> #include <AM1Dbg.c> int main() { AM *am1d = AM1Dbg_opn(1, am0, AMapSim11_opn(1, am0)); AM_fre(am1d, (void *) (AInt) 0x01234567, 1234); AM_dst(am0); return 0; } [実行結果] AM1Dbg_fre(): error: sz != -1 -これはポインタが登録されていないときに出るエラーです。うまく検出できています。 -今度はわざとサイズを間違えて解放してみます。 #define ADgbLv 5 // 以下のプログラムは非常に危険なので必ずデバッグモードで実行する. #include <acl1Tiny.c> #include <AM1Dbg.c> int main() { AM *am1d = AM1Dbg_opn(1, am0, AMapSim11_opn(1, am0)); void *p = AM_alc(am1d, 0, 1234); AM_fre(am1d, p, 1233); AM_dst(am0); return 0; } [実行結果] AM1Dbg_fre(): error: sz != 1234 -これはサイズが確保した時と一致していないことを表しています。 -誤って自動解放オブジェクトを手動で解放した場合はどうでしょうか? #include <acl1Tiny.c> #include <AM1Dbg.c> int main() { AM *am1d = AM1Dbg_opn(1, am0, AMapSim11_opn(1, am0)); void *p = AM_alc(am1d, 1, 1234); AM_fre(am1d, p, 1234); // 誤って自動解放オブジェクトを手動で解放してしまった. AM_dst(am0); return 0; } [実行結果] AM1Dbg_fre(): error: sz != -1 -手動解放で消えた後に、自動解放で消そうとして、ポインタが登録されてないというエラーになります。 -どのケースも解放処理が行われる前にエラーで止まっているので、ヒープメモリのチェイン構造を全く壊さずに済んでいます。 ** (4) -C言語は生ポインタが簡単に扱えてしまうから、メモリリークなどが起きやすい言語だと言われることがあります。これを解決するために Rust などの言語が生まれたのだと思っています。 -それはとても重要なアプローチですが、一方であえてC言語にとどまって、他の言語でやっていることを真似できないかと考えることも大事だと私は思います。そうすることで、いろんなアルゴリズムが試されていくことになると思うのです。 ** (5) おまけ -sprintf()はいろんなことが簡単にできて便利なのですが、以下の2点は不満です。 --関数の戻り値が文字数であって、文字列のポインタじゃない。 --sprintfの結果を入れるためのメモリを呼び出し元が用意しなければいけない。 -これを解消したのが、 AM_spf() です。メモリは強制的に自動解放モードで確保されます。 -これを使うと以下のようなプログラムを作れます、もちろんメモリリークはしません。 -我ながら、おもしろいなーと思います。 #include <acl1Tiny.c> int main() { char *s = ""; int i; for (i = 0; i < 10; i++) s = AM_spf(am0, "%s %d", s, i); // s = my_sprintf("%s %d", s, i); みたいなもの. puts(s); AM_dst(am0); return 0; } [実行結果] 0 1 2 3 4 5 6 7 8 9 ** (6) さらに -acl1Tinyには、qsortよりも速いクイックソートや、C言語でC++のテンプレートの真似をするためのacpp0も入っています。それらも全部合わせて 8.0KB です。 -acl1Tinyには、qsortよりも速いクイックソートや、C言語でC++のテンプレートの真似をするためのacpp0も入っています。それらも全部合わせて 7.9KB です。 --[[a24_aQSort]] → まずは (4) ベンチマーク を見てみてください --[[a24_acpp0]]