簡體   English   中英

從大文件中讀取多行數據的最快方法是什么

[英]What is the fastest way to read several lines of data from a large file

我的應用程序需要從大約 300GB 十億行的大型 csv 文件中讀取數千行,每行包含幾個數字。 數據是這樣的:

1, 34, 56, 67, 678, 23462, ...
2, 3, 6, 8, 34, 5
23,547, 648, 34657 ...
...
...

我嘗試fget在 c 中逐行讀取文件,但它花費了非常非常長的時間,即使在 linux 中使用wc -l ,只是為了讀取所有行,花費了相當長的時間。

我還嘗試根據應用程序的邏輯將所有數據寫入sqlite3數據庫。 但是,數據結構與上面的 csv 文件不同,現在有 1000 億行,每行只有兩個數字。 然后我在它們之上創建了兩個索引,生成了一個 2.5TB 的數據庫,而之前沒有索引是 1 TB。 由於索引的規模大於數據,查詢必須讀取整個 1.5 TB 索引,我認為使用數據庫方法沒有任何意義吧?

所以我想問一下,在 C 或 python 中有十億行的大型 csv 文件中讀取幾行的最快方法是什么。 順便說一句,是否有任何公式或東西來計算讀取文件和RAM容量之間的時間消耗。

環境:linux,RAM 200GB,C,python

要求

  • 巨大的 csv 文件,數百 GB
  • 每行包含幾個數字
  • 程序每次運行必須提取幾千行
  • 該程序多次使用同一個文件,只應提取不同的行

由於 csv 文件中的行具有可變長度,因此您必須讀取整個文件才能獲取所需行的數據。 整個文件的順序讀取仍然會非常慢 - 即使您盡可能優化文件讀取。 一個好的指標實際上是 wc -l 的運行時間,正如問題中的 OP 已經提到的那樣。

相反,應該在算法層面進行優化。 需要對數據進行一次性預處理,這樣就可以快速訪問某些行,而無需讀取整個文件。

有幾種可能的方法,例如:

  1. 使用帶有索引的數據庫
  2. 以編程方式創建索引文件(將行號與文件偏移量關聯)
  3. 將csv文件轉換為固定格式的二進制文件

OP 測試表明方法 1) 導致 1.5 TB 指數。 方法2),創建一個將行號與文件偏移量連接起來的小程序當然也是一種可能。 最后,方法 3 將允許計算文件偏移量到行號,而不需要單獨的索引文件。 如果每行的最大數字數已知,則此方法特別有用。 否則,方法 2 和方法 3 非常相似。

下面將更詳細地解釋方法 3。 可能還有其他要求需要稍微修改該方法,但以下內容應該可以開始。

需要一次性預處理。 將文本 csv 行轉換為 int arrays 並使用固定記錄格式將 int 以二進制格式存儲在單獨的文件中。 然后讀取特定行n ,您可以簡單地計算文件偏移量,例如使用line_nr * (sizeof(int) * MAX_NUMBERS_PER_LINE); . 最后,使用fseeko(fp, offset, SEEK_SET); 跳轉到這個偏移量並讀取 MAX_NUMBERS_PER_LINE 個整數。 所以你只需要讀取你真正想要處理的數據。

這不僅具有程序運行速度更快的優點,而且它還需要很少的主 memory。

測試用例

創建了一個包含 3,000,000,000 行的測試文件。 每行最多包含 10 個隨機整數,以逗號分隔。

在這種情況下,這給出了一個包含大約 342 GB 數據的 csv 文件。

快速測試

time wc -l numbers.csv 

187.14s user 74.55s system 96% cpu 4:31.48 total

這意味着如果使用順序文件讀取方法,總共需要至少 4.5 分鍾。

對於一次性預處理,轉換器程序讀取每一行並每行存儲 10 個二進制整數。 轉換后的文件稱為“numbers_bin”。 訪問 10,000 個隨機選擇的行的數據的快速測試:

time demo numbers_bin

0.03s user 0.20s system 5% cpu 4.105 total

因此,這個特定示例數據需要 4.1 秒,而不是 4.5 分鍾。 這比速度快了 65 倍。

源代碼

這種方法聽起來可能比實際情況復雜。

讓我們從轉換器程序開始。 它讀取 csv 文件並創建二進制固定格式文件。

有趣的部分發生在 function 預處理過程中:在循環中使用“getline”讀取一行,使用“strtok”和“strtol”提取數字並放入初始化為 0 的 int 數組。最后寫入該數組到帶有“fwrite”的 output 文件。

轉換過程中的錯誤會在 stderr 上產生一條消息,並且程序會終止。

轉換.c

#include "data.h"
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>

