如何快速读取和解析带有数字的文本文件(在 C 中)?

How do I read and parse a text file with numbers, fast (in C)?

最后一次更新:我同学用fread()把整个文件的三分之一读成一个字符串,这样可以避免内存不足。然后处理这个字符串,将这个字符串分离到你的数据结构中。注意,你需要关心一个问题:在这个字符串的末尾,最后这几个字符可能不能组成一个整数。考虑一种检测这种情况的方法,以便您可以将这些字符与下一个字符串的前几个字符连接起来。 每个数字对应于数据结构中的不同变量。您的数据结构应该非常简单,因为每次将数据插入一个数据结构时,速度都非常慢。大部分时间花在将数据插入数据结构上。因此,处理这些数据最快的方法是:用fread()把这个文件读成一个字符串,把这个字符串分成不同的一维数组。 例如(只是一个例子,不是来自我的项目),我有一个文本文件,如:

72     24      20
22     14      30
23     35      40
42     29      50
19     22      60
18     64      70
 .
 .
 .

每一行是一个人的信息。第一栏是这个人的年龄,第二栏是他的存款,第二栏是他妻子的年龄。 然后我们使用 fread() 将这个文本文件读入字符串,然后我使用 stroke() 来分隔它(你可以使用更快的方法来分隔它)。 不要使用数据结构来存储分离的数据! 我的意思是,不要这样做:

struct person
{
    int age;
    int deposit;
    int wife_age;
};
struct person *my_data_store;
my_data_store=malloc(sizeof(struct person)*length_of_this_array);
//then insert separated data into my_data_store

不要用数据结构来存储数据! 存储数据最快的方法是这样的:

int *age;
int *deposit;
int *wife_age;

age=(int*)malloc(sizeof(int)*age_array_length);
deposit=(int*)malloc(sizeof(int)*deposit_array_length);
wife_age=(int*)malloc(sizeof(int)*wife_array_length);
// the value of age_array_length,deposit_array_length and wife_array_length will be known by using `wc -l`.You can use wc -l to get the value in your C program
// then you can insert separated data into these arrays when you use `stroke()` to separate them.

第二次更新:最好的方法是使用freed()将文件的一部分读入一个字符串,然后将这些字符串分离到你的数据结构中。顺便说一句,不要使用任何可以将字符串格式化为整数的标准库函数,那样会很慢,例如fscanf() or atoi(),我们应该编写自己的函数将一个字符串转换为n个整数。不仅如此,我们还应该设计一个更简单的数据结构来存储这些数据。顺便说一句,我同学可以在7秒内读完这个1.7G的文件。有一种方法可以做到这一点。这种方式比使用多线程要好得多。我没有看到他的代码,看到他的代码后,我会第三次更新告诉你你怎么会这样。那将是我们课程结束两个月后。

更新:我用多线程来解决这个问题!!有用!注意:使用多线程时不要使用clock()来计算时间,所以我认为执行时间会增加。

我想澄清的一件事是,在不将值存储到我的结构中的情况下读取文件的时间大约为 20 秒。将值存储到我的结构中的时间约为 60 秒。 "time of reading the file" 的定义包括读取整个文件并将值存储到我的结构中的时间。读取文件的时间=扫描文件+将值存储到我的结构中。因此,有一些更快储值的建议吗? (对了,inout文件我没有控制权,是我们教授生成的,我正在尝试用多线程来解决这个问题,如果可行,我会告诉你结果。)

我有一个文件,大小是1.7G。 看起来像:

1   1427826
1   1427827
1   1750238
1   2
2   3
2   4
3   5
3   6
10  7
11  794106
.
.

还有儿子。 它在文件中有大约一千万行。现在我需要读取这个文件并在 15 秒内将这些数字存储在我的数据结构中。 我试过使用 freed() 读取整个文件,然后使用 strtok() 分隔每个数字,但它仍然需要 80 秒。如果我使用 fscanf(),它会更慢。我该如何加快速度?也许我们不能让它少于 15 秒。但是80秒阅读它太长了。如何以最快的速度阅读它?

这是我的部分阅读代码:

