智慧指標引入的執行期開銷常被開發者質疑。本文設計嚴謹微基準:300 萬次 `push`/`pop` 循環,自訂 `operator new/delete` 精確統計目標型別配置/釋放次數,對比 **裸指標**、**unique_ptr**、**shared_ptr** 在 `std::queue` 中的實際耗時,並示範 `std::move` 正確移動語義用法。
測試架構
- 目標型別:`int` (單一標量,最小化型別本身開銷)
- 容器:`std::queue` / `std::queue>` / `std::queue>`
- 迴圈次數:`MAX_LOOP = 3,000,000`
- 計時:`GetTickCount()` 毫秒級 (Windows)
- 記憶體統計:重載 `new(size_t, int flag)` / `delete(void*)`,旗標 `MY_INT_FLAG=0x12ff0102` 標記目標配置
核心程式碼片段
自訂 new/delete 統計
static size_t s_my_int_count = 0;
const int MY_INT_FLAG = 0x12ff0102;
void* operator new(size_t size, int flag) throw(std::bad_alloc) {
int addLen = sizeof(size_t);
void* p = std::malloc(addLen + 4 + size);
if (!p) throw std::bad_alloc();
if (flag == MY_INT_FLAG) ++s_my_int_count;
memcpy(p, &size, addLen);
memcpy((char*)p + addLen, &flag, 4);
return (char*)p + addLen + 4;
}
void operator delete(void* q) throw() {
int addLen = sizeof(size_t);
void* p = (char*)q - addLen - 4;
int flag; memcpy(&flag, (char*)p + addLen, 4);
if (flag == MY_INT_FLAG) --s_my_int_count;
std::free(p);
}
裸指標版本 (手動 delete)
queue q;
for (int i=0; i
unique_ptr 版本 (移動語義)
queue> q;
for (int i=0; i p(new(MY_INT_FLAG) int);
q.push(std::move(p)); // 必須 move,unique_ptr 不可複製
}
while (!q.empty()) {
unique_ptr p = std::move(q.front()); // front() 回傳左值引用,需 move 取得所有權
q.pop();
// 自動 delete
}
shared_ptr 版本 (複製語義、引用計數)
queue> q;
for (int i=0; i(new(MY_INT_FLAG) int)); // 複製構造,原子增減引用計數
}
while (!q.empty()) { q.pop(); } // 自動釋放
實測結果 (Windows, Release 建置)
| 指標類型 | 耗時 (ms) | 相對比率 | 關鍵開銷來源 |
| 裸指標 (raw) | 5,375 | 1.00x (基準) | 無額外開銷 |
| unique_ptr | 7,313 | 1.36x | 移動構造/賦值、空指標檢查、刪除器呼叫 |
| shared_ptr | 11,171 | 2.08x | 控制塊配置 (雙重配置)、原子引用計數增減、快取未命中 |
結論:`unique_ptr` 開銷僅 **36%**,`shared_ptr` 約 **2 倍**。若所有權單一且不需共享,**優先使用 `unique_ptr`**。
關鍵 C++11/14 技術點解析
| 特性 | 程式碼體現 | 效能影響 |
| 移動構造/賦值 | `unique_ptr` 刪除複製、提供 `move` | 避免深層複製,僅轉移指標所有權 |
| `std::move` | `q.push(std::move(p))` | 將左值轉為右值參考,啟用移動語義 |
| 控制塊 | `shared_ptr` 內部配置控制塊 | 額外堆配置、原子操作、記憶體局部性差 |
| 自定義刪除器 | `unique_ptr` | 零開銷 (空基類優化 EBO),支援陣列 `unique_ptr` |
最佳實務建議
- 預設選 `unique_ptr`:單一所有權、零成本抽象、編譯期防止複製錯誤。
- 需共享所有權才用 `shared_ptr`:多執行緒共享、觀察者模式、快取。
- 避免 `shared_ptr` 循環引用:使用 `weak_ptr` 打破環。
- 陣列用 `unique_ptr` 或 `shared_ptr` (C++17):正確呼叫 `delete[]`。
- 自訂配置器:大量小物件配置時,搭配物件池/記憶體池 (`operator new` 重載) 可進一步大幅降低配置開銷,效果遠超指標選擇差異。
擴展驗證:編譯器優化影響
- **Release /O2** 開啟內聯、移除邊界檢查、優化原子操作,上述比率具代表性。
- **Debug** 模式下智慧指標檢查 (如 `_DEBUG` 疊代器偵錯) 開銷劇增,**不可用 Debug 效能判斷 Release 表現**。
- 現代編譯器 (MSVC 19.3+, GCC 11+, Clang 13+) 對 `unique_ptr` 幾乎零開銷內聯,`shared_ptr` 原子操作仍為主要成本。
以實測數據為依據,在架構設計時自信選擇智慧指標,兼得安全與效能。
- 上一篇文章: Java 取得系統屬性與環境變數完整指南:System.getProperties、getenv、常用鍵值對照表
- 下一篇文章: C++ 避免函數回傳物件觸發拷貝建構:explicit 拷貝建構子強制移動語義與 NRVO/RVO 編譯器優化
Copyright ©
程式碼之家 All Rights Reserved