Vec<T>は動的な、成長可能な配列であり、要素は単一の割り当てられた(ヒープ上の)メモリブロックに保存されます。新しい要素の追加(push)は長さを増加させ、必要に応じて新しいメモリが割り当てられます(再アロケーション)。pushの際には、常に再アロケーションを避けるために、capacityを指数関数的に増加させます。要素の削除(pop/remove)時には、capacityは自動的には減少しません。
よくある問題は、繰り返し追加する際の過剰なアロケーションと再アロケーションです。
事前割り当てを使用した例:
let mut v = Vec::with_capacity(1000); for i in 0..1000 { v.push(i); } assert_eq!(v.capacity(), 1000);
質問: v.shrink_to_fit()を呼び出した後、vecのcapacityはどうなりますか?長さと正確に等しくなりますか?
間違った答え: はい、常に、shrink_to_fitの後にcapacity == len。
正しい答え: 必ずしもそうではありません。shrink_to_fitの実装は、アロケータに対する「希望」です。通常は可能な限り最小のcapacityにしようとしますが、アロケータの実装によっては(例えば、長さよりも高い状態になることがあります)特性が異なる場合があります。
例:
let mut v = Vec::with_capacity(10); for i in 0..5 { v.push(i); } v.shrink_to_fit(); // capacity ≥ len (5)ですが、== lenであることは保証されていません
物語
開発者はつねにcapacityを指定せずにVecにオブジェクトをプッシュし続け、データ量が多い場合に再アロケーションが指数的に増加し、「重い」ループで全体の処理を遅延させました。
with_capacityを使った最適化により、時間を10倍短縮しました。
物語
チームは、各pop()の後に定期的に
shrink_to_fitを呼び出すことでメモリを節約しようとしました。その結果、常に再アロケーション/freeのループが発生し、パフォーマンスが低下し、サービスをDoSに陥らせる危険がありました。
物語
Vecを構造体のフィールドとして内部参照付きで使用していました。再アロケーション後(capacityを超えるpush)のリンクが無効化され、発生したバグはproductionに投入するまで計算が困難でした。