我有一个近 900 万行的数据文件(很快将超过 5 亿行),我正在寻找读取它的最快方法。五个对齐的列被填充并用空格分隔,所以我知道在哪里在每一行上查找我想要的两个字段。 我的 Python 例程需要 45 秒:
import sys,time
start = time.time()
filename = 'test.txt' # space-delimited, aligned columns
trans=[]
numax=0
for line in open(linefile,'r'):
nu=float(line[-23:-11]); S=float(line[-10:-1])
if nu>numax: numax=nu
trans.append((nu,S))
end=time.time()
print len(trans),'transitions read in %.1f secs' % (end-start)
print 'numax =',numax
而我在 C 中想出的例程是更令人愉快的 4 秒:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define BPL 47
#define FILENAME "test.txt"
#define NTRANS 8858226
int main(void) {
size_t num;
unsigned long i;
char buf[BPL];
char* sp;
double *nu, *S;
double numax;
FILE *fp;
time_t start,end;
nu = (double *)malloc(NTRANS * sizeof(double));
S = (double *)malloc(NTRANS * sizeof(double));
start = time(NULL);
if ((fp=fopen(FILENAME,"rb"))!=NULL) {
i=0;
numax=0.;
do {
if (i==NTRANS) {break;}
num = fread(buf, 1, BPL, fp);
buf[BPL-1]='\0';
sp = &buf[BPL-10]; S[i] = atof(sp);
buf[BPL-11]='\0';
sp = &buf[BPL-23]; nu[i] = atof(sp);
if (nu[i]>numax) {numax=nu[i];}
++i;
} while (num == BPL);
fclose(fp);
end = time(NULL);
fprintf(stdout, "%d lines read; numax = %12.6f\n", (int)i, numax);
fprintf(stdout, "that took %.1f secs\n", difftime(end,start));
} else {
fprintf(stderr, "Error opening file %s\n", FILENAME);
free(nu); free(S);
return EXIT_FAILURE;
}
free(nu); free(S);
return EXIT_SUCCESS;
}
Fortran、C++ 和 Java 中的解决方案需要中等的时间(27 秒、20 秒、8 秒)。 我的问题是:我在上面是否犯了任何令人发指的错误(特别是C代码)?有什么方法可以加快 Python 例程的速度吗?我很快意识到将数据存储在元组数组中比为每个条目实例化一个类更好。
几点:
你的C例程在作弊;它正在被提示文件大小,并且正在预分配......
Python:考虑使用 array.array('d') ... S 和 nu 各一个。然后尝试预分配。
Python:将例程编写为函数并调用它——访问函数局部变量比访问模块全局变量要快。
可能适用于 C、C++ 和 Python 版本的一种方法是使用内存映射文件。最显着的好处是,它可以减少将数据从一个缓冲区复制到另一个缓冲区时的双重处理量。在许多情况下,由于 I/O 系统调用数量的减少,还有一些好处。
在 C 实现中,您可以尝试将
fopen()
/fread()
/fclose()
库函数替换为较低级别的系统调用 open()
/read()
/close()
。加速可能来自于以下事实:fread()
进行了大量缓冲,而 read()
则没有。
此外,使用较大的块更少地调用
read()
将减少系统调用的数量,因此您将减少用户空间和内核空间之间的切换。当您发出 read()
系统调用(无论是否从 fread()
库函数调用)时,内核所做的就是从磁盘读取数据,然后将其复制到用户空间。如果您在代码中经常发出系统调用,则复制部分会变得昂贵。通过阅读更大的块,您最终会减少上下文切换和复制。
请记住,
read()
不能保证返回您想要的确切字节数的块。这就是为什么在可靠且正确的实现中,您始终必须检查 read()
的返回值。
您在
1
中的 BPL
和 fread()
参数以错误的方式存在(按照您的方式,它可以读取部分行,您无需测试)。在尝试使用返回的数据之前,您还应该测试 fread()
的返回值。
您可以通过一次读取多行来加快 C 版本的速度
#define LINES_PER_READ 1000
char buf[LINES_PER_READ][BPL];
/* ... */
while (i < NTRANS && (num = fread(buf, BPL, LINES_PER_READ, fp)) > 0) {
int line;
for (line = 0; i < NTRANS && line < num; line++)
{
buf[line][BPL-1]='\0';
sp = &buf[line][BPL-10]; S[i] = atof(sp);
buf[line][BPL-11]='\0';
sp = &buf[line][BPL-23]; nu[i] = atof(sp);
if (nu[i]>numax) {numax=nu[i];}
++i;
}
}
在支持
posix_fadvise()
的系统上,您还应该在打开文件后预先执行此操作:
posix_fadvise(fileno(fp), 0, 0, POSIX_FADV_SEQUENTIAL);
绝对必要。为了尝试模拟OP的需求,我有一个方便的3列ASCII-only
.txt
文件,由
'='
而不是
' '
,分隔 跨越一些
148 million rows
几乎
7.6 GiB
。
将同一个文件传输 3 次,得到一些
- 这只是我用于代码验证检查的一个大素数文件
- 这 3 列是素数的以 2 为底的对数,固定宽度为 5 个小数位,数字本身,采用直接 ASCII 十进制数字,以及该值的十六进制。
444 mn rows
的合成输入(距离
500 mn
提到的
OP
目标不远),并使用
awk
收集它的一些基本统计数据 —(总结)第一列作为浮点值, 以及第二列和第三列的列宽分组计数)
444 mn rows
22.8 GiB
、78.26 secs
合成平面文本文件,没有预制索引。以相同的速率处理 900 万行只会:
1.586 秒 -当 shell 脚本具有这样的速度时,
C
可能会带来更多麻烦。
( for ___ in $$ $$$$ $$$$$$; do; pv -q < "$__"; done | pvE 0.1 in0 | mawk2 ; )
77.05s user 7.66s system 108% cpu 1:18.26 total
gcat -b 0.00s user 0.00s system 0% cpu 1:18.26 total
(出于可读性目的,删除了数百行)
log2(_)-sum :: ~ 29790561307.14320755004882812-bits
11515 3 3 34545
11370 3 6 68655
10369 3 9 99762
10154 3 12 130224
10098 3 15 160518
...
2222 3 4449 14761953
2000 6 6123 18273609
1111 165 69522 105717363
1000 177 90270 127527948
999 252 90522 127779696
888 330 124305 159575346
777 432 170421 197709843
666 975 232761 242405211
555 924 341022 308075370
444 1878 553710 412565496
333 4317 969051 571686960
222 35214 2756478 1036817178
111 53748 7457043 1796338806
99 80262 8224686 1876339980
88 95472 9469041 1991870625
77 124485 10676904 2090514033
66 171246 12489018 2217919404
55 280368 14771130 2354031702
44 827844 20983845 2649656613
33 1502661 30431553 3001060344
22 7765587 59774289 3729032556
11 21562245 433469169 9124979175
9 2761992 443583456 9223360053
8 414552 443998008 9226676469
7 295692 444293700 9228746313
6 148572 444442272 9229637745
5 23955 444466227 9229757520
4 3183 444469410 9229770252
3 429 444469839 9229771539
2 54 444469893 9229771647
3333 3 855 3876831
1111 114 38097 56141871
1000 153 54039 72918171
999 174 54213 73091997
888 159 76497 94118478
777 258 109593 121425312
666 537 157764 155947470
555 504 231174 200476329
444 1212 366900 267599124
333 4389 685701 388875105
222 12837 1553802 619755666
111 61842 6291783 1374531276
99 66285 7030575 1451567319
88 67101 7769130 1520132172
77 149163 8927163 1614135813
66 150294 10389507 1718099022
55 243546 12483837 1842271380
44 339105 15356979 1981646316
33 692868 23890017 2301350703
22 1083543 37376904 2664284580
11 105021729 327544728 6535425114
9 19497831 437237055 7612850553
8 5280768 442517823 7655096697
7 1395681 443913504 7664866464
6 373872 444287376 7667109696
5 163302 444450678 7667926206
4 17544 444468222 7667996382
3 1530 444469752 7668000972
2 141 444469893 7668001254
mawk2 '
BEGIN { CONVFMT = OFMT = "%.250g"
_ = +(FS = "=" )
OFS = "=| "
} {
__ += $++_
___[length($++_) ]++
____[-_ + \
length($(_--+_--))]++
} END {
printf "\f\f\r\t log2(_)-sum :: ~ %29.17f-bits\n\n", __
_____(____, _____(___))
}
function _____(___, _, __, ____, _______) {
_ = ""
for (_ in ___) {
_ = length(___)
break
}
if (_) {
NF *= NF = (__ ^= NF = _<_)\
+ (____ = __)
for (_ in ___)__ < +_ &&
__ = +_
_______ = "(000|^(1+|2+|3+|4+|5+|6+|7+|8+|9+|.....+))$"
for (_ = ++__; --_; ) {
if (__ = +___[_]) {
$NF += ($____ = _) * ($(____+____) = __)
$(NF-____) += __
if (_ ~ _______)
print
}
}
print "\f\r" (_=(_="---=| ") _) _ (_) _ "---\f\n"
return
}'
double *pS = S, *pnu = nu;
...
*pS++ = atof(sp);
*pnu = atof(sp);
...
此外,由于您总是在 buf 中的相同位置从 char 转换为 double,因此请在循环外部预先计算地址,而不是每次在循环中计算它们。