int Read_File(FILE *fd,int round)
{
clock_t start_read = clock();


int first,second;
first=0;
second=0;


fseek(fd,0,SEEK_END);
long int fileSize=ftell(fd);
fseek(fd,0,SEEK_SET);
char * buffer=(char *)malloc(sizeof(char)*fileSize);
char *string_first;
long int newFileSize=fread(buffer,1,fileSize,fd);
char *string_second;
     while(string_first!=NULL)

    {
        first=atoi(string_first);

        string_second=strtok(NULL," \t\n");

        second=atoi(string_second);

        string_first=strtok(NULL," \t\n");

        max_num= first > max_num ? first : max_num ;
        max_num= second > max_num ? second : max_num ;
        root_level=first/NUM_OF_EACH_LEVEL;

        leaf_addr=first%NUM_OF_EACH_LEVEL;

        if(root_addr[root_level][leaf_addr].node_value!=first)
        {
            root_addr[root_level][leaf_addr].node_value=first;
            root_addr[root_level][leaf_addr].head=(Neighbor *)malloc(sizeof(Neighbor));
            root_addr[root_level][leaf_addr].tail=(Neighbor *)malloc(sizeof(Neighbor));

            root_addr[root_level][leaf_addr].g_credit[0]=1;

            root_addr[root_level][leaf_addr].head->neighbor_value=second;

           root_addr[root_level][leaf_addr].head->next=NULL;
            root_addr[root_level][leaf_addr].tail=root_addr[root_level][leaf_addr].head;
            root_addr[root_level][leaf_addr].degree=1;
        }
        else
        {
            //insert its new neighbor
            Neighbor *newNeighbor;
            newNeighbor=(Neighbor*)malloc(sizeof(Neighbor));

            newNeighbor->neighbor_value=second;

            root_addr[root_level][leaf_addr].tail->next=newNeighbor;
            root_addr[root_level][leaf_addr].tail=newNeighbor;

            root_addr[root_level][leaf_addr].degree++;
        }
        root_level=second/NUM_OF_EACH_LEVEL;

        leaf_addr=second%NUM_OF_EACH_LEVEL;
        if(root_addr[root_level][leaf_addr].node_value!=second)
        {
            root_addr[root_level][leaf_addr].node_value=second;
            root_addr[root_level][leaf_addr].head=(Neighbor *)malloc(sizeof(Neighbor));
            root_addr[root_level][leaf_addr].tail=(Neighbor *)malloc(sizeof(Neighbor));

            root_addr[root_level][leaf_addr].head->neighbor_value=first;

            root_addr[root_level][leaf_addr].head->next=NULL;
            root_addr[root_level][leaf_addr].tail=root_addr[root_level][leaf_addr].head;

            root_addr[root_level][leaf_addr].degree=1;

            root_addr[root_level][leaf_addr].g_credit[0]=1;
        }
        else
        {
            //insert its new neighbor
            Neighbor *newNeighbor;
            newNeighbor=(Neighbor*)malloc(sizeof(Neighbor));

            newNeighbor->neighbor_value=first;

            root_addr[root_level][leaf_addr].tail->next=newNeighbor;
            root_addr[root_level][leaf_addr].tail=newNeighbor;

            root_addr[root_level][leaf_addr].degree++;
        }
    }

我的建议是形成一个处理管道并将其线程化。读取文件是 I/O 绑定任务,解析文件是 CPU 绑定任务。它们可以同时并行完成。

有几种可能性。您将不得不进行试验。

  • 利用你的 OS 给你的东西。如果 Windows,请查看 overlapped io. This lets your computation proceed with parsing one buffer full of data while the Windows kernel fills another. Then switch buffers and continue. This is related to what @Neal suggested, but has less overhead for buffering. Windows is depositing data directly in your buffer through the DMA channel. No copying. If Linux, check out memory mapped files。这里 OS 使用虚拟内存硬件来或多或少地做 Windows 重叠的事情。

  • 编写您自己的整数转换代码。这可能比对每个整数进行 clib 调用要快一些。

这是示例代码。你想绝对限制比较的次数。

// Process one input buffer.
*end_buf = ' '; // add a sentinel at the end of the buffer
for (char *p = buf; p < end_buf; p++) {
  // somewhat unsafe (but fast) reliance on unsigned wrapping
  unsigned val = *p - '0';
  if (val <= 9) {
    // Found start of integer.
    for (;;) {
      unsigned digit_val = *p - '0';
      if (digit_val > 9) break;
      val = 10 * val + digit_val;
      p++;
    }
    ... do something with val
  }
}
  • 不要每条记录调用一次 malloc。您应该一次分配许多结构的块。

  • 试验缓冲区大小。

  • 加强编译器优化。优秀的代码生成就是这样的代码。

是的,标准库转换函数出奇地慢。

如果可移植性不是问题,我会对该文件进行内存映射。然后,可以使用类似于以下 C99 代码(未经测试)的内容来解析整个内存映射:

#include <stdlib.h>
#include <errno.h>

struct pair {
    unsigned long  key;
    unsigned long  value;
};

typedef struct {
    size_t       size; /* Maximum number of items */
    size_t       used; /* Number of items used */
    struct pair  item[];
} items;

/* Initial number of items to allocate for */
#ifndef ITEM_ALLOC_SIZE
#define ITEM_ALLOC_SIZE 8388608
#endif

/* Adjustment to new size (parameter is old number of items) */
#ifndef ITEM_REALLOC_SIZE
#define ITEM_REALLOC_SIZE(from) (((from) | 1048575) + 1048577)
#endif 

items *parse_items(const void *const data, const size_t length)
{
    const unsigned char       *ptr = (const unsigned char *)data;
    const unsigned char *const end = (const unsigned char *)data + length;
    items                  *result;
    size_t                  size = ITEMS_ALLOC_SIZE;
    size_t                  used = 0;
    unsigned long           val1, val2;

    result = malloc(sizeof (items) + size * sizeof (struct pair));
    if (!result) {
        errno = ENOMEM;
        return NULL;
    }

    while (ptr < end) {

        /* Skip newlines and whitespace. */
        while (ptr < end && (*ptr == '[=10=]' || *ptr == '\t' ||
                             *ptr == '\n' || *ptr == '\v' ||
                             *ptr == '\f' || *ptr == '\r' ||
                             *ptr == ' '))
            ptr++;

        /* End of data? */
        if (ptr >= end)
            break;

        /* Parse first number. */
        if (*ptr >= '0' && *ptr <= '9')
            val1 = *(ptr++) - '0';
        else {
            free(result);
            errno = ECOMM; /* Bad data! */
            return NULL;
        }
        while (ptr < end && *ptr >= '0' && *ptr <= '9') {
            const unsigned long old = val1;
            val1 = 10UL * val1 + (*(ptr++) - '0');
            if (val1 < old) {
                free(result);
                errno = EDOM; /* Overflow! */
                return NULL;
            }
        }

        /* Skip whitespace. */
        while (ptr < end && (*ptr == '\t' || *ptr == '\v'
                             *ptr == '\f' || *ptr == ' '))
            ptr++;
        if (ptr >= end) {
            free(result);
            errno = ECOMM; /* Bad data! */
            return NULL;
        }

        /* Parse second number. */
        if (*ptr >= '0' && *ptr <= '9')
            val2 = *(ptr++) - '0';
        else {
            free(result);
            errno = ECOMM; /* Bad data! */
            return NULL;
        }
        while (ptr < end && *ptr >= '0' && *ptr <= '9') {
            const unsigned long old = val2;
            val1 = 10UL * val2 + (*(ptr++) - '0');
            if (val2 < old) {
                free(result);
                errno = EDOM; /* Overflow! */
                return NULL;
            }
        }
        if (ptr < end) {
            /* Error unless whitespace or newline. */
            if (*ptr != '[=10=]' && *ptr != '\t' && *ptr != '\n' &&
                *ptr != '\v' && *ptr != '\f' && *ptr != '\r' &&
                *ptr != ' ') {
                free(result);
                errno = ECOMM; /* Bad data! */
                return NULL;
            }

            /* Skip the rest of this line. */
            while (ptr < end && *ptr != '\n' && *ptr != '\r')
                ptr++;
        }

        /* Need to grow result? */
        if (used >= size) {
            items *const old = result;

            size = ITEMS_REALLOC_SIZE(used);
            result = realloc(result, sizeof (items) + size * sizeof (struct pair));
            if (!result) {
                free(old);
                errno = ENOMEM;
                return NULL;
            }
        }

        result->items[used].key = val1;
        result->items[used].value = val2;
        used++;
    }

    /* Note: we could reallocate result here,
     *       if memory use is an issue.
    */

    result->size = size;
    result->used = used;

    errno = 0;
    return result;
}

我使用了类似的方法来加载分子数据以进行可视化。此类数据包含浮点值,但精度通常只有七位有效数字,不需要多精度数学运算。解析此类数据的自定义例程在速度上至少比标准函数快一个数量级。

至少 Linux 内核非常善于观察 memory/file 访问模式;使用 madvise() 也有帮助。

如果你不能使用内存映射,那么解析函数会有点不同:它会追加到现有结果,如果缓冲区中的最后一行是部分的,它会指出(和数字未解析的字符数),以便调用者可以 memmove() 缓冲区、读取更多数据并继续解析。 (使用 16 字节对齐的地址读取新数据,以最大限度地提高复制速度。您不必将未读取的数据移动到缓冲区的确切开头,您知道;只需保留缓冲数据中的当前位置即可。)

有问题吗?

首先,你的磁盘硬件是什么?单个 SATA 驱动器可能会达到 100 MB/sec。可能更像是 50-70 MB/sec。如果您已经尽可能快地从驱动器上移出数据,那么您所做的所有软件调整都将付诸东流。

如果你的硬件可以支持更快的读取?首先,您的读取模式——将整个文件读入内存一次——是直接 IO 的完美用例。使用 open( "/file/name", O_RDONLY | O_DIRECT ); 打开文件。以页面大小的块读取页面对齐的缓冲区(参见 valloc() 的手册页)。使用直接 IO 将导致您的数据绕过内核页面缓存中的双缓冲,当您快速读取那么多数据而不是一遍又一遍地重新读取相同的数据页面时,这是无用的。

如果您 运行 在一个真正的高性能文件系统上,您可以使用 lio_listio() 或 aio_read() 进行异步读取并且可能速度更快。或者你可以只使用多个线程来读取 - 并使用 pread() 这样你就不会浪费时间寻找 - 因为当你使用多个线程读取时,对打开文件的搜索会影响所有试图从文件中读取的线程.

并且 而不是 尝试快速读入新 malloc 的内存块 - 首先 memset() 它。因为真正快速的磁盘系统可以比虚拟内存管理器为进程创建虚拟页面更快地将数据泵入CPU。

一些建议:

a) 考虑将文件转换(或预处理)为二进制格式;目的是最小化文件大小并大大降低解析成本。我不知道您的值的范围,但是各种技术(例如使用一位来判断数字是小还是大并将数字存储为 7 位整数或 31 位整数)可以将文件减半IO(以及从磁盘读取文件的速度翻倍)并将解析成本降至几乎为零。 注意:为了获得最大效果,您首先要修改创建该文件的任何软件。

b) 在解析之前将整个文件读入内存是错误的。它使所需的 RAM 量增加一倍(以及 allocating/freeing 的成本)并且对 CPU 缓存有不利影响。取而代之的是读取少量文件(例如 16 KiB)并处理它,然后读取下一块并处理它,依此类推;这样您就可以不断重复使用相同的小缓冲内存。

c) 对文件 IO 使用并行性。在处理文件的前一部分时读取文件的下一部分应该不难(通过使用 2 个线程或使用异步 IO)。

d) 为 "neighbour" 结构预分配内存并从循环中删除 most/all malloc() 调用。最好的情况是使用静态分配的数组作为池——例如Neighbor myPool[MAX_NEIGHBORS]; 其中 malloc() 可以替换为 &myPool[nextEntry++];。这 reduces/removes malloc() 的开销,同时还改进了数据本身的缓存局部性。

e) 使用并行来存储值。例如,您可以有多个线程,其中第一个线程处理 root_level % NUM_THREADS == 0 的所有情况,第二个线程处理 root_level % NUM_THREADS == 1 的所有情况,等等

有了以上所有(假设是现代 4 核 CPU),我认为您可以将总时间(用于读取和存储)减少到 15 秒以下。