【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::stringstreamstd::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; };
posted @ 2025-08-17 12:22  仰望星河Leon  阅读(9)  评论(0)    收藏  举报