1. ホーム
  2. c

[解決済み] なぜC言語の `free` は解放するバイト数を受け取らないのですか?

2023-04-20 06:04:15

質問

はっきりさせておきたいのですが、私が知っているのは mallocfree はCライブラリで実装されており、通常はOSからメモリの塊を割り当て、より小さなロットのメモリをアプリケーションに小分けするための独自の管理を行い、割り当てられたバイト数を記録しています。 この問題は free はどのようにして解放する量を知るのか .

むしろ知りたいのは、なぜ free がそもそもこのように作られたのかを知りたいのです。低レベル言語である C 言語では、どのメモリがどれだけ割り当てられたかだけでなく、その量も記録するように C プログラマに求めるのは完全に合理的だと思います (実際、私は通常、とにかく割り当てられたバイト数を記録することにしています)。また、明示的にバイト数を free たとえば、異なる割り当てサイズに対して別々のプールを持つアロケータは、入力引数を見るだけで、どのプールから解放するかを決定することができ、全体としてより少ないスペース オーバーヘッドとなるでしょう。

というわけで、要するに、なぜ mallocfree は、内部で割り当てられたバイト数を記録するように要求されるように作成されたのでしょうか?それは単なる歴史的な事故なのでしょうか?

小さな編集です。 何人かの人が、「割り当てた量と異なる量を解放したらどうなるか」というようなポイントを提供しました。私の想像する API は、単に、割り当てられたバイト数を正確に解放することを要求することができます。私は、他の可能性についての議論を妨げたいわけではありません。

どのように解決するのか?

1つの引数 free(void *) (Unix V7 で導入) には、以前の 2 つの引数である mfree(void *, size_t) に比べてもう一つ大きな利点があります。 free は劇的にすべての その他の ヒープメモリで動作する他のすべての API を劇的に簡素化します。例えば、もし free がメモリブロックの大きさを必要としていた場合 strdup は何らかの方法で1つの値(ポインタ)ではなく2つの値(ポインタ+サイズ)を返さなければならず、Cは複数値のリターンを単一値のリターンに比べてはるかに面倒にします。代わりに char *strdup(char *) の代わりに、次のように書かなければなりません。 char *strdup(char *, size_t *) と書くか、あるいは struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *) . (最近では、NUL終端文字列が コンピューティングの歴史の中で最も破滅的な設計バグであることを知っているからです。 しかし、それは後知恵の話です。70 年代には、C 言語が文字列を単純な char * として文字列を扱えるということは、実際には Pascal や Algol といった競合製品に対する決定的な優位性であると考えられていました。 .) さらに、それは単に strdup だけでなく、ヒープメモリを割り当てるすべてのシステム関数やユーザ定義関数に影響します。

初期の Unix の設計者は非常に賢い人たちでした。 free よりも mfree ということで、基本的には、このことに気づいて、それに従ってシステムを設計したというのが答えだと思います。その決断の瞬間に、彼らの頭の中で何が起こっていたのか、直接の記録は見つからないでしょう。しかし、私たちは想像することができます。

V6 Unix 上で動作するアプリケーションを C 言語で書いていると仮定して、その 2 つの引数である mfree . これまでは何とかなっていたのですが、このポインタのサイズを追跡することが、プログラムが進むにつれて面倒になってきました。 がより野心的になるにつれて ヒープに割り当てられた変数をより多く使用する必要があるためです。しかし、そこであなたは素晴らしいアイデアを思いつきました。 size_t を常にコピーする代わりに、割り当てられたメモリ内に直接サイズを格納するユーティリティ関数をいくつか書けばよいのです。

void *my_alloc(size_t size) {
    void *block = malloc(sizeof(size) + size);
    *(size_t *)block = size;
    return (void *) ((size_t *)block + 1);
}
void my_free(void *block) {
    block = (size_t *)block - 1;
    mfree(block, *(size_t *)block);
}

そして、これらの新しい関数を使ってコードを書けば書くほど、これらの関数はより素晴らしいものに見えてきます。コードを書きやすくするだけでなく はあなたのコードを を高速化します。 -- という、あまり両立しない2つのことを実現しました。以前は、これらの size_t をあちこちにコピーしていたため、CPU のオーバーヘッドが増え、レジスタを頻繁に流出させなければならなくなり (特に関数の追加引数のために)、メモリも無駄になりました (関数呼び出しが入れ子になると size_t の複数のコピーが異なるスタックフレームに格納されるため)。新しいシステムでは、依然として size_t を格納するためにメモリを使う必要がありますが、それは 1 回だけで、どこにもコピーされることはありません。これらは小さな効率に見えるかもしれませんが、256 KB の RAM を搭載したハイエンド マシンの話であることに留意してください。

これであなたはハッピーです! そこであなたは、次の Unix リリースに取り組んでいる髭面の男性たちとあなたのクールなトリックを共有しますが、それは彼らを幸せにするのではなく、悲しませることになります。ちょうどその頃、彼らは strdup のような新しいユーティリティ関数をたくさん追加しているところで、あなたのクールなトリックを使っている人たちは、彼らの新しい関数を使うことができないことに気づきました。そしてそれはあなた自身も悲しくなります。 strdup(char *) 関数を自分で書き直さなければならないことに気づくからです。

しかし、待ってください! これは 1977 年の話であり、後方互換性はあと 5 年は発明されないでしょう! それに、まじめな人は誰も実際に を使うことはありません。 この無名の "Unix" を使っている人はいないのです。K&Rの初版は今出版社に向かっているところですが、それは問題ありません--最初のページには「Cは文字列のような複合オブジェクトを直接扱う操作を提供しない...ヒープもない..."」と書いてあるのです。この時点では string.hmalloc はベンダーの拡張機能です(!)。そこで、ヒゲ男1号が提案するのは、それらを好きなように変更することができるということです。トリッキーなアロケータを宣言するのは 公式 アロケータであると宣言してはどうでしょうか?

数日後、髭男爵 2 号は新しい API を見て、ちょっと待てよ、これは前より良くなっているが、サイズを保存するために 1 回の割り当てごとに単語全体を費やしている、と言いました。彼はこれを神への冒涜と見なしました。他のみんなは、彼がおかしいという目で見ています。その夜、彼は遅くまで残って新しいアロケーターを発明しました。それはサイズを全く保存せず、代わりにポインター値に対して黒魔術のビットシフトを行うことでサイズをその場で推測し、新しいAPIをそのままにそれをスワップするものでした。新しい API は、誰もその切り替えに気づかないことを意味しますが、翌朝、コンパイラが使用する RAM が 10% 少ないことに気づきます。

そして今、誰もが満足しています。あなたは書きやすく高速なコードを手に入れ、ひげ男 1 号はシンプルで素敵な strdup を書くことができ、ひげ男2号はちょっとだけ得をした気分になって quines をいじくり回す . 出荷しろ!

少なくとも、そのような が起こったのです。