读取文本列的大型数据文件的最快方法是什么?

4

我有一个将近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例程的速度?我很快意识到,将数据存储在元组数组中比为每个条目实例化一个类更好。


2
请解释一下您的 C 代码中的魔数(您是如何想出 47 和 858226 的)? - mikerobi
6
你应该在Python代码上运行性能分析器,以查明它的缓慢之处。此外,你应该尝试遵循Python的PEP8风格规范,这会使其更易于阅读。 - Daenyth
1
对于Python:https://dev59.com/questions/-HI-5IYBdhLWcg3wW2-p - karlphillip
1
抱歉 - BPL = 47 是每行的字节数,包括 \n EOL 字符; 8588226 是文件中的总行数 - 因此我知道需要多少内存来存储数据。 - Chris
Python在输入和输出方面非常慢。进行性能分析,但你可能会发现这是瓶颈所在。唯一的解决方法是使用不同的语言(例如C语言)。 - Alex Reynolds
显示剩余6条评论
6个回答

3

以下是一些要点:

  1. 你的C程序在作弊;它被告知了文件大小,并且正在预分配...

  2. Python:考虑使用array.array('d')...一个用于S,一个用于nu。然后尝试预分配。

  3. Python:将您的例程编写为函数并调用它--访问函数本地变量比访问模块全局变量快得多。


1
谢谢 - 我认为你提到的第二点是为什么读取预分配的numpy数组让我只用了20秒的原因。将其作为函数调用,我们可以缩短到17.3秒。有人能给出如何实现迭代器方法的指针吗? - Chris

2
在C语言中,你可以尝试使用低级别的系统调用open()/read()/close()替换fopen()/fread()/fclose()库函数。这样做可能会加快速度,因为fread()进行了大量缓冲,而read()则没有。
此外,使用更大的块较少调用read()将减少系统调用的数量,因此你将在用户空间和内核空间之间切换次数较少。当你发出read()系统调用时,无论是从fread()库函数调用还是其他方式调用,内核都会从磁盘读取数据,然后将其复制到用户空间。如果在代码中非常频繁地发出系统调用,则复制部分变得很昂贵。通过读取更大的块,你将减少上下文切换和复制。
但要记住,read()不能保证返回您想要的确切字节数的块。这就是为什么在可靠和正确的实现中,您始终必须检查read()的返回值。

我不明白。首先,您建议通过从fread()/...函数更改为read()/...函数来避免使用缓冲区。然后,您建议通过读取更大的数据块来使用缓冲区。在我看来,您建议避免自动缓冲,并实际上使用手动缓冲以及所有可能存在于错误实现中的错误。 - Didier Trosset
一个正确的实现总是会检查 read() 实际读取的字节数。重点是,使用 47 字节内存块调用 read(2) 的效率远不如使用 1024 字节块。 - Blagovest Buyukliev
1
当然可以,但这正是你使用fread()的原因 - 它将使用更大的值调用read()并为您进行缓冲。 - caf

2

一种可能适用于C、C++和Python版本的方法是使用内存映射文件。最重要的好处是可以减少数据被从一个缓冲区复制到另一个缓冲区时的双重处理量。在许多情况下,由于I/O系统调用数量的减少,也会带来其他好处。


+1;内存映射可以通过Java本地方式和Fortran通过C绑定实现。 - Christoph

0

fread()中,您把1BPL参数搞反了(这样可能会读取部分行,而您没有测试)。在尝试使用返回的数据之前,您还应该测试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);

-2

另一个可能的加速方法,考虑到需要执行的次数,是使用指向S和nu的指针,而不是索引数组,例如:

double *pS = S, *pnu = nu;
...
*pS++ = atof(sp);
*pnu = atof(sp);
...

此外,由于您总是在buf的相同位置从char转换为double,请在循环之外预先计算地址,而不是每次在循环中计算它们。

2
请不要在测量时执行此操作。这样做不利于可读性,并且通常对性能没有任何影响。 - Didier Trosset
谢谢你的回答 - 但是当我这样做时,我会得到一个分段错误? 另外,如果BPL被定义了,编译器不会在执行之前为我进行预计算吗? - Chris
我认为大多数现代编译器都会自动执行这些优化。因此,Didier的评论是正确的。 - torak

-2

除非绝对必要,否则不要涉及C语言。为了模拟OP的需求,我有一个方便的三列ASCII文本文件,以'='作为分隔符,而不是' '

跨越近7.6 GiB148百万行

  • 这只是一个我用于代码验证检查的大型素数文件

  • 这三列分别是素数的以2为底的对数,保留5位小数,素数本身的ASCII十进制数字,以及该值的十六进制表示。

将同一文件导入三次,得到了约444百万行的合成输入(接近OP提到的500百万目标),并使用awk收集了一些基本统计信息 - (对第一列求和作为浮点数值,以及对第二列和第三列进行按列宽分组的计数)


awk处理了一个444万行22.8 GiB的合成纯文本文件,没有预先制作的索引,仅用了78.26秒。以相同速度处理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
}'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接