遍历?

是的,遍历!遍历本来就是饱受诟病的事情,它就是为低效而生的,若要提高性能,采用别的数据结构多好,干嘛要提高遍历的性能?!

我不会没事瞎写文章充数的,一定是发生了什么,当然,这个我放到最后说。

是的,遍历也需要高性能。特别是在某些万不得已的折中环境下,比如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内存的紧凑性连续性以提高遍历性能相关推荐

  1. 让机器“提纲挈领”:视觉系统的紧凑性初探|VALSE2018之七

    编者按:王勃在<滕王阁序>中创作出了千古名句: "落霞与孤鹜齐飞,秋水共长天一色." 短短十四个字,极具层次性地提炼出了视觉画面中的色彩之美.动态之美.虚实之美.以及立 ...

  2. 【操作系统】内存管理设计性实验报告

    操作系统#内存管理设计性实验报告 正文 一. 实验目的 1.通过本次试验体会操作系统中内存的分配模式: 2.掌握内存分配的方法(首次适应(FF),最佳适应(BF),最差适应(WF)): 3.学会进程的 ...

  3. 提高C++性能的编程技术笔记:多线程内存池+测试代码

    为了使多个线程并发地分配和释放内存,必须在分配器方法中添加互斥锁. 全局内存管理器(通过new()和delete()实现)是通用的,因此它的开销也非常大. 因为单线程内存管理器要比多线程内存管理器快的 ...

  4. 提高C++性能的编程技术笔记:单线程内存池+测试代码

    频繁地分配和回收内存会严重地降低程序的性能.性能降低的原因在于默认的内存管理是通用的.应用程序可能会以某种特定的方式使用内存,并且为不需要的功能付出性能上的代价.通过开发专用的内存管理器可以解决这个问 ...

  5. Multi-thread提高C++性能的编程技术笔记:单线程内存池+测试代码

    频繁地分配和回收内存会严重地降低程序的性能.性能降低的原因在于默认的内存管理是通用的.应用程序可能会以某种特定的方式使用内存,并且为不需要的功能付出性能上的代价.通过开发专用的内存管理器可以解决这个问 ...

  6. LSM树——放弃读能力换取写能力,将多次修改放在内存中形成有序树再统一写入磁盘,查找复杂度O(k*log(n)),结合bloom filter提高查找性能...

    来自:http://www.open-open.com/lib/view/open1424916275249.html 十年前,谷歌发表了 "BigTable" 的论文,论文中很多 ...

  7. 提高代码性能及并发性的方法浅谈

    最近在做系统调优,总结了下cache相关知识,以及如何提高性能和并发性能的方法. 一CACHE相关 1. cache概述 cache,中译名高速缓冲存储器,其作用是为了更好的利用局部性原理,减少CPU ...

  8. c# 定位内存快速增长_c#如何避免内存分配瓶颈以提高多线程性能

    我使用C#作为研究工具,经常需要运行CPU密集型任务,例如优化.从理论上讲,我应该能够通过多线程化代码来提高性能,但实际上当我使用与工作站上可用内核数量相同的线程数时,我通常会发现CPU仍然只运行在2 ...

  9. Mozilla Firefox 66 将使用更少的内存,提高扩展性能

    开发四年只会写业务代码,分布式高并发都不会还做程序员?   即将发布的 Firefox 66 将使用 indexedDB 作为数据存储方式,放弃使用传统的 JSON 文件. 扩展的数据将会自动从 JS ...

  10. 内存大计算机运行就快吗,提高电脑内存的运行速度的方法你会吗

    有时候你会发现,自己的电脑运行的速度非常的慢也非常的卡,但是你也是找不到任何原因来解释为什么会出现这种情况,那么该怎么样才可以解决这些问题?一般来说,电脑运行是跟内存有着莫大的关系,接下来将为大家简单 ...

最新文章

  1. 高精地图与自动驾驶(上)
  2. 自动禁止ssh的root登陆
  3. 第八章-数据类、结构
  4. MindCon极客周 · 点亮城市接力活动正式启动!来为你的城市打Call,还有多重好礼相送!...
  5. OpenStack Skyline 现代化的管理界面
  6. 前端入门技巧之浏览器调试
  7. 重新组织函数--《重构》阅读笔记
  8. [转载] Java中的命名参数
  9. 统计xml文件中的标签出现框数及出现过的图片数
  10. linux mq脚本,Linux自动化命令工具expect
  11. android真实项目教程(三)——首页初点缀_by_CJJ
  12. 团队第一阶段站立会议05
  13. 计算机考研 东华大学,东华大学(专业学位)计算机技术考研难吗
  14. 【每日算法Day 94】经典面试题:机器人的运动范围
  15. 暨南大学锐捷校园网路由器教程
  16. jdk和cglib动态代理
  17. 蓝桥杯学习——递归问题(上楼梯)
  18. Android--高效地加载大图片
  19. android 拔插键盘自动切换输入法
  20. 写个脚本薅区块鱼羊毛

热门文章

  1. Maven安装和使用(详细版)
  2. 使用SDK Manager给TX2刷机且安装OpenCV3.4.0、CUDNN7.6.5、Pytorch、Miniforge(含百度云安装包)
  3. 『解疑』script标签 中 deffer和async属性的区别?
  4. 彩虹网盘外链程序源码V5.1|网盘外链源码
  5. labelme打开不了jpg格式和其他一些格式的图片
  6. 计算机的用户账户,计算机用户名是什么意思(如何修改和设置用户名)
  7. feedsky-对他扫兴至极
  8. 大数据这么火,具体用用到哪些领域?揭秘大数据十三大具体应用场景
  9. 开源无国界!CSDN 董事长蒋涛、GitHub 副总裁 Thomas Dohmke 对话实录
  10. MTK和Android有区别,Android系统 下一个山寨MTK的代名词