Linux内核为需要动态分配内存的内核程序提供了kmalloc/kfree/kcalloc/krealloc函数接口,它们分别对应于C标准库
的malloc/free/calloc/krealloc。除此之外,Linux还提供了kmem_cache_xxx系列系统调用,以提供比上述接口更低的时间复杂度和空间复杂度,那么两者的效率究竟能差多少,它们又各自适合于何种场合呢?
出于效率上的考虑,上述函数为被声明为内联编译,81行的__builtin_constant_p是gcc的内建函数[ 3 ],它能够在编译时判定它的参数是否是编译时常量,如果是它将返回真,因为此函数为内联函数,并且__builtin_constant_p的参数为整型变量,所以判定仍有效。这也就意味着,如果你用如下方式调用kmalloc:
实际执行的将是82-79行的代码。为了能将这部分代码展开,我们不得不参考文件kmalloc_sizes.h的内容:
代码展开后将是一系列的if-else语句块,顺序查找到最小的满足kmalloc大小要求的malloc_sizes的数组的索引。看到这里你也许会失望,顺序查找的时间复杂度不是O(n)么,既然malloc_sizes是数组,为啥不用更加高效的二分搜索呢?这个疑问先保留,不要忘了以上代码是size为常量的时候才会被执行的,其中肯定存在某些玄机。如果size为常量,那么每个if语句的值在编译时就应该能被算出,汇编的结果也证实了我的猜想,不仅仅是i的值在编译时就被算了出来,就连cs_cachep和cs_dmacachep的偏移量也是编译时计算出来的,编译器的智能程度远远地超出了我的预期。
需要指明的一点是,以上行为只有在打开编译器优化的时候才会起效,否则连inline函数它都不会展开。说起来,Linux内核和gcc编译器还真的是“狼狈为奸”好多年,连这点儿优化诡计都被利用上了。
从以上分析来看如果是申请固定大小的内存空间,kmalloc和kmem_cache_alloc时间效率相当。如果申请动态大小的空间,编译时优化是指望不上了,但是因为kmem_cache_alloc也只能申请固定大小的对象空间,所以也只能把kmalloc赶上架了。
哦,因为size不是常量,所以kmalloc将不会走以上分支,这时它调用__kmalloc:
看到顺序查找的代码是不是感觉挺差劲的?猜想它也是基于大多数内存块都不是很大这个事实,所以顺序查找的事实时间复杂度反而会比二分查找低。但是,这多少有些让人不爽,SLUB的代码针对这部分又做了优化,基于大多数内存块是按2^N大小连续分部这个事实,用查表加上位级别的大小对齐处理提高了时间效率。由于这部分与本文所讨论的东西关联不大,所以留给看客自己去分析吧!
Linux内存系统的层次结构
为了保证灵活性,Linux内存分配系统是分层设计的,这种层次设计使得整个设计更加明晰,每个层次都能简单而有效地进行局部优化,所以一般也能提供更好的性能,当然扩展性也更好了,不然 SLUB[1] 的出现就不可能那么轻松自然了。Linux内存系统自上而下分成以下几部分:单位 | 接口 | 算法 |
动态大小 | kmalloc/kfree/krealloc/kcalloc | 按大小组织的缓存数组 |
固定大小 | kmem_cache_create/kmem_cache_destroy kmem_cache_alloc/kmem_cache_free | Slab[2] |
2^n页 | alloc_pages/free_pages __get_free_pages/__free_pages | 伙伴算法 |
kmalloc相对于kmem_cache_alloc的时间复杂度
因为kmalloc是基于kmem_cache_create实现的,那么时间效率上kmalloc肯定是占不到任何便宜了,那么究竟能差多少呢?让我们先来看看相关代码: |
出于效率上的考虑,上述函数为被声明为内联编译,81行的__builtin_constant_p是gcc的内建函数[ 3 ],它能够在编译时判定它的参数是否是编译时常量,如果是它将返回真,因为此函数为内联函数,并且__builtin_constant_p的参数为整型变量,所以判定仍有效。这也就意味着,如果你用如下方式调用kmalloc:
ptr = kmalloc(128, GTP_KERNEL); |
实际执行的将是82-79行的代码。为了能将这部分代码展开,我们不得不参考文件kmalloc_sizes.h的内容:
|
代码展开后将是一系列的if-else语句块,顺序查找到最小的满足kmalloc大小要求的malloc_sizes的数组的索引。看到这里你也许会失望,顺序查找的时间复杂度不是O(n)么,既然malloc_sizes是数组,为啥不用更加高效的二分搜索呢?这个疑问先保留,不要忘了以上代码是size为常量的时候才会被执行的,其中肯定存在某些玄机。如果size为常量,那么每个if语句的值在编译时就应该能被算出,汇编的结果也证实了我的猜想,不仅仅是i的值在编译时就被算了出来,就连cs_cachep和cs_dmacachep的偏移量也是编译时计算出来的,编译器的智能程度远远地超出了我的预期。
|
需要指明的一点是,以上行为只有在打开编译器优化的时候才会起效,否则连inline函数它都不会展开。说起来,Linux内核和gcc编译器还真的是“狼狈为奸”好多年,连这点儿优化诡计都被利用上了。
从以上分析来看如果是申请固定大小的内存空间,kmalloc和kmem_cache_alloc时间效率相当。如果申请动态大小的空间,编译时优化是指望不上了,但是因为kmem_cache_alloc也只能申请固定大小的对象空间,所以也只能把kmalloc赶上架了。
哦,因为size不是常量,所以kmalloc将不会走以上分支,这时它调用__kmalloc:
void *__kmalloc(size_t size, gfp_t flags) { } |
static inline kmem_cache_t *__find_general_cachep(size_t size, gfp_t gfpflags) { #if DEBUG #endif } |
看到顺序查找的代码是不是感觉挺差劲的?猜想它也是基于大多数内存块都不是很大这个事实,所以顺序查找的事实时间复杂度反而会比二分查找低。但是,这多少有些让人不爽,SLUB的代码针对这部分又做了优化,基于大多数内存块是按2^N大小连续分部这个事实,用查表加上位级别的大小对齐处理提高了时间效率。由于这部分与本文所讨论的东西关联不大,所以留给看客自己去分析吧!