當(dāng)我把 push_back 換成 emplace_back 后,代碼性能竟然......
大家好,我是小康。
你有沒有過這樣的經(jīng)歷?寫了一大堆代碼,明明邏輯沒問題,程序跑得卻像蝸牛一樣慢。特別是當(dāng)你在處理大量數(shù)據(jù),往容器里瘋狂塞東西的時(shí)候。
如果你經(jīng)常和 C++ 的 vector、list 這些容器打交道,那么今天這篇文章絕對值得你花幾分鐘時(shí)間——因?yàn)槲乙嬖V你一個(gè)小技巧,它能讓你的代碼不僅寫起來更爽,還能跑得更快!
它就是容器的"隱藏技能":emplace_back()。
"又是一個(gè)新函數(shù)?我的push_back不香了嗎?"
別急,咱們先來看個(gè)例子,感受一下這兩者的區(qū)別:
#include <vector>
#include <string>
class Person {
public:
Person(std::string name, int age) : m_name(name), m_age(age) {
printf("構(gòu)造了一個(gè)人:%s, %d歲\n", name.c_str(), age);
}
// 拷貝構(gòu)造函數(shù)
Person(const Person& other) : m_name(other.m_name), m_age(other.m_age) {
printf("拷貝構(gòu)造了一個(gè)人:%s\n", m_name.c_str());
}
private:
std::string m_name;
int m_age;
};
int main() {
std::vector<Person> people;
printf("=== 使用push_back ===\n");
people.push_back(Person("張三", 25));
people.clear(); // 清空容器
printf("\n=== 使用emplace_back ===\n");
people.emplace_back("李四", 30);
}
運(yùn)行這段代碼,你會看到這樣的輸出:
=== 使用push_back ===
構(gòu)造了一個(gè)人:張三, 25歲
拷貝構(gòu)造了一個(gè)人:張三
=== 使用emplace_back ===
構(gòu)造了一個(gè)人:李四, 30歲
看出區(qū)別了嗎?
- 使用push_back時(shí),我們先創(chuàng)建了一個(gè)臨時(shí)的 Person 對象,然后 vector 把它拷貝到容器里
- 而使用emplace_back時(shí),我們直接傳入構(gòu)造 Person 所需的參數(shù),vector 直接在容器內(nèi)部構(gòu)造對象
結(jié)果就是:push_back額外調(diào)用了一次拷貝構(gòu)造函數(shù),而emplace_back沒有!
"所以emplace_back就是直接傳構(gòu)造函數(shù)參數(shù)?"
沒錯(cuò)!這就是它最大的特點(diǎn)。
- push_back(x) 需要你先構(gòu)造好一個(gè)對象x,然后把它放進(jìn)容器
- emplace_back(args...) 則是直接把構(gòu)造函數(shù)的參數(shù) args 傳進(jìn)去,在容器內(nèi)部構(gòu)造對象
這個(gè)差別看似小,實(shí)際上在性能上卻能帶來很大的提升,尤其是當(dāng):
對象構(gòu)造成本高(比如有很多成員變量)
拷貝成本高(比如內(nèi)部有動態(tài)分配的內(nèi)存)
你需要插入大量對象時(shí)
"來點(diǎn)實(shí)際的例子!"
好的,我們來看一個(gè)真正能展示差異的例子。我們創(chuàng)建一個(gè)拷貝成本真正很高的類,這樣才能看出 emplace_back 的威力:
#include <vector>
#include <string>
#include <chrono>
#include <iostream>
#include <memory>
// 設(shè)計(jì)一個(gè)拷貝成本很高的類
class ExpensiveToCopy {
public:
// 構(gòu)造函數(shù) - 創(chuàng)建一個(gè)大數(shù)組
ExpensiveToCopy(const std::string& name, int dataSize)
: m_name(name), m_dataSize(dataSize) {
// 分配大量內(nèi)存,模擬昂貴的資源
m_data = new int[dataSize];
for (int i = 0; i < dataSize; i++) {
m_data[i] = i; // 初始化數(shù)據(jù)
}
}
// 拷貝構(gòu)造函數(shù) - 非常昂貴,需要復(fù)制整個(gè)大數(shù)組
ExpensiveToCopy(const ExpensiveToCopy& other)
: m_name(other.m_name), m_dataSize(other.m_dataSize) {
// 深拷貝,非常耗時(shí)
m_data = new int[m_dataSize];
for (int i = 0; i < m_dataSize; i++) {
m_data[i] = other.m_data[i];
}
// 輸出提示以便觀察拷貝構(gòu)造函數(shù)的調(diào)用情況
std::cout << "拷貝構(gòu)造: " << m_name << std::endl;
}
// 析構(gòu)函數(shù)
~ExpensiveToCopy() {
delete[] m_data;
}
// 禁用賦值運(yùn)算符以簡化例子
ExpensiveToCopy& operator=(const ExpensiveToCopy&) = delete;
private:
std::string m_name;
int* m_data;
int m_dataSize;
};
// 計(jì)時(shí)輔助函數(shù)
template<typename Func>
long long timeIt(Func func) {
auto start = std::chrono::high_resolution_clock::now();
func();
auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
}
int main() {
const int COUNT = 100; // 對象數(shù)量,減少一點(diǎn)以便看到輸出
const int DATA_SIZE = 100000; // 每個(gè)對象中數(shù)組的大小
std::cout << "=== 測試push_back ===\n";
long long pushTime = timeIt([&]() {
std::vector<ExpensiveToCopy> objects;
objects.reserve(COUNT); // 預(yù)分配空間避免重新分配的影響
for (int i = 0; i < COUNT; i++) {
// 創(chuàng)建臨時(shí)對象然后放入vector
// 這個(gè)過程會調(diào)用拷貝構(gòu)造函數(shù)
objects.push_back(ExpensiveToCopy("對象" + std::to_string(i), DATA_SIZE));
}
});
std::cout << "\n=== 測試emplace_back ===\n";
long long emplaceTime = timeIt([&]() {
std::vector<ExpensiveToCopy> objects;
objects.reserve(COUNT); // 預(yù)分配空間避免重新分配的影響
for (int i = 0; i < COUNT; i++) {
// 直接傳遞構(gòu)造函數(shù)參數(shù)
// 直接在vector內(nèi)部構(gòu)造對象,避免了拷貝
objects.emplace_back("對象" + std::to_string(i), DATA_SIZE);
}
});
std::cout << "\npush_back耗時(shí): " << pushTime << " 微秒" << std::endl;
std::cout << "emplace_back耗時(shí): " << emplaceTime << " 微秒" << std::endl;
double percentDiff = (static_cast<double>(pushTime) / emplaceTime - 1.0) * 100.0;
std::cout << "性能差異: push_back比emplace_back慢了 " << percentDiff << "%" << std::endl;
}
在我的電腦上,大約是這樣的結(jié)果:
=== 測試emplace_back ===
push_back耗時(shí): 66979 微秒
emplace_back耗時(shí): 35858 微秒
性能差異: push_back比emplace_back慢了 86.7896%
這意味著push_back比emplace_back慢了約86%!這可不是小數(shù)目,尤其是在處理大對象時(shí)。
"看起來emplace_back完勝啊!為什么還有人用push_back?"
好問題!emplace_back雖然在大多數(shù)情況下更快,但并不是所有場景都適合用它:
- 當(dāng)你已經(jīng)有一個(gè)現(xiàn)成的對象時(shí),push_back可能更直觀
- 對于基本類型(int, double等),兩者性能差異可以忽略不計(jì)
- 對于某些編譯器優(yōu)化情況,比如移動語義,差距可能不明顯
來看一個(gè)例子,說明什么時(shí)候兩者其實(shí)差不多:
std::vector<int> numbers;
// 對于基本類型,這兩個(gè)是等價(jià)的
numbers.push_back(42);
numbers.emplace_back(42);
// 如果已經(jīng)有一個(gè)現(xiàn)成的對象
std::string name = "張三";
std::vector<std::string> names;
// 這種情況下,如果 string 支持移動構(gòu)造,兩者性能接近
names.push_back(name); // 拷貝name
names.push_back(std::move(name)); // 移動name(推薦)
names.emplace_back(name); // 拷貝name
names.emplace_back(std::move(name)); // 移動name(推薦)
"完美轉(zhuǎn)發(fā)是什么鬼?聽說emplace_back跟這個(gè)有關(guān)?"
沒錯(cuò)!emplace_back的強(qiáng)大之處,部分來自于它使用了"完美轉(zhuǎn)發(fā)"(Perfect Forwarding)技術(shù)。
簡單來說,完美轉(zhuǎn)發(fā)就是把函數(shù)參數(shù)"原汁原味"地傳遞給另一個(gè)函數(shù),保持它的所有特性(比如是左值還是右值,是const還是non-const)。
在C++中,這通常通過模板和std::forward實(shí)現(xiàn):
template <typename... Args>
void emplace_back(Args&&... args) {
// 在容器內(nèi)部直接構(gòu)造對象
// 完美轉(zhuǎn)發(fā)所有參數(shù)
new (memory_location) T(std::forward<Args>(args)...);
}
這樣的設(shè)計(jì)讓emplace_back能夠接受任意數(shù)量、任意類型的參數(shù),并且完美地轉(zhuǎn)發(fā)給對象的構(gòu)造函數(shù)。這就是為什么你可以直接這樣寫:
people.emplace_back("張三", 25); // 直接傳構(gòu)造函數(shù)參數(shù)
而不需要先構(gòu)造一個(gè)對象。
"還有其他 emplace 系列函數(shù)嗎?"
是的!STL容器中有一系列的emplace函數(shù):
- vector、deque、list: emplace_back()
- list, forward_list: emplace_front()
- 所有容器: emplace()(在指定位置構(gòu)造元素)
- 關(guān)聯(lián)容器(map, set等): emplace_hint()(帶提示的插入)
它們的共同點(diǎn)是:直接在容器內(nèi)部構(gòu)造元素,而不是先構(gòu)造再拷貝/移動。
實(shí)戰(zhàn)建議:什么時(shí)候用 emplace_back?
- 復(fù)雜對象插入:當(dāng)你要插入的對象構(gòu)造成本高、拷貝代價(jià)大時(shí)
- 大量數(shù)據(jù)操作:需要插入大量元素時(shí),性能差異會更明顯
- 直接傳參更方便時(shí):比如插入 pair 到 map
// 不那么優(yōu)雅
std::map<int, std::string> m;
m.insert(std::make_pair(1, "one"));
// 更優(yōu)雅,也更高效
m.emplace(1, "one");
- 臨時(shí)對象場景:當(dāng)你需要?jiǎng)?chuàng)建臨時(shí)對象并插入容器時(shí)
總結(jié)
emplace_back本質(zhì)上是通過減少不必要的對象創(chuàng)建和拷貝來提升性能。它利用了 C++ 的完美轉(zhuǎn)發(fā)功能,讓你可以直接傳遞構(gòu)造函數(shù)參數(shù),而不需要先創(chuàng)建臨時(shí)對象。
在處理復(fù)雜對象或大量數(shù)據(jù)時(shí),這種優(yōu)化尤為明顯。當(dāng)然,對于簡單類型或已有對象,兩者差異不大。
所以下次當(dāng)你在寫:
myVector.push_back(MyClass(arg1, arg2));
的時(shí)候,不妨試試:
myVector.emplace_back(arg1, arg2);
代碼更簡潔,運(yùn)行更高效,何樂而不為呢?
記住,在編程世界里,這種看似微小的優(yōu)化,累積起來就是質(zhì)的飛躍!