在c中构建未知长度的大字符串

时间:2013-11-13 20:06:03

标签: c performance

我毫不怀疑这个地方有一个答案,我找不到它。

经过长时间的休息后,我刚回到c,非常生疏,所以请原谅我的愚蠢错误。我需要生成一个大的(可能等于10mb)字符串。我不知道它会建成多长时间。

我尝试了以下两种测试速度的方法:

int main() {
#if 1
  size_t message_len = 1; /* + 1 for terminating NULL */
  char *buffer = (char*) malloc(message_len);
  for (int i = 0; i < 200000; i++)
  {
    int size = snprintf(NULL, 0, "%d \n", i);
    char * a = malloc(size + 1);
    sprintf(a, "%d \n", i);

    message_len += 1 + strlen(a); /* 1 + for separator ';' */
    buffer = (char*) realloc(buffer, message_len);
    strncat(buffer, a, message_len);
  }
#else
  FILE *f = fopen("test", "w"); 
  if (f == NULL) return -1; 
  for (int i = 0; i < 200000; i++)
  {
    fprintf(f, "%d \n", i);
  }
  fclose(f);
  FILE *fp = fopen("test", "r");
  fseek(fp, 0, SEEK_END);
  long fsize = ftell(f);
  fseek(fp, 0, SEEK_SET);
  char *buffer = malloc(fsize + 1);
  fread(buffer, fsize, 1, f);
  fclose(fp);
  buffer[fsize] = 0;
#endif
  char substr[56];
  memcpy(substr, buffer, 56);
  printf("%s", substr);
  return 1;
}

每次连接字符串的第一个解决方案需要3.8秒,写入文件的第二个解决方案需要0.02秒。

当然有一种快速的方法可以在c中构建一个大字符串而不需要读取和写入文件?我只是做一些非常低效的事情吗?如果没有,我可以写入某种文件对象,然后在最后阅读它,从不保存它?

在C#中你会使用字符串缓冲来避免缓慢的连接,c中的等值是什么?

提前致谢。

4 个答案:

答案 0 :(得分:4)

你用这些线条让生活变得非常粗糙:

for (int i = 0; i < 200000; i++)
  {
    int size = snprintf(NULL, 0, "%d \n", i);  // << executed in first loop only
    char * a = malloc(size + 1);               // allocate enough space for "0 \n" + 1
    sprintf(a, "%d \n", i);                    // may try to squeeze "199999 \n" into a

    message_len += 1 + strlen(a); /* 1 + for separator ';' */
    buffer = (char*) realloc(buffer, message_len);
    strncat(buffer, a, message_len);
  }

