C 优化 - 低级代码
C optimization - low level code
我正在尝试编写一个与 dlmalloc 相当的内存分配器,后者是 glibc 中使用的 malloc。 dlmalloc 是具有块拆分功能的最佳分配器,在将块再次合并为大块之前,它会保留最近使用的块池。我正在编写的分配器首先适合样式。
我的问题是双重的:(1)与glibc malloc相比,我的代码的测试时间非常不规则,(2)有些日子,我的代码的平均运行时间会长3到4倍;(2)没什么大不了的,但我想了解为什么glibc malloc不会以同样的方式遭受痛苦。在这篇文章中,进一步展示了 malloc 和我的代码之间 (1) 中描述的行为示例。有时,一批 1000 次测试的平均时间远高于上面的 malloc( 问题 (2) 的时间,有时平均值是相同的。但是,对我的代码进行一批测试的测试时间总是非常不规则的(上面的问题 (1));这意味着在一批测试中,时间跳跃到平均值的 20 倍,并且这些跳跃穿插在其他规则(接近平均)的时间中。glibc malloc 不这样做。
我正在处理的代码如下。
====
==================================/* represent an allocated/unallocated block of memory */
struct Block {
/* previous allocated or unallocated block needed for consolidation but not used in allocation */
Block* prev;
/* 1 if allocated and 0 if not */
unsigned int tagh;
/* previous unallocated block */
Block* prev_free;
/* next unallocated block */
Block* next_free;
/* size of current block */
unsigned int size;
};
#define CACHE_SZ 120000000
/* array to be managed by allocator */
char arr[CACHE_SZ] __attribute__((aligned(4)));
/* initialize the contiguous memory located at arr for allocator */
void init_cache(){
/* setup list head node that does not change */
Block* a = (Block*) arr;
a->prev = 0;
a->tagh = 1;
a->prev_free = 0;
a->size = 0;
/* setup the usable data block */
Block* b = (Block*) (arr + sizeof(Block));
b->prev = a;
b->tagh = 0;
b->prev_free = a;
b->size = CACHE_SZ - 3*sizeof(Block);
a->next_free = b;
/* setup list tail node that does not change */
Block* e = (Block*)((char*)arr + CACHE_SZ - sizeof(Block));
e->prev = b;
e->tagh = 1;
e->prev_free = b;
e->next_free = 0;
e->size = 0;
b->next_free = e;
}
char* alloc(unsigned int size){
register Block* current = ((Block*) arr)->next_free;
register Block* new_block;
/* search for a first-fit block */
while(current != 0){
if( current->size >= size + sizeof(Block)) goto good;
current = current->next_free;
}
/* what to do if no decent size block found */
if( current == 0) {
return 0;
}
/* good block found */
good:
/* if block size is exact return it */
if( current->size == size){
if(current->next_free != 0) current->next_free->prev_free = current->prev_free;
if(current->prev_free != 0) current->prev_free->next_free = current->next_free;
return (char* ) current + sizeof(Block);
}
/* otherwise split the block */
current->size -= size + sizeof(Block);
new_block = (Block*)( (char*)current + sizeof(Block) + current->size);
new_block->size = size;
new_block->prev = current;
new_block->tagh = 1;
((Block*)((char*) new_block + sizeof(Block) + new_block->size ))->prev = new_block;
return (char* ) new_block + sizeof(Block);
}
main(int argc, char** argv){
init_cache();
int count = 0;
/* the count considers the size of the cache arr */
while(count < 4883){
/* the following line tests malloc; the quantity(1024*24) ensures word alignment */
//char * volatile p = (char *) malloc(1024*24);
/* the following line tests above code in exactly the same way */
char * volatile p = alloc(1024*24);
count++;
}
}
====
===================================我简单地编译上面的代码:
g++ -O9 分配.c
并运行一个简单的测试,该测试始终用于拆分块并且永远不会返回确切大小的块:
bash$ for((i=0; i<1000; i++)); do (time ./a.out) 2>&1|grep real; done
我的代码和glibc malloc的测试示例输出如下:
我的代码 :
real 0m0.023s
real 0m0.109s <----- irregular jump >
real 0m0.024s
real 0m0.086s
real 0m0.022s
real 0m0.104s <----- again irregular jump >
real 0m0.023s
real 0m0.023s
real 0m0.098s
real 0m0.023s
real 0m0.097s
real 0m0.024s
real 0m0.091s
real 0m0.023s
real 0m0.025s
real 0m0.088s
real 0m0.023s
real 0m0.086s
real 0m0.024s
real 0m0.024s
Malloc代码(尼斯和定期住宿接近20ms):
real 0m0.025s
real 0m0.024s
real 0m0.024s
real 0m0.026s
real 0m0.024s
real 0m0.026s
real 0m0.025s
real 0m0.026s
real 0m0.026s
real 0m0.025s
real 0m0.025s
real 0m0.024s
real 0m0.024s
real 0m0.024s
real 0m0.025s
real 0m0.026s
real 0m0.025s
请注意,malloc 代码时间更规律。在其他不可预测的时间,我的代码有 0m0.070s 而不是 0m0.020s,因此平均运行时间接近 70ms 而不是 25ms(上面的问题 (2)),但这不在此处显示。在这种情况下,我很幸运地让它运行在接近 malloc 的平均值(25 毫秒)
附近问题是,(1)我如何修改我的代码以获得更规律的时间,例如glibc malloc?(2)如果可能的话,我怎样才能让它比glibc malloc更快,因为我读过dlmalloc是一个特征平衡的分配器,不是最快的(只考虑拆分/最佳拟合/首次拟合分配器而不是其他分配器)?
不要使用"实时"时间:尝试"用户"+"系统"。对大量迭代进行求平均值。问题是双重的:(a)您的进程在处理器上并不孤单,它会根据其他进程的作用而中断,(b)时间与时间的测量具有粒度。我不确定今天是什么,但在过去,它只是一个时间片的大小 => 1/100 秒。
是的,我已经比较了这两种解决方案,并以几种不同的变体运行它们。我不确定问题是什么,但我的猜测是,大部分时间都花在"创建 12000000000 字节的大型连续板上"。如果我减小了大小,并且仍然执行相同数量的分配,时间就会减少。
另一个证据指出这一点,system
时间是real
时间的很大一部分,user
时间几乎为零。
现在,在我的系统上,一旦我以高内存负载运行这些东西几次,它就不会上下摆动那么多。这很可能是因为一旦我换掉了内存中积累的一堆旧垃圾,系统就有足够的"备用"页面用于我的进程。当内存受到更多限制时(因为我让系统去做一些其他事情,比如在我实验的"网站"上做一些数据库工作[它是一个真实网站的"沙盒"版本,所以它在数据库中有真实的数据,可以快速填充内存等等],我得到更多的变化,直到我再次清除内存。
但我认为"神秘"的关键在于系统时间是所使用的大部分时间。同样值得注意的是,当使用带有大块的malloc
时,内存实际上并没有"真正分配"。在分配较小的块时,似乎malloc
实际上在某些方面更聪明,并且比"优化"分配更快 - 至少对于较大的内存量。不要问我这是怎么回事。
这里有一些证据:
我将代码中的main
更改为:
#define BLOCK_SIZE (CACHE_SZ / 5000)
int main(int argc, char** argv){
init_cache();
int count = 0;
int failed = 0;
size_t size = 0;
/* the count considers the size of the cache arr */
while(count < int((CACHE_SZ / BLOCK_SIZE) * 0.96) ){
/* the following line tests malloc; the quantity(1024*24) ensures word alignment */
//char * volatile p = (char *) malloc(1024*24);
/* the following line tests above code in exactly the same way */
char * volatile p;
if (argc > 1)
p = (char *)malloc(BLOCK_SIZE);
else
p = alloc(BLOCK_SIZE);
if (p == 0)
{
failed++;
puts("p = NULLn");
}
count++;
size += BLOCK_SIZE;
}
printf("Count = %d, total=%zd, failed=%dn", count, size, failed);
}
然后改变CACHE_SZ,并在有或没有参数的情况下运行,以使用alloc
或malloc
选项:
因此,缓存大小为 12000000 (12MB):
这些数字是:
real 0m0.008s
user 0m0.001s
sys 0m0.007s
Count = 4800, total=11520000, failed=0
real 0m0.007s
user 0m0.000s
sys 0m0.006s
Count = 4800, total=11520000, failed=0
real 0m0.008s
user 0m0.001s
sys 0m0.006s
Count = 4800, total=11520000, failed=0
real 0m0.014s
user 0m0.003s
sys 0m0.010s
和几个运行与malloc
:
real 0m0.010s
user 0m0.000s
sys 0m0.009s
Count = 4800, total=11520000, failed=0
real 0m0.017s
user 0m0.001s
sys 0m0.015s
Count = 4800, total=11520000, failed=0
real 0m0.012s
user 0m0.001s
sys 0m0.010s
Count = 4800, total=11520000, failed=0
real 0m0.021s
user 0m0.007s
sys 0m0.013s
Count = 4800, total=11520000, failed=0
real 0m0.010s
user 0m0.001s
sys 0m0.008s
Count = 4800, total=11520000, failed=0
real 0m0.009s
user 0m0.001s
sys 0m0.007s
将缓存大小增大 10 倍会得到以下alloc
结果:
real 0m0.038s
user 0m0.001s
sys 0m0.036s
Count = 4800, total=115200000, failed=0
real 0m0.040s
user 0m0.001s
sys 0m0.037s
Count = 4800, total=115200000, failed=0
real 0m0.045s
user 0m0.001s
sys 0m0.043s
Count = 4800, total=115200000, failed=0
real 0m0.044s
user 0m0.001s
sys 0m0.043s
Count = 4800, total=115200000, failed=0
real 0m0.046s
user 0m0.001s
sys 0m0.043s
Count = 4800, total=115200000, failed=0
real 0m0.042s
user 0m0.000s
sys 0m0.042s
并带有malloc
:
real 0m0.026s
user 0m0.004s
sys 0m0.021s
Count = 4800, total=115200000, failed=0
real 0m0.027s
user 0m0.002s
sys 0m0.023s
Count = 4800, total=115200000, failed=0
real 0m0.022s
user 0m0.002s
sys 0m0.018s
Count = 4800, total=115200000, failed=0
real 0m0.016s
user 0m0.001s
sys 0m0.015s
Count = 4800, total=115200000, failed=0
real 0m0.027s
user 0m0.002s
sys 0m0.024s
Count = 4800, total=115200000, failed=0
另外 10 倍带有alloc
:
real 0m1.408s
user 0m0.002s
sys 0m1.395s
Count = 4800, total=1152000000, failed=0
real 0m1.517s
user 0m0.001s
sys 0m1.505s
Count = 4800, total=1152000000, failed=0
real 0m1.478s
user 0m0.000s
sys 0m1.466s
Count = 4800, total=1152000000, failed=0
real 0m1.401s
user 0m0.001s
sys 0m1.389s
Count = 4800, total=1152000000, failed=0
real 0m1.445s
user 0m0.002s
sys 0m1.433s
Count = 4800, total=1152000000, failed=0
real 0m1.468s
user 0m0.000s
sys 0m1.458s
Count = 4800, total=1152000000, failed=0
malloc
:
real 0m0.020s
user 0m0.002s
sys 0m0.017s
Count = 4800, total=1152000000, failed=0
real 0m0.022s
user 0m0.001s
sys 0m0.020s
Count = 4800, total=1152000000, failed=0
real 0m0.027s
user 0m0.005s
sys 0m0.021s
Count = 4800, total=1152000000, failed=0
real 0m0.029s
user 0m0.002s
sys 0m0.026s
Count = 4800, total=1152000000, failed=0
real 0m0.020s
user 0m0.001s
sys 0m0.019s
Count = 4800, total=1152000000, failed=0
如果我们更改代码以使BLOCK_SIZE
常数为 1000,则alloc
和malloc
之间的差异会小得多。以下是alloc
结果:
Count = 1080000, total=1080000000, failed=0
real 0m1.183s
user 0m0.028s
sys 0m1.137s
Count = 1080000, total=1080000000, failed=0
real 0m1.179s
user 0m0.017s
sys 0m1.143s
Count = 1080000, total=1080000000, failed=0
real 0m1.196s
user 0m0.026s
sys 0m1.152s
Count = 1080000, total=1080000000, failed=0
real 0m1.197s
user 0m0.023s
sys 0m1.157s
Count = 1080000, total=1080000000, failed=0
real 0m1.188s
user 0m0.021s
sys 0m1.147s
现在malloc
:
Count = 1080000, total=1080000000, failed=0
real 0m0.582s
user 0m0.063s
sys 0m0.482s
Count = 1080000, total=1080000000, failed=0
real 0m0.586s
user 0m0.062s
sys 0m0.489s
Count = 1080000, total=1080000000, failed=0
real 0m0.582s
user 0m0.059s
sys 0m0.483s
Count = 1080000, total=1080000000, failed=0
real 0m0.590s
user 0m0.064s
sys 0m0.477s
Count = 1080000, total=1080000000, failed=0
real 0m0.586s
user 0m0.075s
sys 0m0.473s
- C和C 中的代码优化
- 使用清理代码优化多个出口点
- 在 c++ 中从 txt 文件中提取条目的代码优化问题
- 为什么传递值参数经常使编译器更容易进行代码优化
- 来自MATLAB的代码优化直方图C
- C - 代码优化
- 代码优化子集总和
- 在代码优化过程中,C++11编译器是否会在可能的情况下将局部变量转换为右值
- C++-筛选Atkin代码优化
- 协议缓冲区 GetRepeatedField (反射) 代码优化
- C++代码优化
- 编译和代码优化
- C++ ARM 设备上代码优化的提示
- 我是否应该将 const 用于局部变量以获得更好的代码优化
- 用于并行计算的C++代码优化示例
- gcc/C++:如果CPU负载很低,那么代码优化用处不大,这是真的
- 反转每个单词在一个句子中使用c++需要代码优化我的代码片段
- 是我的编译器将适当的代码优化为无用的崩溃代码
- Arduino代码优化的多路复用LED矩阵
- 在代码优化中使用new运算符是否值得?