C 优化 - 低级代码

C optimization - low level code

本文关键字:代码 优化      更新时间:2023-10-16

我正在尝试编写一个与 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,并在有或没有参数的情况下运行,以使用allocmalloc选项:

因此,缓存大小为 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,则allocmalloc之间的差异会小得多。以下是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