$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言語でメモリ管理を考えた話
    サイボウズ・ラボ
    川合秀実

    View Slide

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

    View Slide

  3. でもC++がうらやましい
    • C++では、オブジェクトを宣言しているスコープ(コードブロッ
    ク)を抜けると、自動でデストラクタを呼んでくれる機能がありま
    す。
    • 当たり前ですが、C言語にはそんな機能はありません。明示的に
    デストラクタ関数を呼ぶ必要があります。
    • 私はC++のこれがうらやましくてたまりません。デストラクタを
    呼び忘れないように気を付けるのは生産的じゃないです。
    {
    ….
    ClassAbc abc(1, 2, 3);
    ….

    } // スコープを抜けると自動でabcのデストラクタを呼びだしてくれる

    View Slide

  4. Cleanクラス
    • 似たようなことをどうしてもやりたくて、以下のようなものを
    作りました。
    • Cleanクラス
    • 関数のポインタと引数を複数登録できる構造体
    • Clean_out()関数で、それらの関数を登録とは逆順に全部呼び出す
    関数ポインタ 引数1 引数2 引数3
    関数ポインタ 引数1 引数2 引数3
    関数ポインタ 引数1 引数2 引数3
    関数ポインタ 引数1 引数2 引数3
    登録方向 呼出方向

    View Slide

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


    Clean_set(clean, &ClassAbc_deinit, …); // ここでデストラクタを登録
    }
    void ClassAbc_deinit(…) // ClassAbcのデストラクタ
    {

    }

    View Slide

  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される
    }

    View Slide

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

    View Slide

  8. 本当に悩みは減ったのか?
    Begin(clean);
    オブジェクトをたくさん作って、最後にたくさん捨てる範囲
    Clean_out(&clean);
    演算の結果など、外に出さなければいけないオブジェクト
    外に出さなきゃいけないものだけを、外の
    cleanオブジェクトのポインタを指定して、
    クローンする
    結果などのオブジェクト
    外側のスコープ

    View Slide

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

    View Slide

  10. Cleanが使えないケース
    • オンメモリの簡易データベースのクラス
    • データをINSERTしたりDELETEしたり
    • DELETEしたものをデストラクタのClean_outで片付けようと
    すると、データベースのデストラクタを呼びだすまで片付かな
    いことになる(それはきっとまずい)
    • だからこれはClean_out方式が向いていない
    • こういうときは、従来通りの方法(自分でfree)でやる。
    • Clean *を指定する文脈でNULLを指定すれば、デストラクタやfreeを
    Cleanオブジェクトに登録しなくなるので、それらについては自前で
    管理するようにする。

    View Slide

  11. そのほかの補助ツール
    • 「コンストラクタが呼ばれたらポインタを登録して、デストラクタが呼ば
    れたら、登録ポインタを削除する表」を作ったので、登録状態を確認する
    ことで、デストラクタの呼び忘れを見つけやすくなりました(デバッグ時
    のみ有効)。
    • メンバ関数では、冒頭でポインタが前述の表に登録済みかを確認するので、
    デストラクタ呼び出しが早すぎる場合も気づけます(デバッグ時のみ有
    効) 。
    • 「mallocしたら登録して、freeしたら削除する表」もあるので、メモリ
    リークも見つけられます(デバッグ時のみ有効) 。
    • Cleanオブジェクトはスコープのネストを登録可能で、中にClean_outし
    ていないオブジェクトが残っている場合、「エラーにする」or「先に内側
    をClean_outする」を選べます。通常は書き忘れが疑われるのでエラーを
    選びますが、longjmpのときは内側Clean_outを選びます。

    View Slide

  12. ここまで作った上の感想
    • もうオブジェクトの寿命では悩まない!
    • ミスがないことにも自信が持てるようになりました。
    • Clean_outを使うと、実行ファイルは1~2KBほど増えます。これは
    私としては十分に許容範囲です!
    • Clean_outすら書かなくてもいいC++や、ガーベージコレクションの
    ある言語(たとえばJava)には負けるけど、C言語の軽さも魅力なの
    で、私はもうあまり他言語をうらやましいとは思わなくなりました。
    私としては、CleanクラスがあればC言語はいいバランスです!
    • 現状では、何をするにもCleanオブジェクトのポインタ指定が必要で、
    それが少しうっとうしいので、自作言語でこれをきれいに書けるよ
    うにしたいです。なんなら、ブロックを抜けるときに、自動で
    Clean_outするコードを出力させればいいかもしれないです。

    View Slide

  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で全
    部片づけられる。
    デストラクタの呼び忘れ防止だけではなく、
    もっと積極的に活用してみました!

    View Slide

  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);

    View Slide

  15. C++ との比較
    [clean-out前提の方式 (Javaに似ている)]
    変数の寿命が尽きても、clean-outするまではオブジェ
    クトは消えない。だから以下ができる。
    変数(ポインタ)が指しているオブジェクトはいろいろ。
    誰からも参照されない値があってもいい(オブジェクト1)
    複数の変数から参照されていてもいい(オブジェクト2)
    abc def ghi
    オブジェクト1 オブジェクト2
    オブジェクト3
    [C++方式 (参照カウントを使わない場合)]
    変数の寿命が尽きれば、オブジェクトは消える。
    変数が指しているオブジェクトは固定。
    「どのオブジェクトを指すか」を変えずに、オブ
    ジェクトの中身を書き換えることで、値を変更す
    る。
    変数の寿命が尽きたらオブジェクトが消えちゃう
    ので、他の変数が参照していては危ない。
    だから abc = ghi; ってするだけで代入できる。
    ムーブとかは気にしないでいい。
    ただし、オブジェクトは原則として書き換えない。
    変更した値が欲しければ、新規にオブジェクトを
    作って、そこを参照させる。

    View Slide

  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方式なら、登録しないだけな
    ので、問題なくできる。

    View Slide

  17. まとめ
    • くだらない動機(C++がうらやましいけど、C++は使いたくな
    い)でささやかな規模のものを作っただけだったのに、意外に
    役立つ面白いものができたと思います。
    • 簡易データベースの例のように、この枠組みですべてカバー
    できるというわけではないけど、だからといってClean_outが
    ダメだということはないと思います。得意・不得意があるのは
    気にしなくていいと思います。わずかな短所のために、多くの
    長所をあきらめるのはもったいないです。
    • これをもっとすっきり書ける自作言語ができたら、相当に面白
    いことになる気がします!

    View Slide

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

    View Slide