[英]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 文件中的行具有可變長度,因此您必須讀取整個文件才能獲取所需行的數據。 整個文件的順序讀取仍然會非常慢 - 即使您盡可能優化文件讀取。 一個好的指標實際上是 wc -l 的運行時間,正如問題中的 OP 已經提到的那樣。
相反,應該在算法層面進行優化。 需要對數據進行一次性預處理,這樣就可以快速訪問某些行,而無需讀取整個文件。
有幾種可能的方法,例如:
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.