* acl4の開発ログ #01
-(by [[K]], 2025.12.01)
** 2025.12.01(月) #0
-結局このスタイルが一番書きやすいので、これにしました(笑)。整理してきちんと書くのはそのうちでいいやー。
-標準関数の malloc/free/realloc の仕組みでは、mallocで得たメモリはどのメモリもreallocでリサイズする可能性があって、私はそれがちょっと不満です。
-リサイズするかもしれない時と、リサイズする予定が全くない場合とでは、保持しておかなければいけない情報が違うのです。標準関数のモデルを維持しようとすると、結局すべてのメモリを「リサイズするかもしれない」として扱う必要があって、まったくうまくありません。
-(なるほど、そうか。・・・ここに書くまで、自分が何に悩んでいたのかよくわかってなかったのですが、ここに書いたら分かってきました。)
-ということで、mallocで得たメモリをreallocでサイズ変更するっていうのはやめて、realloc用のメモリは別の方法で確保させることにします。
** 2025.12.01(月) #1
-acl4では、メモリをfreeするときもサイズを渡すことになっています。これはなぜかというと、解放する人はサイズを知っている場合がとても多いからです。
-解放時にサイズが分かれば、mallocのチェーン内にサイズ情報を格納しておく必然性もなくなって、メモリ使用効率が改善します。
-だって4バイトとか8バイトとか16バイト程度の小さなメモリをmallocすることは、木構造みたいなものを扱うとよく出てきますが、そのたびにサイズ情報がヘッダとして追加されると、メモリ効率が悪くなるわけです。
-acl3やそれ以前でも「free時にもサイズを指定する」という仕様でやってきましたが、今のところずっとうまくいっていました。可変長オブジェクトの場合は、そのサイズを内部に持っておいて、解放時にそれを指定すればいいわけです。
** 2025.12.07(日) #0
-やっと「どこからどう作るべきか」がわかる瞬間が来た!よし、ぱぱっと作ろう。
** 2025.12.10(水) #0
-まだ「よしこの仕様で行こう!」と決めきれたわけではないのですが、まあ技術的にこういうことはできているという説明を書こうと思います。
-以下のようなプログラムを用意します。
// t20251210a.c: メモリリーク実験.
#include "acl4.c"
int main(void)
{
char *a[5];
acl4_ini();
a[0] = Amalc(1); // mallocの代わりです.
a[1] = Amalc(3);
a[2] = Amalc(5);
a[3] = Amalc(7);
a[4] = Amalc(9);
Amfre(a[1], 3); // freeの代わりです.
Amfre(a[3], 7);
Amalc_debugList(); // a[0], a[2], a[4]は未開放なので、ここで検出したい.
return 0;
}
-これをコンパイルして実行するとこうなります(私は最近は Visual Studio Community を使っています)。
C:\rsd0027\progs\acl4>cl /nologo /O2 /MD t20251210a.c
t20251210a.c
C:\rsd0027\progs\acl4>dir t20251210a.exe [余計な表示はカットしています]
2025/12/10 10:37 10,240 t20251210a.exe
C:\rsd0027\progs\acl4>t20251210a
t20251210a.c(15): Amalc_debugList()
[p:0x00b4ccc8 siz:1 / t20251210a.c(8)]
[p:0x00b4c760 siz:5 / t20251210a.c(10)]
[p:0x00b530e8 siz:9 / t20251210a.c(12)]
C:\rsd0027\progs\acl4>
-このようにテストプログラムは10.0KBになって、未開放のメモリ一覧が見えます。それぞれのメモリが、どこのAmalc()に由来するものかもわかります。
-プログラムの終了時や、もしくは適当なタイミングでこれを表示してデバッグすれば、メモリリークがないプログラムを作るのはそれほど大変ではなくなると私は思います。
----
-デバッグ時はこれでいいとして、リリース時にはこんな冗長なエラー処理は省略してほしいです。ということで、 /DADbgLv=0 をつけてコンパイルします。
C:\rsd0027\progs\acl4>cl /nologo /O2 /MD /DADbgLv=0 t20251210a.c
t20251210a.c
C:\rsd0027\progs\acl4>dir t20251210a.exe [余計な表示はカットしています]
2025/12/10 10:49 7,680 t20251210a.exe
C:\rsd0027\progs\acl4>t20251210a
C:\rsd0027\progs\acl4>
-このように、実行ファイルは7.5KBになって、Amalc_debugList()は何もしなくなります。ソースは全く変えなくていいわけです。
** 2025.12.10(水) #1
-この7.5KBというのが十分に小さいのかどうかという疑問が残ります。ということで、次のようなプログラムも用意します。
// t20251210b.c: メモリリーク実験.
#include <stdlib.h>
int main(void)
{
char *a[5]; // acl4を全く使わなかったら何バイトになるか?.
a[0] = malloc(1);
a[1] = malloc(3);
a[2] = malloc(5);
a[3] = malloc(7);
a[4] = malloc(9);
free(a[1]);
free(a[3]);
return 0;
}
-これをコンパイルして実行するとこうなります。
C:\rsd0027\progs\acl4>cl /nologo /O2 /MD t20251210b.c
t20251210b.c
C:\rsd0027\progs\acl4>dir t20251210b.exe [余計な表示はカットしています]
2025/12/10 10:57 7,680 t20251210b.exe
C:\rsd0027\progs\acl4>t20251210b
C:\rsd0027\progs\acl4>
-このサイズは、ADbgLv=0のときのt20251210a.cと全く同じです。
-ということで、まあおおざっぱではありますが、サイズ的にはほぼ消えていると言っていいと思います。
-ちなみに、サイズ優先最適化である /O1 を指定して同じことをするとこうなります。ADgbLv=2はデフォルト値です。
||/O1|/O2|
|t20251210a.c (ADgbLv=2)|RIGHT:9,216|RIGHT:10,240|
|t20251210a.c (ADbgLv=0)|RIGHT:7,680|RIGHT:7,680|
|t20251210b.c|RIGHT:7,680|RIGHT:7,680|
-ということで、 ADgbLv=0 にすれば、十分に消えてくれそうだということはわかります。
** 2025.12.10(水) #2
-Amfreでサイズ指定を間違えたらどうなるかの確認テストです。
// t20251210c.c: メモリサイズ指定ミス.
#include "acl4.c"
int main(void)
{
acl4_ini();
char *a = Amalc(1234);
Amfre(a, 123);
Amalc_debugList(); // ここには到達しないので、書いておく意味はあまりないですが.
return 0;
}
-実行結果はこうなります。うまくいっています。
C:\rsd0027\progs\acl4>t20251210c
t20251210c.c(8): Amfre: bad siz (head.siz=1234, arg.siz=123)
C:\rsd0027\progs\acl4>
-内部のデバッグチェック用管理ヘッダには1234って書いてあるけど、Amfreの引数では123が指定されているよ、という指摘です。
-サイズを指定するのにはメモリ節約になる以外にもメリットがあって、「自分はaというオブジェクトを開放しているつもりだったのに、間違ってbのポインタを渡していた」みたいな場合に、サイズが不一致ならすぐに気づけるというのがあります。
** 2025.12.10(水) #3
-Amfreで二重開放したらちゃんと教えてくれるかどうかのテストです。
// t20251210d.c: 二重開放の検出テスト.
#include "acl4.c"
int main(void)
{
acl4_ini();
char *a = Amalc(1234);
Amfre(a, 1234);
Amfre(a, 1234); // これを検出してほしい.
Amalc_debugList();
return 0;
}
-実行結果はこうなります。うまくいっています。
C:\rsd0027\progs\acl4>t20251210d
t20251210d.c(9): Amfre: bad signature (head)
C:\rsd0027\progs\acl4>
-これはどういう原理かというと、ADbgLv=2のAmalc()は確保時にデバッグ用のヘッダをつけて、そこにシグネチャをつけてくれます。
-Amfre()にはこのシグネチャをチェックすることで、「Amalcで確保したメモリかどうか」を確認してから解放しているわけです。
-また解放時にはこのシグネチャをクリアするので、二重開放時にはエラー検出できるのです。
** 2025.12.10(水) #4
-Amalc_debugList()のやり方はうまいと思ったので、このやり方を真似して、初期化済みのオブジェクトに対して、まだデストラクタが呼ばれていないものの一覧も取れるようにしてみます。
** 2025.12.11(木) #0
-C言語ではC++みたいなRAII(Resource Acquisition Is Initialization)がありません。だからスコープを抜けたときに自動でデストラクタを呼んでくれることはありません。デストラクタは必要な時に自分で呼び出す必要があります。
-でも人間なので呼び忘れてしまってリソースがリークしてしまうことはあります。それを早期に見つけるために、今生きているオブジェクトの一覧を出せるようにしました。これをプログラムの最後や、プログラムのチェックポイントで呼び出して、想定通りになっているか確認することで、デストラクタの呼び忘れを見つけられます。
-これで本決まりというわけではないですが、まあ自分が納得できそうなレベルにはなりました。デストラクタの呼び忘れを教えてくれます。
-デストラクタを呼ばなくてもいいクラスは表示されません。
-デストラクタを呼ばなくてもいいオブジェクトは表示されません。
// t20251210e.c: 二重開放の検出テスト.
#include "acl4.c"
AClass(File) { // 新しいクラスを作る.
FILE *fp;
ADbgLv1_Aodi // デストラクタが必要なクラスでは、クラス定義の中にこの記述を入れる(ADbgLv=0のときは消えてくれる).
};
Astatic void File_open(File *w, const char *p, const char *m _ADbgLv2_FL4)
// コンストラクタ(引数リストにちょっと変なのをつける).
{
w->fp = fopen(p, m);
ADbgLv2_ini(w, "File"); // デストラクタが必要なクラスでは、コンストラクタにこの記述を入れる(ADbgLv=0のときは消えてくれる).
}
Astatic void File_close(File *w _ADbgLv2_FL4)
// デストラクタ(引数リストにちょっと変なのをつける).
{
fclose(w->fp);
ADbgLv2_dei(w); // デストラクタが必要なクラスでは、デストラクタにこの記述を入れる(ADbgLv=0のときは消えてくれる).
}
int main(void)
{
acl4_ini();
File f[3];
File_open(&f[0], "t20251210a.c", "rb" _1); // コンストラクタ・デストラクタには _1 という引数を追加で渡す(ADbgLv=0のときは消えてくれる).
File_open(&f[1], "t20251210b.c", "rb" _1);
File_open(&f[2], "t20251210c.c", "rb" _1);
File_close(&f[1] _1);
AObjMan_debugList(); // これで、openしたままになっているオブジェクトの一覧が出てほしい.
return 0;
}
-[Q]変な引数が増えているけど、オーバーヘッドがあるんじゃないの?
-[A]それは主に呼び出し時のソースコード上の行番号やファイル名を渡している引数です。もちろんオーバーヘッドはあります。しかしADbgLv=0になれば消えるので気にする必要はないです。私は気にしません。
-実行結果はこの通りになります。うまくいっています。
C:\rsd0027\progs\acl4>t20251210e
t20251210e.c(31): AObjMan_debugList()
[p:0x00cffd1c class:File / t20251210e.c(27)]
[p:0x00cffd54 class:File / t20251210e.c(29)]
C:\rsd0027\progs\acl4>