$30 off During Our Annual Pro Sale. View Details »

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

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

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

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

Hidemi KAWAI

April 29, 2022
Tweet

More Decks by Hidemi KAWAI

Other Decks in Programming

Transcript

  1. C言語でメモリ管理を考えた話 サイボウズ・ラボ 川合秀実

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

    10~20KBなんて大したことないといえばそうなのですが、私はた いてい20~100KBくらいの開発をするので、C++を使うだけで1~5 割程度大きくなってしまうことになり、それが私には不満だったの で、「C言語だけでどこまでやれるか」を頑張ってみることにしま した。
  3. でもC++がうらやましい • C++では、オブジェクトを宣言しているスコープ(コードブロッ ク)を抜けると、自動でデストラクタを呼んでくれる機能がありま す。 • 当たり前ですが、C言語にはそんな機能はありません。明示的に デストラクタ関数を呼ぶ必要があります。 • 私はC++のこれがうらやましくてたまりません。デストラクタを

    呼び忘れないように気を付けるのは生産的じゃないです。 { …. ClassAbc abc(1, 2, 3); …. … } // スコープを抜けると自動でabcのデストラクタを呼びだしてくれる
  4. Cleanクラス • 似たようなことをどうしてもやりたくて、以下のようなものを 作りました。 • Cleanクラス • 関数のポインタと引数を複数登録できる構造体 • Clean_out()関数で、それらの関数を登録とは逆順に全部呼び出す

    関数ポインタ 引数1 引数2 引数3 関数ポインタ 引数1 引数2 引数3 関数ポインタ 引数1 引数2 引数3 関数ポインタ 引数1 引数2 引数3 登録方向 呼出方向
  5. Cleanクラス • コンストラクタ関数は、引数にCleanオブジェクトへのポイン タを受け付けるようにします。そして初期化が済んだら、 Cleanオブジェクトに自身のデストラクタ関数を登録します。 void ClassAbc_init(…, Clean *clean) //

    ClassAbcのコンストラクタ { … … Clean_set(clean, &ClassAbc_deinit, …); // ここでデストラクタを登録 } void ClassAbc_deinit(…) // ClassAbcのデストラクタ { … }
  6. Cleanクラス • メモリを確保する関数(aMalloc)もCleanオブジェクトへの ポインタを受け付けるようにして、malloc後に、freeのための 関数呼び出しをCleanオブジェクトに登録します。 • これで、どのデストラクタを呼ばなければいけないかとかに悩 むことなく、スコープに入ったらCleanオブジェクトを初期化 して、スコープを抜けるときに機械的にClean_out()を1回呼ぶ だけでOKになりました。

    { Begin(clean); // Cleanオブジェクトを宣言して初期化するマクロ char *p = aMalloc(1234, &clean); // malloc(1234)相当 … Clean_out(&clean); // pはこのタイミングでfreeされる }
  7. 本当に悩みは減ったのか? • [step1] • まず、mainの冒頭でCleanオブジェクトを初期化。 • あとはClean *が必要になったときに、全部このCleanオブジェクトのポイン タを渡す。 •

    mainの最後にClean_out()を記述。 • つまりCleanオブジェクトは全体で1つだけ。とりあえずこれで動く! • これで問題がなければ、これでおしまい。 • [step2] • メモリを使いすぎてよくないところを見つけたら、それを囲める範囲で、 新しいCleanオブジェクトを初期化。そして、この範囲内のオブジェクトの うち、外のスコープに出さなければいけないものだけをcloneして、範囲外で も使えるようにする。 • 厳密に考えすぎず、「ここに来たらもう絶対に触らない」と思える ところでClean_outすればいいだけ。・・・こう思ったら楽でした。
  8. 本当に悩みは減ったのか? Begin(clean); オブジェクトをたくさん作って、最後にたくさん捨てる範囲 Clean_out(&clean); 演算の結果など、外に出さなければいけないオブジェクト 外に出さなきゃいけないものだけを、外の cleanオブジェクトのポインタを指定して、 クローンする 結果などのオブジェクト 外側のスコープ

  9. オブジェクトがなくなったらClean_out • [Q] 単純なスコープではなく、あるオブジェクトがなくなった タイミングでClean_outしたい。どうしたらいいか? • [A] そのオブジェクトのメンバーにCleanオブジェクトを持たせ て、そのオブジェクトのデストラクタ内で、Clean_outすれば いいと思います。

  10. Cleanが使えないケース • オンメモリの簡易データベースのクラス • データをINSERTしたりDELETEしたり • DELETEしたものをデストラクタのClean_outで片付けようと すると、データベースのデストラクタを呼びだすまで片付かな いことになる(それはきっとまずい) •

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

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

    ある言語(たとえばJava)には負けるけど、C言語の軽さも魅力なの で、私はもうあまり他言語をうらやましいとは思わなくなりました。 私としては、CleanクラスがあればC言語はいいバランスです! • 現状では、何をするにもCleanオブジェクトのポインタ指定が必要で、 それが少しうっとうしいので、自作言語でこれをきれいに書けるよ うにしたいです。なんなら、ブロックを抜けるときに、自動で Clean_outするコードを出力させればいいかもしれないです。
  13. さらに応用: 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で全 部片づけられる。 デストラクタの呼び忘れ防止だけではなく、 もっと積極的に活用してみました!
  14. 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);
  15. C++ との比較 [clean-out前提の方式 (Javaに似ている)] 変数の寿命が尽きても、clean-outするまではオブジェ クトは消えない。だから以下ができる。 変数(ポインタ)が指しているオブジェクトはいろいろ。 誰からも参照されない値があってもいい(オブジェクト1) 複数の変数から参照されていてもいい(オブジェクト2) abc

    def ghi オブジェクト1 オブジェクト2 オブジェクト3 [C++方式 (参照カウントを使わない場合)] 変数の寿命が尽きれば、オブジェクトは消える。 変数が指しているオブジェクトは固定。 「どのオブジェクトを指すか」を変えずに、オブ ジェクトの中身を書き換えることで、値を変更す る。 変数の寿命が尽きたらオブジェクトが消えちゃう ので、他の変数が参照していては危ない。 だから abc = ghi; ってするだけで代入できる。 ムーブとかは気にしないでいい。 ただし、オブジェクトは原則として書き換えない。 変更した値が欲しければ、新規にオブジェクトを 作って、そこを参照させる。
  16. 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方式なら、登録しないだけな ので、問題なくできる。
  17. まとめ • くだらない動機(C++がうらやましいけど、C++は使いたくな い)でささやかな規模のものを作っただけだったのに、意外に 役立つ面白いものができたと思います。 • 簡易データベースの例のように、この枠組みですべてカバー できるというわけではないけど、だからといってClean_outが ダメだということはないと思います。得意・不得意があるのは 気にしなくていいと思います。わずかな短所のために、多くの

    長所をあきらめるのはもったいないです。 • これをもっとすっきり書ける自作言語ができたら、相当に面白 いことになる気がします!
  18. 感想 • きっとこういう「くだらない動機でちょっとしたものを作った ら、意外に面白いことになる」ことって、もっと他にも埋もれ ている気がします。 • 立派なものじゃなくても、簡単そうなものでも、とにかく思い ついたら作ってみるのがいいのかな、なんて思いました!