当你申请的size较大,有效载荷 / 耗费空间的比例会比较高,内碎片占比不高,但但size较小,这个占比就高,如果这种小size的chunk非常多,就会造成内存的极大浪费。
《C++设计新思维》一书中的loki库实现了一个小对象分配器,通过隐式链表的方式解决了这个问题,有兴趣的可以去看看。
(b)cached obj
《C++ Primer》实现了一个CachedObj类模板,任何需要拥有这种cached能力的类型都可以通过从CachedObj<T>派生而获得。
它的主要思想是为该种类型维护一个FreeList,每个节点就是一个Object,对象申请的时候,检查FreeList,如果FreeList不为空,则摘除头结点返回,如果FreeList为空,则new一批Object并串到FreeList,然后走FreeList不为空的分配流程,通过重载类的operator new和operator delete,达到对类的使用者透明的目的。
(c)内存分配和对象构建分离
c的malloc用来动态分配内存,free用来归还内存;C++的new做了3件事,通过operator new(本质上等同malloc)分配内存,在分配的内存上构建对象,返回对象指针;而delete干了两件事,调用析构函数,归还内存。
C++通过placement new可以分离内存分配和对象构建,结合显示的析构函数调用,达到自控的目的。
我优化过一个游戏项目,启动时间过长,记忆中需要几十秒(至少十几秒),分析后发现主要是因为游戏执行预分配策略(对象池),在启动的时候按最大容量创建怪和玩家,对象构建很重,大量对象构建耗时过长,通过分离内存分配和对象构建,把对象构建推迟到真正需要的时候,实现了服务的重启秒起。
(d)内存复用
编解码、加解密、序列化反序列化(marshal/unmarshal)的时候一般都需要动态申请内存,这种调用频次很高,可以考虑用静态内存,为了避免多线程竞争,可以用thread local。
当然你也可以改进静态内存策略,比如封装一个GetEncodeMemeory(size_t)函数,维护一个void* + size_t结构体对象(初始化为NULL+0),对比参数size跟对象的size成员,如果参数size<=对象size,直接返回对象大的void*指针,否则free掉void*指针,再按参数size分配一个更大的void*,并用参数size更新对象size。
cache优化i-cache优化:i-cache的优化可以通过精简code path,简化调用关系,减少代码量,减少复杂度来实现。
具体措施包括,减少函数调用(就地展开、inline),利用分支预测,减少函数指针,可以考虑把code path上相关的函数定义在一起,把相关的函数定义到一个源文件,并让它们在源文件上临近,这样生成的object文件,在运行时加载后相关函数大概率也内存临近,当然编译器也一直在做这方面的努力,但我们写代码不应该依赖编译器优化,尽量去帮助编译器生成更高效的代码。
d-cache优化:d-cache优化包括改进数据结构和算法获取更好的数据访问时空局部性,比如二分查找就是d-cache友好算法。一个cache line一般是64B,如果数据跨越两个cache-line,则会导致load & store2次,所以,需要结合cache对齐,尽量让相关访问的数据在一个cache-line。
如果结构体过大,则各成员不仅可能在不同cache-line,甚至可能在不同page,所以应该避免结构体过大。
如果结构体的成员变量过多,一般而言对各成员的访问频次也会满足2-8定律,可以考虑把hot和cold的成员分开,重排结构体成员变量顺序,但这些骚操作我不建议在开始的时候用,因为说不定哪天又要增删成员,从而破坏苦心孤诣搭建的积木。
判断前置判断前置指在函数中讲判断返回的语句前置,这样不至于忙活半天,你跟我说对不起不合适,玩儿呢?
在写多个判断的时候,把不满足可能性高的放在前面。
在写条件或的时候把为true的放在前面,在写条件与的时候把为false的放在前面。
另外,如果在循环里调用一个函数,而这个函数里检查某条件,不符合就返回,这种情况,可以考虑把检查放到调用函数的外面,这样不满足的话就不用陷入函数,当然,你也可以说,这样的操作违背软件工程,但看你想要什么,你不总是能够两全其美,对吧?
凑零为整与化整为零凑零为整其实的思想在日志批处理里提了,不再展开。
化整为零体现了分而治之的思想,可以把一个大的操作,分摊开来,避免在做大操作的时候导致卡顿,从而让CPU占比更加平稳。
分频