static void pre_process(FILE *in, FILE *out) {
    int *block = get_buffer();
    char *line = NULL;
    size_t line_capp = 0;

    while (getline(&line, &line_capp, in) > 0) {
        line[strcspn(line, "\n")] = '\0';
        memset(block, 0, sizeof(int) * MAX_ELEMENTS_PER_LINE);

        char *token;
        char *ptr = line;
        int i = 0;
        while ((token = strtok(ptr, ", ")) != NULL) {
            if (i >= MAX_ELEMENTS_PER_LINE) {
                fprintf(stderr, "too many elements in line");
                exit(EXIT_FAILURE);
            }
            char *end_ptr;
            errno = 0;
            long val = strtol(token, &end_ptr, 10);
            if (val > INT_MAX || val < INT_MIN || errno || *end_ptr != '\0' || end_ptr == token) {
                fprintf(stderr, "value error with '%s'\n", token);
                exit(EXIT_FAILURE);
            }
            ptr = NULL;
            block[i] = (int) val;
            i++;
        }
        fwrite(block, sizeof(int), MAX_ELEMENTS_PER_LINE, out);
    }
    free(block);
    free(line);
}


static void one_off_pre_processing(const char *csv_in, const char *bin_out) {
    FILE *in = get_file(csv_in, "rb");
    FILE *out = get_file(bin_out, "wb");
    pre_process(in, out);
    fclose(in);
    fclose(out);
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "usage: convert <in> <out>\n");
        exit(EXIT_FAILURE);
    }
    one_off_pre_processing(argv[1], argv[2]);
    return EXIT_SUCCESS;
}

數據.h

使用了一些輔助功能。 它們或多或少是不言自明的。

#ifndef DATA_H
#define DATA_H

#include <stdio.h>
#include <stdint.h>

#define NUM_LINES 3000000000LL
#define MAX_ELEMENTS_PER_LINE 10

void read_data(FILE *fp, uint64_t line_nr, int *block);
FILE *get_file(const char *const file_name, char *mode);
int *get_buffer();

#endif //DATA_H

數據.c

#include "data.h"
#include <stdlib.h>

void read_data(FILE *fp, uint64_t line_nr, int *block) {
    off_t offset = line_nr * (sizeof(int) * MAX_ELEMENTS_PER_LINE);
    fseeko(fp, offset, SEEK_SET);
    if(fread(block, sizeof(int), MAX_ELEMENTS_PER_LINE, fp) != MAX_ELEMENTS_PER_LINE) {
        fprintf(stderr, "data read error for line %lld", line_nr);
        exit(EXIT_FAILURE);
    }
}

FILE *get_file(const char *const file_name, char *mode) {
    FILE *fp;
    if ((fp = fopen(file_name, mode)) == NULL) {
        perror(file_name);
        exit(EXIT_FAILURE);
    }
    return fp;
}

int *get_buffer() {
    int *block = malloc(sizeof(int) * MAX_ELEMENTS_PER_LINE);
    if(block == NULL) {
        perror("malloc failed");
        exit(EXIT_FAILURE);
    }
    return block;
}

演示.c

最后是一個演示程序,它讀取 10,000 條隨機確定的行的數據。

function request_lines 確定 10,000 個隨機行。 這些行使用 qsort 排序。 讀取這些行的數據。 一些代碼行被注釋掉了。 如果將它們注釋掉,則讀取到調試控制台的數據為 output。

#include "data.h"
#include <stdlib.h>
#include <assert.h>
#include <sys/stat.h>


static int comp(const void *lhs, const void *rhs) {
    uint64_t l = *((uint64_t *) lhs);
    uint64_t r = *((uint64_t *) rhs);
    if (l > r) return 1;
    if (l < r) return -1;
    return 0;
}

static uint64_t *request_lines(uint64_t num_lines, int num_request_lines) {
    assert(num_lines < UINT32_MAX);
    uint64_t *request_lines = malloc(sizeof(*request_lines) * num_request_lines);

    for (int i = 0; i < num_request_lines; i++) {
        request_lines[i] = arc4random_uniform(num_lines);
    }
    qsort(request_lines, num_request_lines, sizeof(*request_lines), comp);

    return request_lines;
}


#define REQUEST_LINES 10000

int main(int argc, char *argv[]) {

    if (argc != 2) {
        fprintf(stderr, "usage: demo <file>\n");
        exit(EXIT_FAILURE);
    }

    struct stat stat_buf;
    if (stat(argv[1], &stat_buf) == -1) {
        perror(argv[1]);
        exit(EXIT_FAILURE);
    }

    uint64_t num_lines = stat_buf.st_size / (MAX_ELEMENTS_PER_LINE * sizeof(int));

    FILE *bin = get_file(argv[1], "rb");
    int *block = get_buffer();

    uint64_t *requests = request_lines(num_lines, REQUEST_LINES);
    for (int i = 0; i < REQUEST_LINES; i++) {
        read_data(bin, requests[i], block);
        //do sth with the data, 
        //uncomment the following lines to output the data to the console
//        printf("%llu: ", requests[i]);
//        for (int x = 0; x < MAX_ELEMENTS_PER_LINE; x++) {
//            printf("'%d' ", block[x]);
//        }
//        printf("\n");
    }

    free(requests);
    free(block);
    fclose(bin);

    return EXIT_SUCCESS;
}

概括

這種方法提供的結果比順序讀取整個文件要快得多(樣本數據每次運行 4 秒而不是 4.5 分鍾)。 它還需要很少的主 memory。

前提是將數據一次性預處理為二進制格式。 這種轉換非常耗時,但是之后可以使用查詢程序非常快速地讀取某些行的數據。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM