保持hlist_node内存的紧凑性连续性以提高遍历性能
遍历?
是的,遍历!遍历本来就是饱受诟病的事情,它就是为低效而生的,若要提高性能,采用别的数据结构多好,干嘛要提高遍历的性能?!
我不会没事瞎写文章充数的,一定是发生了什么,当然,这个我放到最后说。
是的,遍历也需要高性能。特别是在某些万不得已的折中环境下,比如hlist本来就是哈希链表了,为了解决必然会遇到的哈希冲突,遍历就是必须的,比如以下的接口都是hlist_head遍历:
hlist_for_each
hlist_for_each_entry
hlist_for_each_entry_safe
hlist_for_each_entry_rcu
...
当然了,平心而论,如果hlist过长,此时首先要考虑的是增加hash桶的数量以及提高hash算法的散列性能,而不是去优化遍历的性能,但是就事论事,在非hash表的情况下,如果单纯考虑遍历本身,我这里确实有些话要说。
首先,先摆出一个事实:
- 在连续的内存上进行遍历,其性能远超在离散的内存上进行遍历!
这是因为CPU在访问内存地址P时,会把一个cacheline的数据预取到cache,在连续的内存上,随着遍历的进行,链表项的访问和预取将形成一个流水化作业,这个流水线只要不被打断,遍历就好像在cache中进行一样。
现在考虑实际情况,我们的链表项能保证地址连续吗?或者退一步说,能保证其地址尽可能连续吗?
我建议先阅读我前面的一篇文章:
https://blog.csdn.net/dog250/article/details/105544111
kmalloc由于使用全局共享的kmem_cache slab(当前实现是slub,所以这里没有写错)数组,因此很难保证内存对象的连续,那么如果我们自己ceate一个kmem_cache呢?事情会变好吗?
还是看我前面的那篇文章,有个结论:
- kmem_cache是栈式管理对象的!free操作相当于一个push,alloc操作相当于一个pop!
我们看看这种栈式管理的性质:
- 栈式管理保证了kmem_cache的slab本身内部碎片的最小化。
- 栈式管理无法保证连续分配到的对象地址的连续性。
- 栈式管理的slab对象分配顺序取决于已用对象的释放的顺序。
我们做一个实验以证之,还是那个alloc.stp,稍微修改一下:
%{#include <linux/module.h>
#include <linux/time.h>struct stub {struct hlist_node hnode;unsigned char m[5];
};#define SIZE 8000static inline void hlist_add_behind_rcu(struct hlist_node *n,struct hlist_node *prev)
{n->next = prev->next;n->pprev = &prev->next;rcu_assign_pointer(hlist_next_rcu(prev), n);if (n->next)n->next->pprev = &n->next;
}static void *(*_global_cache_flush)(void);
%}function kmemcache_test()
%{struct timeval tstart, tend;unsigned long tuses = 0, tuseu = 0;struct hlist_head *hashlist;struct hlist_node *node;struct kmem_cache *memcache;struct stub *p, *ent, *ent_prev;unsigned long rnd;unsigned long addr, addr_pre, addr_next;int i, j, retry = 2, sort = 0;hashlist = kmalloc(sizeof(struct hlist_head), GFP_KERNEL);if (!hashlist) {return;}_global_cache_flush = (void *)kallsyms_lookup_name("global_cache_flush");if (!_global_cache_flush) {return;}INIT_HLIST_HEAD(hashlist);memcache = kmem_cache_create("test_", sizeof(struct stub), 0, 0, NULL);if (!memcache) {return;}realloc:for (i = 0; i < SIZE; i ++) {p = kmem_cache_alloc(memcache, GFP_KERNEL);if (p && sort == 0) {hlist_add_head_rcu(&p->hnode, hashlist);} else if (p && sort == 1) {addr = (unsigned long)p;if (hlist_empty(hashlist)) {hlist_add_head_rcu(&p->hnode, hashlist);} else { // 该else分支处理按序插入hlist_for_each_entry_safe(ent, node, hashlist, hnode) {addr_pre = (unsigned long)ent;if (addr < addr_pre) {hlist_add_head_rcu(&p->hnode, hashlist);break;}if (node == NULL) {hlist_add_behind_rcu(&p->hnode, &ent->hnode);break;} else {struct stub *ns = hlist_entry(node, struct stub, hnode);addr_next = (unsigned long)ns;}if (addr > addr_pre && addr < addr_next) {hlist_add_behind_rcu(&p->hnode, &ent->hnode);break;}}}}}i = j = 0;/* 排除cache的既有影响:* 大概率已经在cacheline了,释放掉以凸显链表节点的地址对遍历的影响) */_global_cache_flush();do_gettimeofday(&tstart);hlist_for_each_entry_rcu(ent, hashlist, hnode) {/* 将以下的打印注释释放你将看到细节 *///STAP_PRINTF("[%d] 0x%p ", i, ent);if (!strcmp(ent->m, "abcd")) // 稍微做点事,显得很忙。j ++;if (i > 0) {unsigned long hi = (unsigned long)ent;unsigned long lo = (unsigned long)ent_prev;signed long delta = hi - lo;if (delta < 0)delta = lo - hi;//STAP_PRINTF("delta [%llx] sort:%d retry:%d\n", delta, sort, retry);} else {//STAP_PRINTF("delta [0] sort:%d retry:%d\n", sort, retry);}ent_prev = ent;i ++;}do_gettimeofday(&tend);tuses = (tend.tv_sec - tstart.tv_sec)*1000000 + tend.tv_usec - tstart.tv_usec;tuseu = tuses/1000000;tuses = tuses - tuseu*1000000;if (retry != 2) {STAP_PRINTF("%ld .. %ld sort:%d retry:%d %d\n", tuseu, tuses, sort, retry, j);}begin:// 随机释放!当然了,可以安排一个精心的释放序列,干点坏事hlist_for_each_entry_safe(ent, node, hashlist, hnode) {get_random_bytes(&rnd, sizeof(unsigned long));if (rnd % 2 == 0) {hlist_del(&ent->hnode);kmem_cache_free(memcache, ent);}}if (!hlist_empty(hashlist)) {goto begin;}/* 控制流程:* 第1次:刚刚初始化的kmem_cache slab分配SIZE个对象并遍历(内存地址是顺序的)* 第2次:随机顺序将对象全部释放后再重新分配SIZE个对象并遍历(内存地址是散乱的)* 第3次:随机顺序将对象再次全部释放后重新分配SIZE个对象按照内存地址顺序插入hlist并遍历*/if (retry > 0) {if (retry == 1)sort = 1;retry --;goto realloc;}kmem_cache_destroy(memcache);
%}probe begin
{kmemcache_test();exit(); // oneshot模式
}
以下是几次的运行结果:
0 .. 218 sort:0 retry:1 i:00 .. 213 sort:1 retry:0 i:00 .. 222 sort:0 retry:1 i:00 .. 208 sort:1 retry:0 i:00 .. 219 sort:0 retry:1 i:00 .. 211 sort:1 retry:0 i:00 .. 227 sort:0 retry:1 i:00 .. 209 sort:1 retry:0 i:00 .. 237 sort:0 retry:1 i:00 .. 207 sort:1 retry:0 i:00 .. 240 sort:0 retry:1 i:00 .. 206 sort:1 retry:0 i:0
足以见得按照内存排序对遍历性能的影响。
你可能想知道栈式管理的slab在随机释放对象后再分配时发生了什么,我画一幅图就明白了,为简单起见,我用顺序的自然数编号内存地址:
我们可以得出一些trick式的结论,在确定hash桶无法增加的情况下,为了让hlist桶冲突链表的遍历性能更高,我们需要:
- 尽量保持hlist_node的内存地址的紧凑且单调递增。
- 条件允许的情况下,每个桶一个kmem_cache,单独维护一个slab池子。
想做到保持内存的紧凑,内核已经提供了API:
void hlist_add_after_rcu(struct hlist_node *prev, struct hlist_node *n);
// 低版本内核没有add behind接口,需要自己copy
void hlist_add_behind_rcu(struct hlist_node *n, struct hlist_node *prev);
插入的代价仅仅是执行一次冒泡,如果插入操作是非频操作,建议这么做!
说说我为什么写这篇文章。
昨天发现了使用kmalloc导致的性能问题后,我换成了自行维护的kmem_cache来管理对象的内存,性能相比之前减少了20%的pps损耗,好事啊!
然而,经过频繁的alloc,free操作后,我发现了偶然的性能抖动,难道我自己维护的slab还有什么幺蛾子问题吗?
通过crash工具的kmem -S以及slabinfo,slabtop,我发现我自己的slab也变得零散了…在解决这个问题之前,我首先想到了一个 坏主意:
- 我什么都不用做,只要在一个slab里不断的分配内存,然后按照精心布局的顺序释放掉它们,就能把这个slab打的全是空洞…
然后就又是左右手互搏了。如果我自己碰到了我这样的坏人,我该怎么应对!
旁边的同事建议我自己维护一个预分配好的大池子,自己管理自己的内存,不要使用栈式的slab。当然,我也是蠢蠢欲动地想实现一个 基于位图的连续内存分配算法 :
- 学习buddy系统(管理全局内存)的样子做一个大小固定的buddy系统。
大小不固定和大小固定的内存对象管理算法,说到底就是一个是解决内碎片的问题,一个是解决外碎片的问题,类似于段式(大小不固定)和页式(大小固定)的内存管理方法。
可是我不会编程啊!
说到底还是因为懒,最后发现方法虽然不是很精确,但是足够简单!大多数情况够用了,就是我上面说的 按照node的内存地址重排 的方案。结果测试下来,pps性能照之前的优化又提高了将近一倍!损耗由%4进一步降低到了2%左右!
皮鞋啊皮鞋,买皮鞋
保持hlist_node内存的紧凑性连续性以提高遍历性能相关推荐
- 让机器“提纲挈领”:视觉系统的紧凑性初探|VALSE2018之七
编者按:王勃在<滕王阁序>中创作出了千古名句: "落霞与孤鹜齐飞,秋水共长天一色." 短短十四个字,极具层次性地提炼出了视觉画面中的色彩之美.动态之美.虚实之美.以及立 ...
- 【操作系统】内存管理设计性实验报告
操作系统#内存管理设计性实验报告 正文 一. 实验目的 1.通过本次试验体会操作系统中内存的分配模式: 2.掌握内存分配的方法(首次适应(FF),最佳适应(BF),最差适应(WF)): 3.学会进程的 ...
- 提高C++性能的编程技术笔记:多线程内存池+测试代码
为了使多个线程并发地分配和释放内存,必须在分配器方法中添加互斥锁. 全局内存管理器(通过new()和delete()实现)是通用的,因此它的开销也非常大. 因为单线程内存管理器要比多线程内存管理器快的 ...
- 提高C++性能的编程技术笔记:单线程内存池+测试代码
频繁地分配和回收内存会严重地降低程序的性能.性能降低的原因在于默认的内存管理是通用的.应用程序可能会以某种特定的方式使用内存,并且为不需要的功能付出性能上的代价.通过开发专用的内存管理器可以解决这个问 ...
- Multi-thread提高C++性能的编程技术笔记:单线程内存池+测试代码
频繁地分配和回收内存会严重地降低程序的性能.性能降低的原因在于默认的内存管理是通用的.应用程序可能会以某种特定的方式使用内存,并且为不需要的功能付出性能上的代价.通过开发专用的内存管理器可以解决这个问 ...
- LSM树——放弃读能力换取写能力,将多次修改放在内存中形成有序树再统一写入磁盘,查找复杂度O(k*log(n)),结合bloom filter提高查找性能...
来自:http://www.open-open.com/lib/view/open1424916275249.html 十年前,谷歌发表了 "BigTable" 的论文,论文中很多 ...
- 提高代码性能及并发性的方法浅谈
最近在做系统调优,总结了下cache相关知识,以及如何提高性能和并发性能的方法. 一CACHE相关 1. cache概述 cache,中译名高速缓冲存储器,其作用是为了更好的利用局部性原理,减少CPU ...
- c# 定位内存快速增长_c#如何避免内存分配瓶颈以提高多线程性能
我使用C#作为研究工具,经常需要运行CPU密集型任务,例如优化.从理论上讲,我应该能够通过多线程化代码来提高性能,但实际上当我使用与工作站上可用内核数量相同的线程数时,我通常会发现CPU仍然只运行在2 ...
- Mozilla Firefox 66 将使用更少的内存,提高扩展性能
开发四年只会写业务代码,分布式高并发都不会还做程序员? 即将发布的 Firefox 66 将使用 indexedDB 作为数据存储方式,放弃使用传统的 JSON 文件. 扩展的数据将会自动从 JS ...
- 内存大计算机运行就快吗,提高电脑内存的运行速度的方法你会吗
有时候你会发现,自己的电脑运行的速度非常的慢也非常的卡,但是你也是找不到任何原因来解释为什么会出现这种情况,那么该怎么样才可以解决这些问题?一般来说,电脑运行是跟内存有着莫大的关系,接下来将为大家简单 ...
最新文章
- 高精地图与自动驾驶(上)
- 自动禁止ssh的root登陆
- 第八章-数据类、结构
- MindCon极客周 · 点亮城市接力活动正式启动!来为你的城市打Call,还有多重好礼相送!...
- OpenStack Skyline 现代化的管理界面
- 前端入门技巧之浏览器调试
- 重新组织函数--《重构》阅读笔记
- [转载] Java中的命名参数
- 统计xml文件中的标签出现框数及出现过的图片数
- linux mq脚本,Linux自动化命令工具expect
- android真实项目教程(三)——首页初点缀_by_CJJ
- 团队第一阶段站立会议05
- 计算机考研 东华大学,东华大学(专业学位)计算机技术考研难吗
- 【每日算法Day 94】经典面试题:机器人的运动范围
- 暨南大学锐捷校园网路由器教程
- jdk和cglib动态代理
- 蓝桥杯学习——递归问题(上楼梯)
- Android--高效地加载大图片
- android 拔插键盘自动切换输入法
- 写个脚本薅区块鱼羊毛
热门文章
- Maven安装和使用(详细版)
- 使用SDK Manager给TX2刷机且安装OpenCV3.4.0、CUDNN7.6.5、Pytorch、Miniforge(含百度云安装包)
- 『解疑』script标签 中 deffer和async属性的区别?
- 彩虹网盘外链程序源码V5.1|网盘外链源码
- labelme打开不了jpg格式和其他一些格式的图片
- 计算机的用户账户,计算机用户名是什么意思(如何修改和设置用户名)
- feedsky-对他扫兴至极
- 大数据这么火,具体用用到哪些领域?揭秘大数据十三大具体应用场景
- 开源无国界!CSDN 董事长蒋涛、GitHub 副总裁 Thomas Dohmke 对话实录
- MTK和Android有区别,Android系统 下一个山寨MTK的代名词