你计算size并在第一次迭代中为a分配空间 - 然后继续在每次后续迭代中使用它(i变大,你原则上会超过分配给a的存储空间。如果你做得正确(在每个循环中为a分配大小),你也必须在每个循环中free,或者造成巨大的内存泄漏。

C中的解决方案是预先分配大量内存 - 并且仅在紧急情况下重新分配。如果您“大致”知道字符串的大小,请立即分配所有内存;跟踪它有多大,如果你做空,还要增加更多。最后,你总能“回馈你没用过的东西”。对realloc的调用过多会导致内存不断移动(因为您经常没有足够的连续内存)。正如@Matt在他的评论中澄清的那样:每次调用realloc 都会移动整个内存块存在真正的风险 - 随着块变大,这会成为一个二次增加的负载系统。这里是一个可能更好的解决方案(完整,有小N和BLOCK测试只是为了显示原理,你会希望使用大N(您的200000值),较大块 - 和摆脱printf声明那些表明事情正在发挥作用):

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

#define N 2000000 
#define BLOCK 32 
int main(void) {
size_t message_len = BLOCK; //
  char *buffer = (char*) malloc(message_len);
  int bb;
  int i, n=0;
  char* a = buffer;
  clock_t start, stop;
  for(bb = 1; bb < 128; bb *= 2) {
  int rCount = 0;
  start = clock();
  for (i = 0; i < N; i++)
  {
    a = buffer + n;
    n += sprintf(a, "%d \n", i);
    if ((message_len - n) < BLOCK*bb) {
      rCount++;
      message_len += BLOCK*bb;
      //printf("increasing buffer\n");
      //printf("increased buffer to %ld\n", (long int)message_len);
      buffer = realloc(buffer, message_len);
    }
  }
  stop = clock();
  printf("\nat the end, buffer length is %d; rCount = %d\n", strlen(buffer), rCount);
//  buffer = realloc(buffer, strlen(buffer+1));
  //printf("buffer is now: \n%s\n", buffer);
  printf("time taken with blocksize = %d: %.1f ms\n", BLOCK*bb, (stop - start) * 1000.0 / CLOCKS_PER_SEC);
  }
}

您需要为BLOCK使用相当大的值 - 这会将调用次数限制为realloc。我会用100000这样的东西;无论如何,你最终摆脱了空间。

编辑我修改了我发布的代码以允许循环的时间 - 将N增加到200万以获得“合理的时间”。我还最小化了初始内存分配(迫使大量调用realloc并修复了一个错误(当realloc必须移动内存时,a不再指向buffer中的偏移量{1}}。现在通过跟踪n中的字符串长度来解决这个问题。

这非常快 - 最小块为450 ms,较大块(200万个数字)为350 ms。这与您的文件读/写操作具有可比性(在我的测量分辨率范围内)。但是 - 文件I / O流和相关的内存管理都经过了高度优化......

答案 1 :(得分:1)

我遗漏了一些细节,但我的做法一般都是这样的

创建一个像这样的结构

typedef struct {
    char *curr ;
    char *start ;
    char *end ;
} VBUF ;

沿着这些方向编写一些函数:

void vbuf_alloc(VBUF *v,int n)
{
    v->start = malloc(n) ;
    v->end = v->start + n ;
    v->curr = v->start ;
    }

int vbuf_add(VBUF *v,char *s,int length)
{
    if (v->end - v->curr < length) {
        vbuf_realloc(v,(v->end - v->start) * 2) ;
        }
    memcpy(v->curr,s,length) ;
    v->curr += length ;
    return length ;
    }

int vbuf_adds(VBUF *v,char *s)
{
    return vbud_add(v,s,strlen(s)) ;
    }

您可以根据需要扩展这套功能。

答案 2 :(得分:0)

c没有对象,所以没有等同于C#字符串缓冲区(尽管在C++中你会使用std::string)。

通过不在每个追加上调用realloc,并且永远不会按照你的方式调用malloc,你将获得性能提升。

只需声明一个足够大的char []来打印最大的int,就可以完全避免使用malloc;这也可以避免使用snprintf,而且尺寸相当小。

而不是经常调用realloc,你应该将缓冲区增加一些合理的大小...比如4kb(一个不错的大小与页面大小相对应),并且只有当它接近于时才会再次增长它耗尽(也就是说,当它的当前用量小于你从上面使用的数组大小时)。

答案 3 :(得分:0)

我建议在每个连续的字符串上代替realloc,如果长度太短,请提前智能realloc。换句话说,尽可能避免重新分配。

伪代码中的天真实现可能类似于

Initialize an int/long to "written so far"
Initialize an int/long to remember "buffer size"
Alloc memory for a string up to the "buffer size"
Read in the next chunk into a temporary buffer
Get the "chunk size" from the temporary buffer
If "written so far" + "chunk size" > "buffer size"
    Reallocate the chunk to be much bigger (double "buffer size"?)
    Set the new "buffer size"
Copy the data from the temporary buffer to "buffer address" + "written so far" + 1
Set "written so far" to "written so far" + "chunk size"

我只是将它们放在一起,因此可能存在索引错误,但您明白了:只有在拥有时才分配和复制,而不是每次都通过循环。