Upgrade to Pro — share decks privately, control downloads, hide ads and more …

C言語でメモリ管理を考えた話

 C言語でメモリ管理を考えた話

くだらない動機(C++がうらやましいけど、C++は使いたくない)でささやかな規模のものを作っただけだったのに、意外に役立つ面白いものができた。

これをもっとすっきり書ける自作言語ができたら、相当に面白いことになる気がしている。

Hidemi KAWAI

April 29, 2022
Tweet

More Decks by Hidemi KAWAI

Other Decks in Programming

Transcript

  1. 出発点(最初のきっかけ) • C++は便利な機能がたくさんあるけど、C++でクラスをある程度 使うと(継承とか仮想関数とかを使わなくても)、実行ファイルが 10~20KB増えてしまいます。 • C言語で同じ処理をする場合と比較しています。 • リンクされるライブラリが増えるから? •

    10~20KBなんて大したことないといえばそうなのですが、私はた いてい20~100KBくらいの開発をするので、C++を使うだけで1~5 割程度大きくなってしまうことになり、それが私には不満だったの で、「C言語だけでどこまでやれるか」を頑張ってみることにしま した。
  2. Cleanクラス • 似たようなことをどうしてもやりたくて、以下のようなものを 作りました。 • Cleanクラス • 関数のポインタと引数を複数登録できる構造体 • Clean_out()関数で、それらの関数を登録とは逆順に全部呼び出す

    関数ポインタ 引数1 引数2 引数3 関数ポインタ 引数1 引数2 引数3 関数ポインタ 引数1 引数2 引数3 関数ポインタ 引数1 引数2 引数3 登録方向 呼出方向
  3. 本当に悩みは減ったのか? • [step1] • まず、mainの冒頭でCleanオブジェクトを初期化。 • あとはClean *が必要になったときに、全部このCleanオブジェクトのポイン タを渡す。 •

    mainの最後にClean_out()を記述。 • つまりCleanオブジェクトは全体で1つだけ。とりあえずこれで動く! • これで問題がなければ、これでおしまい。 • [step2] • メモリを使いすぎてよくないところを見つけたら、それを囲める範囲で、 新しいCleanオブジェクトを初期化。そして、この範囲内のオブジェクトの うち、外のスコープに出さなければいけないものだけをcloneして、範囲外で も使えるようにする。 • 厳密に考えすぎず、「ここに来たらもう絶対に触らない」と思える ところでClean_outすればいいだけ。・・・こう思ったら楽でした。
  4. Cleanが使えないケース • オンメモリの簡易データベースのクラス • データをINSERTしたりDELETEしたり • DELETEしたものをデストラクタのClean_outで片付けようと すると、データベースのデストラクタを呼びだすまで片付かな いことになる(それはきっとまずい) •

    だからこれはClean_out方式が向いていない • こういうときは、従来通りの方法(自分でfree)でやる。 • Clean *を指定する文脈でNULLを指定すれば、デストラクタやfreeを Cleanオブジェクトに登録しなくなるので、それらについては自前で 管理するようにする。
  5. そのほかの補助ツール • 「コンストラクタが呼ばれたらポインタを登録して、デストラクタが呼ば れたら、登録ポインタを削除する表」を作ったので、登録状態を確認する ことで、デストラクタの呼び忘れを見つけやすくなりました(デバッグ時 のみ有効)。 • メンバ関数では、冒頭でポインタが前述の表に登録済みかを確認するので、 デストラクタ呼び出しが早すぎる場合も気づけます(デバッグ時のみ有 効)

    。 • 「mallocしたら登録して、freeしたら削除する表」もあるので、メモリ リークも見つけられます(デバッグ時のみ有効) 。 • Cleanオブジェクトはスコープのネストを登録可能で、中にClean_outし ていないオブジェクトが残っている場合、「エラーにする」or「先に内側 をClean_outする」を選べます。通常は書き忘れが疑われるのでエラーを 選びますが、longjmpのときは内側Clean_outを選びます。
  6. ここまで作った上の感想 • もうオブジェクトの寿命では悩まない! • ミスがないことにも自信が持てるようになりました。 • Clean_outを使うと、実行ファイルは1~2KBほど増えます。これは 私としては十分に許容範囲です! • Clean_outすら書かなくてもいいC++や、ガーベージコレクションの

    ある言語(たとえばJava)には負けるけど、C言語の軽さも魅力なの で、私はもうあまり他言語をうらやましいとは思わなくなりました。 私としては、CleanクラスがあればC言語はいいバランスです! • 現状では、何をするにもCleanオブジェクトのポインタ指定が必要で、 それが少しうっとうしいので、自作言語でこれをきれいに書けるよ うにしたいです。なんなら、ブロックを抜けるときに、自動で Clean_outするコードを出力させればいいかもしれないです。
  7. さらに応用: xsprintf() char *xsprintf(Clean *c, const char *f, ...) {

    char s[1024 * 1024]; va_list ap; va_start(ap, f); int l = vsnprintf(s, sizeof s, f, ap); char *t = aMalloc(l + 1, c); strcpy(t, s); va_end(ap); return t; } char *s = “”; Begin(clean); // Cleanオブジェクトを初期化 for (I = 0; I < 100; i++) s = xsprintf(&clean, “%s %d”, s, i); puts(s); Clean_out(&clean); sのサイズとかを気にせずに、xsprintfでペタ ペタと文字列をつないでいるだけ。 気楽! (まあ多分この書き方は遅いだろうけど) こんなに雑なことをしても、clean-outで全 部片づけられる。 デストラクタの呼び忘れ防止だけではなく、 もっと積極的に活用してみました!
  8. Matrixクラスの例(行列クラス) • add、sub、mulはMatrix *を返しています。 • C言語ではメンバ関数が使えないですが、自作のトランスコン パイラを使って、ごまかしています( -> の代わりに .

    が使え ます)。 • (add、sub、mulの引数内の&cがうっとうしい・・・) Begin(c); Matrix *A = Mat22_init(1, 2, 3, 4, &c); Matrix *B = Mat22_init(5, 7, 8, 6, &c); Matrix *C1 = A.add(B, &c).mul(A.sub(B, &c), &c); // (A+B)*(A-B) Matrix *C2 = A.mul(A, &c).sub(B.mul(B, &c), &c); // A*A-B*B C1.print("C1="); C2.print("C2="); Clean_out(&c);
  9. C++ との比較 [clean-out前提の方式 (Javaに似ている)] 変数の寿命が尽きても、clean-outするまではオブジェ クトは消えない。だから以下ができる。 変数(ポインタ)が指しているオブジェクトはいろいろ。 誰からも参照されない値があってもいい(オブジェクト1) 複数の変数から参照されていてもいい(オブジェクト2) abc

    def ghi オブジェクト1 オブジェクト2 オブジェクト3 [C++方式 (参照カウントを使わない場合)] 変数の寿命が尽きれば、オブジェクトは消える。 変数が指しているオブジェクトは固定。 「どのオブジェクトを指すか」を変えずに、オブ ジェクトの中身を書き換えることで、値を変更す る。 変数の寿命が尽きたらオブジェクトが消えちゃう ので、他の変数が参照していては危ない。 だから abc = ghi; ってするだけで代入できる。 ムーブとかは気にしないでいい。 ただし、オブジェクトは原則として書き換えない。 変更した値が欲しければ、新規にオブジェクトを 作って、そこを参照させる。
  10. xsprintf2() char *xsprintf(Clean *c, const char *f, ...) { char

    s[1024 * 1024]; va_list ap; va_start(ap, f); int l = vsnprintf(s, sizeof s, f, ap); if (strcmp(s, “0”) == 0) return “0”; char *t = aMalloc(l + 1, c); strcpy(t, s); va_end(ap); return t; } もし仮に“0”が頻出なら、こんな特別処理を 書くこともできる。 “0”はCleanに登録しないので少し速い。メ モリも節約できる。 C++では、「ヒープメモリ上のポインタを 返す」か「文字リテラルを返す」か、どち らなのかわからない関数は基本的によろし くない。 (freeするべきかどうか判断に困るから。) でもclean-out方式なら、登録しないだけな ので、問題なくできる。