realloc
への不必要な呼び出しが最適化されていると便利な状況に遭遇しました。ただし、clangもgccもそのようなことをしないようです( godbolt )。 -malloc
を複数回呼び出すと最適化が行われますが。
例:
void *myfunc() {
void *data;
data = malloc(100);
data = realloc(data, 200);
return data;
}
次のようなものに最適化されることを期待していました。
void *myfunc() {
return malloc(200);
}
なぜclangもgccも最適化しないのですか? -許可されていませんか?
彼らはそうすることを許されていませんか?
たぶん、しかし、この場合に行われない最適化は、コーナー機能の違いが原因かもしれません。
割り当て可能なメモリが150バイト残っている場合、data = malloc(100); data = realloc(data, 200);
はNULL
を返しますが、100バイトが消費(およびリーク)され、残りは50バイトです。
data = malloc(200);
はNULL
を返し、0バイトが消費され(リークなし)、150バイトが残ります。
異なる機能この狭いケースでは、最適化が妨げられる場合があります。
コンパイラーはreallocの最適化を許可されていますか?
おそらく-私はそれが許可されることを期待するでしょう。しかし、コンパイラを強化していつ実行できるかを判断することは、効果に値しない場合があります。
...
がメモリの一部を設定した可能性がある場合、成功したmalloc(n); ... realloc(p, 2*n)
はmalloc(2*n);
とは異なります。
...
が空のコードであってもメモリを設定しなかったことを保証することは、コンパイラの設計を超えている可能性があります。
作成者が努力する価値があると考えた場合、malloc/calloc/free/reallocの自己完結型バージョンをバンドルするコンパイラは、示された最適化を合法的に実行できます。外部提供の関数にチェーンするコンパイラーは、そのような関数への呼び出しの正確なシーケンスを観察可能な副作用と見なさないと文書化した場合でも、そのような最適化を実行できますが、そのような扱いはもう少し手間がかかる可能性があります。
Malloc()とrealloc()の間にストレージが割り当てられていない場合、realloc()のサイズは、malloc()の実行時に認識され、realloc()サイズはmalloc()サイズよりも大きくなります。 malloc()操作とrealloc()操作を単一のより大きな割り当てに統合することは理にかなっています。ただし、その間にメモリの状態が変化する可能性がある場合、そのような最適化により、成功するはずの操作が失敗する可能性があります。たとえば、次のシーケンスを考えます:
void *p1 = malloc(2000000000);
void *p2 = malloc(2);
free(p1);
p2 = realloc(p2, 2000000000);
システムは、p1が解放されるまで、p2に2000000000バイトを使用できない場合があります。コードを次のように変更する場合:
void *p1 = malloc(2000000000);
void *p2 = malloc(2000000000);
free(p1);
その結果、p2の割り当てが失敗します。標準は割り当て要求が成功することを保証しないため、このような動作は不適合ではありません。一方、以下も「適合」実装です。
void *malloc(size_t size) { return 0; }
void *calloc(size_t size, size_t count) { return 0; }
void free(void *p) { }
void *realloc(void *p, size_t size) { return 0; }
このような実装は、おそらく他のほとんどのものよりも「効率的」と見なされる可能性がありますが、おそらく、上記の関数がコードパスで呼び出されるまれな状況を除いて、非常に有用であると考えるにはかなり鈍感にならなければなりません決して実行されません。
少なくとも元の質問の場合と同じくらい簡単な場合には、標準は明らかに最適化を許可すると思います。そうでなければ成功する可能性のある操作が失敗する可能性がある場合でも、規格はそれを許可します。おそらく、多くのコンパイラが最適化を実行しない理由は、作成者が、安全で有用なケースを特定するために必要な労力を正当化するのに十分な利点があると著者が考えなかったためです。
コンパイラーは、純粋な関数と見なされる関数への複数の呼び出しを最適化することを許可されています。 -効果。
したがって、問題はrealloc()
が純関数かどうかです。
C11標準委員会ドラフトN1570は、realloc
関数について次のように述べています。
7.22.3.5 realloc関数
...
2。realloc
関数は、ptrが指す古いオブジェクトの割り当てを解除し、sizeで指定されたサイズを持つ新しいオブジェクトへのポインターを返します。新しいオブジェクトの内容は、新しいサイズと古いサイズのうち小さい方まで、割り当て解除前の古いオブジェクトの内容と同じでなければなりません。古いオブジェクトのサイズを超える新しいオブジェクトのバイトには不定値があります。戻り値
4。realloc
関数は、新しいオブジェクトへのポインター(mayは古いオブジェクトへのポインターと同じ値を持つ)、または新しいオブジェクトを割り当てることができなかった場合は、nullポインター。
コンパイラーは、コンパイル時に、各呼び出しで返されるポインターの値を予測できないことに注意してください。
これは、realloc()
を純粋な関数と見なすことはできず、コンパイラによる複数の呼び出しを最適化できないことを意味します。
しかし、2番目のrealloc()で使用する最初のmalloc()の戻り値をチェックしていません。同様にNULLにすることもできます。
コンパイラは、最初の戻り値について不当な仮定を行わずに、2つの呼び出しを単一の呼び出しに最適化する方法を教えてください。
次に、別の可能なシナリオがあります。 FreeBSD 以前は持っていた arealloc()
これは基本的にmalloc + memcpy +古いポインタを解放しました。
空きメモリが230バイトしかないと仮定します。その実装では、ptr = malloc(100)
に続いてrealloc(ptr, 200)
が失敗しますが、単一のmalloc(200)
は成功します。
私の理解では、そのような最適化は禁止される可能性があります(特に、malloc
は成功するが、次のrealloc
が失敗する-実際にはほとんどありません)。
malloc
とrealloc
は常に成功すると仮定できます(つまり、C11標準、 n157 に反します。私の joke-implementation of malloc
も調べてください)。その仮説(厳密な制約は間違っていますが、一部のLinuxシステムでは メモリオーバーコミットメント を使用して錯覚を与えています)で GCC を使用すると、独自の GCCプラグイン このような最適化を行います。
そのようなGCCプラグインをコーディングするのに数週間または数ヶ月を費やす価値があるかどうかはわかりません(実際には、おそらくmalloc
とrealloc
の間のいくつかのコードを処理することを望みます。そのような中間コードが許容されるものを検出します)が、その選択はあなた次第です。