【C++】性能优化细节汇总
1. 使用\n替代std::endl
std::endl
会刷新缓冲区,较慢。
std::cout<<"hello"<<to_string(100)<<"\n";
2. 追求极致性能时,使用protobuf替代json等数据格式。
2. std::string拼接
- 不要用+或+=,因为每次+=实际都会创建新的string,涉及拷贝。
string str = "hello";
str+="world";
int a=1;
str+=to_string(a);
- 改用
std::stringstream
或std::string::reserve
+append
链式调用
stringstream会自行优化拷贝,不会多次创建新的string
stringstream ss;
int a=1;
ss<<"hello"<<"world"<<a<<to_string(3.14);
ss.c_str();
使用string::reserve提前设定string的capacity能够很好的减少扩容的现象
string str;
str.reserve(100);//100个char
str.append("hello").append("world").append(1).append(3.14);
3. 用 string_view 代替 substr
无拷贝切片。
std::string_view ext = path.substr(pos); // 深拷贝→零拷贝
4. 返回空字符
不要直接return ""
,这会调用string(const char*)
,但现代编译器的RVO似乎会优化掉开销。
//用这个
return string();
or
return string{};
5. 大对象作为形参传const引用替代传值
const引用万能引用,左值引用的话传入右值编译不过,右值引用的话传左值编译不过。
void func(const std::string& str){}
5. constexpr函数
允许编译期完成计算,也可在运行时完成计算,由编译器决定。
6. consteval函数
强制在编译期运行,不可在运行时。
对 consteval 函数的每一次调用,都必须在编译期完成求值,且结果只能用作编译期常量。
要求:完成求值所依赖的其他变量,比如入参,也必须是编译期常量。
consteval int sqr(int n) { return n * n; }
int main()
{
int a = sqr(5); // ✅ OK,在编译期求值,a 就是 25
int b = 5;
int c = sqr(b); // ❌ 编译错误,b 不是编译期常量
}
5. inline + static 修饰小函数【定义】放头文件
inline:设置为内联,要求include处就地展开函数定义。修饰头文件中的定义时,会告诉编译器,每个引入该头文件的cpp文件中虽然都include了该定义,但它们都是【同一个】,不要报重定义错误。
static:设置为内部链接,告知编译器每一个include了头文件的cpp文件中都有一份拷贝(可能是变量,可能是函数,这里是函数),链接器不要跨cpp文件去找符号表,省去一次跳转。
综合下来,既进行了内联,又通过static给每个引入函数定义的cpp文件生成了一份拷贝,减少了函数调用时开销,还减少了去其他cpp文件的跳转。
// foo.h
inline static int add(int a, int b) { return a + b; }
6. std::vector若能知道元素个数,务必使用reserve提前设定容量,减少vector自动扩容的开销。
7. 大规模遍历+少数删除场景选用std::vector而不是std::list
虽然教科书上说std::list因为在内存中的存储是通过指针非连续存储,因而增删都是O(1),而std::vector增删是O(n)。
但实际场景中,增删往往伴随着遍历,而std::vector因为是连续内存,因而遍历更快,list需要不断的在内存中通过指针寻找下个节点,效率很低。
CPU有3级缓存L1/2/3,其速度比内存高几个数量级。CPU从内存中加载数据时,不是一个字节一个字节的加载,而是一次取64KB。因而vector中取一个数据时,会将后面的很多数据都一并取来,这样CPU在遍历下一个数据时便会命中,会非常的快。而list因为数据在内存中位置不连续,每次取数据都要取内存中拿。
因此在这样的场景下,使用vector优于list:数据量很多,并伴随着少量的增删。
此时std::vector在遍历上的高性能将弥补其增删时的低性能,而std::list则正相反。
8. 使用unordered_map替代普通map
9. 通过explicit声明构造避免隐式转换减少临时对象
每一次隐式转换往往都会生成一个对用户不可见的、短暂的临时对象,然后再用这个临时对象去调用真正的函数或赋值。
这个过程包含了:构造+拷贝+析构,这都是开销,而且是用户所不可见的。
如果用户需要传入string,那自己就会通过string str("hello")
去构建一个具名对象传入log()
,此时更好的做法是:禁用隐式转换+写一个log的重载log(const char* s)
。
struct String {
String(const char* s); // 允许 const char* -> String 的隐式转换
};
void log(const String& s);
log("hello"); // OK:编译器悄悄做了一次
// 1. 生成临时 String("hello")
// 2. 把临时对象绑到 const String&
struct String {
explicit String(const char* s); // 禁止隐式转换
};
void log(const String& s);
log("hello"); //编译错误:不能隐式生成临时 String
log(String("hello")); //必须显式构造
10. 使用string_view替代substr()获取子字符串
string_view
取子字符串,没有开辟新内存,也没有memcpy过程。只是记录地址+长度。
string_view 不拥有数据,因此必须保证 原字符串 path 的生命周期 覆盖 ext 的使用期。只要 path 不被销毁或重新分配,ext 就一直安全。
string path("hello");
//老写法,深拷贝
//substr 内部会 new char[len],把 [pos, pos+len) 这段字符逐个复制到新缓冲区。
std::string ext = path.substr(pos);
//新写法,无拷贝开销
//把内部指针往后移 pos 字节;
//把长度改成 len = size - pos。
std::string ext = string_view(path).substr(pos);
or
std::string ext{path.data()+pos(), path.size()-pos()};
11. 使用线程池、内存池等池化工具
12. 其他
循环展开 + 编译器自动矢量化
保持简单线性循环,编译器会帮你 SIMD。
for (size_t i = 0; i < n; ++i) c[i] = a[i] + b[i];
热点路径内联 + 冷路径拆分
减少 icache miss。
[[gnu::hot]] void hot(); [[gnu::cold]] void cold();
编译器优化开关全开
g++ -O3 -march=native -flto -DNDEBUG
PGO(Profile Guided Optimization)
先跑一次典型负载,再重编。
g++ -fprofile-generate ... # 收集
./myapp ... # 运行
g++ -fprofile-use ... # 再编译
Link Time Optimization (LTO)
跨 TU 内联。
g++ -flto a.cpp b.cpp -o app
jemalloc/tcmalloc 替代默认 malloc
减少锁竞争和碎片。
LD_PRELOAD=/usr/lib/libjemalloc.so ./app
避免 false sharing
对齐到 cache line。
struct alignas(64) Counter { std::atomic<size_t> c; };