找回密码
 立即注册
查看: 204|回复: 0

C++ 高性能编程实战(三):内存优化

[复制链接]
发表于 2024-7-15 18:27 | 显示全部楼层 |阅读模式
转载自:
1、tcmalloc 和 jemalloc

线程池技术中,每个线程各司其职,完成一个一个的任务。在 malloc 看来,就是多个长生命周期的线程,随机的在各个时间节点进行内存申请和内存释放。基于这样的场景,首先,尽量分配持续地址空间。其次,多线程下需要考虑分区隔离和减少竞争。
tcmalloc 和 jemalloc 共同的思路是引入线程缓存机制。通过一次从后端获取大块内存,放入缓存供线程多次申请,降低对后端的实际竞争强度。主要分歧点是,当线程缓存被击穿后,tcmalloc 采用了单一的 page heap(简化了中间的 transfer cache 和 central cache) 来承载;而 jemalloc 采用了多个 arena(甚至超过了处事器 core 数)。一般来讲,在线程数较少,或释放强度较低的情况下,较为简洁的 tcmalloc 性能稍胜 jemalloc。在 core 数较多、申请释放频繁时,jemalloc 因为锁竞争强度远小于 tcmalloc,性能较好

抱负的 malloc 模型是什么?

  • 低竞争性和持续性
微处事、流式计算、缓存,这几种业务模型几乎涵盖了所有主流的后端处事场景。而这几种业务对内存的应用有一个重要的特征:拥有边界明确的生命周期。比如在早期的 server 设计中,每个 client 请求都分配一个单独的线程措置,措置完再整体销毁。但随着新型的子任务级线程池并发技术的广泛应用,即请求细分为多个子任务充实操作多核并发来提升计算性能。
std::vector<std::string> 如何优化?这里提供一种思路:

  • 和典型的 vector 措置主要分歧点是:在 clear 或者 pop_back 等操作缩减大小之后,内容对象并不实际析构,只是清空重置。因此,再一次用到这个槽位的时候,可以直接拿到已经构造好的元素,而且其 capacity 之内的内存依然持有。当反复使用同一个实例时,容器内存和每个元素自身的 capacity 城市趋于饱和值,反复的分配和构造需求都被减少了。
内存分配和实例构造功能解耦。这也是 PMR(Polymorphic Memory Resource,C++17 的新特性)设计的出发点,大名鼎鼎的 EASTL 就是它的原型,它就是为低延迟、高频、计算密集型任务开发的。

2、string

短字符串分配
  1. #include <chrono>
  2. #include <iostream>
  3. struct Timer {
  4.     std::chrono::high_resolution_clock::time_point start, end;
  5.     std::chrono::duration<float> duration;
  6.     Timer() { start = std::chrono::high_resolution_clock::now(); }
  7.     ~Timer() {
  8.         end = std::chrono::high_resolution_clock::now();
  9.         duration = end - start;
  10.         float ns = duration.count() * 1000000.0f;
  11.         std::cout << ”Timer took ” << ns << ”ns”
  12.                   << ”\n”;
  13.     }
  14. };
  15. const int SIZE = 1000000;
  16. void test_stack() {
  17.     Timer timer;
  18.     for (int i = 0; i < SIZE; i++) {
  19.         char buf[12];
  20.     }
  21. }
  22. void test_string() {
  23.     Timer timer;
  24.     for (int i = 0; i < SIZE; i++) {
  25.         std::string str(”hello world”);
  26.     }
  27. }
  28. int main() {
  29.     test_stack();
  30.     test_string();
  31.     return 0;
  32. }
复制代码
测试成果:



短字符串构造,char 和 string 性能差不多

长字符串分配
  1. const int SIZE = 1000000;
  2. void test_stack() {
  3.     Timer timer;
  4.     for (int i = 0; i < SIZE; i++) {
  5.         char buf[32];
  6.     }
  7. }
  8. void test_string() {
  9.     Timer timer;
  10.     for (int i = 0; i < SIZE; i++) {
  11.         std::string str(”hello world, it is test string.”);
  12.     }
  13. }
  14. int main() {
  15.     test_stack();
  16.     test_string();
  17.     return 0;
  18. }
复制代码
测试成果:



长字符串构造,string 性能比 char 差很多

string 在 libstadc++ 和 libc++ 的实现方式是纷歧样的,具体参考下面这篇文章:

std::pmr::string
  1. #include <memory_resource>
  2. const int SIZE = 1000000;
  3. void test_stack() {
  4.     Timer timer;
  5.     for (int i = 0; i < SIZE; i++) {
  6.         std::string str(”hello world, it is test string.”);
  7.     }
  8. }
  9. void test_string() {
  10.     Timer timer;
  11.     for (int i = 0; i < SIZE; i++) {
  12.         std::pmr::string str(”hello world, it is test string.”);
  13.     }
  14. }
复制代码
测试成果:


std::pmr::string允许我们在栈上创建string,当超过 1024 个字节后才会在堆上申请内存。
3、vector

stl 中 vector 的内存增长速度是 2 的幂次方,而这个值是可以调整的,比如:folly 的 small vector

4、map

STL 中的 map 是基于红黑树来实现的,而高效的 map 必然是 hash map,进一步优化的思路就是在 hash map 的基础上引入内存池技术。

5、protobuf

比如采纳某些字段合并策略,尽量减少序列化、反序列化的次数。
6、高效使用智能指针


  • 使用 std::make_shared 代替 new T
  1. class MyClass {
  2. public:
  3.     MyClass(std::string s, int i) : s(s), i(i) {}  // 使用初始化列表斗劲快
  4. public:
  5.     std::string s;
  6.     int i;
  7. };
  8. const int SIZE = 1000000;
  9. void test1() {
  10.     Timer timer;
  11.     for (int i = 0; i < SIZE; i++) {
  12.         std::shared_ptr<MyClass> p(new MyClass(”hello”, 123));  // 会调用两次内存打点器,第一次用于创建 MyClass 的实例,第二次用来创建 std::shared_ptr 的内部布局。
  13.     }
  14. }
  15. void test2() {
  16.     Timer timer;
  17.     for (int i = 0; i < SIZE; i++) {
  18.         std::shared_ptr<MyClass> p = std::make_shared<MyClass>(”hello”, 123);  // 一次性分配内存同时保留以上两种数据布局
  19.     }
  20. }
  21. int main() {
  22.     test1();
  23.     test2();
  24.     return 0;
  25. }
复制代码
测试成果:



  • 避免使用 std::shared_ptr 作为函数的入参,而是通过 get() 函数传递实际的指针
  • 通过 = delete 修饰,在类定义中禁止不但愿发生的复制
本系列其他文章:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-21 21:26 , Processed in 0.101882 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表