读大CSV文件中的性能问题在C ++

问题描述 投票:2回答:6

我需要在许多大城市CSV文件中读取在C ++处理(数百MB的范围从几MB)首先,我用fstream的打开,使用函数getline来读取每一行,并用下面的函数来分割每行”

template < class ContainerT >
void split(ContainerT& tokens, const std::string& str, const std::string& delimiters = " ", bool trimEmpty = false)
{
std::string::size_type pos, lastPos = 0, length = str.length();

using value_type = typename ContainerT::value_type;
using size_type = typename ContainerT::size_type;

while (lastPos < length + 1)
{
    pos = str.find_first_of(delimiters, lastPos);
    if (pos == std::string::npos)
    {
        pos = length;
    }

    if (pos != lastPos || !trimEmpty)
        tokens.push_back(value_type(str.data() + lastPos,
        (size_type)pos - lastPos));

    lastPos = pos + 1;
}
}

我试过的boost ::分裂,促进::标记生成器和boost :: Sprint和发现上面提供最佳性能为止。在那之后,我认为阅读整个文件到内存来处理,而不是保持打开的文件,我用下面的功能与下面的函数整个文件如下:

void ReadinFile(string const& filename, stringstream& result)
{
ifstream ifs(filename, ios::binary | ios::ate);
ifstream::pos_type pos = ifs.tellg();

//result.resize(pos);
char * buf = new char[pos];
ifs.seekg(0, ios::beg);
ifs.read(buf, pos);
result.write(buf,pos);
delete[]buf;

}

这两个功能是从净地方复制。然而,我发现,有没有打开或整个文件的读写文件之间的性能太大的区别。表演捕捉如下:

Process 2100 files with boost::split (without read in whole file) 832 sec
Process 2100 files with custom split (without read in whole file) 311 sec
Process 2100 files with custom split (read in whole file) 342 sec

下面请找出一种类型的文件(S)的样品含量,我有6种需要处理。但是,所有的都是相似的。

a1,1,1,3.5,5,1,1,1,0,0,6,0,155,21,142,22,49,1,9,1,0,0,0,0,0,0,0
a1,10,2,5,5,1,1,2,0,0,12,0,50,18,106,33,100,29,45,9,8,0,1,1,0,0,0
a1,19,3,5,5,1,1,3,0,0,18,0,12,12,52,40,82,49,63,41,23,16,8,2,0,0,0
a1,28,4,5.5,5,1,1,4,0,0,24,0,2,3,17,16,53,53,63,62,43,44,18,22,4,0,4
a1,37,5,3,5,1,1,5,0,0,6,0,157,22,129,18,57,11,6,0,0,0,0,0,0,0,0
a1,46,6,4.5,5,1,1,6,0,0,12,0,41,19,121,31,90,34,37,15,6,4,0,2,0,0,0
a1,55,7,5.5,5,1,1,7,0,0,18,0,10,9,52,36,86,43,67,38,31,15,5,7,1,0,1
a1,64,8,5.5,5,1,1,8,0,0,24,0,0,3,18,23,44,55,72,57,55,43,8,19,1,2,3
a1,73,9,3.5,5,1,1,9,1,0,6,0,149,17,145,21,51,8,8,1,0,0,0,0,0,0,0
a1,82,10,4.5,5,1,1,10,1,0,12,0,47,17,115,35,96,36,32,10,8,3,1,0,0,0,0

我的问题是:

1,为什么在整个文件中读出比整个文件无法读取会表现更差?

2任何其他更好的字符串分割功能?

3 ReadinFile功能需要读取到缓冲器,然后写入到一个字符串流来处理,任何方法来避免这种情况?即直接进入stringstream的

4我需要使用函数getline解析每一行(以\ n)和使用分割来标记每一行,对于函数getline字符串相似任何功能?例如getline_str?这样我就可以直接读入字符串

5如何读取整个文件转换成字符串,然后整个字符串分割成以“\ n”载体中,然后用向量分割每个字符串“”来处理?这会不会有更好的表现?什么是极限字符串(最大尺寸)?

6或者我应该这样定义一个结构(基于格式)

struct MyStruct {
  string Item1;
  int It2_3[2];
  float It4;
  int ItRemain[23];
};

并直接读入一个载体?这个怎么做 ?

非常感谢。

Regds

LAM志锋

c++ performance csv split
6个回答
2
投票

只要你有关心的性能,这是很好的尝试选择和衡量他们的表现。一些帮助您实现以下你的问题问一个选项....

鉴于你想读,每个结构,如你的榜样?

struct MyStruct {
  string Item1;
  int It2_3[2];
  float It4;
  int ItRemain[23];
};

...你可以阅读和使用fscanf解析等领域。不幸的是,这是一个C库函数不支持std::strings,所以你需要为每个字符串字段创建一个字符数组缓冲区,然后从那里复制到结构的领域。所有了,是这样的:

char Item1[4096];
MyStruct m;
std::vector<MyStruct> myStructs;
FILE* stream = fopen(filename, "r");
assert(stream);
while (fscanf(stream, "%s,%d,%d,%f,%d,%d,%d,%d...",
              Item1, &m.It2_3[0], &m.It2_3[1], &m.It4,
              &m.ItRemain[0], &m.ItRemain[1], &m.ItRemain[2], ...) == 27)
{
    myStructs.push_back(m);
    myStructs.back().Item1 = Item1;  // fix the std::strings
}
fclose(stream);

(只是把%ds正确数量的格式字符串,并完成其他ItRemain指数)。


另外,我relucatant推荐它,因为它是你可能会挣扎更高级的编程,但内存映射文件,写你自己的分析有被数倍于上述fscanf方法的好机会(但同样,你不会知道,直到它在你的硬件测量)。如果你试图做一些严肃的科学家,也许有一个专业的程序员来完成这为您完成配对。


1
投票

想制作一个快速输入程序时的一个基本考虑是避免读取和处理从文件中的每个字符不止一次。诚然这个转换为数值作为转换程序将重新扫描字符时是不可能的,但总的来说,这是目标。你也应该尝试和限制的函数调用和尽可能多的开销尽可能的数量。当处理领域超过16-32个字符的串并转换功能的优化将几乎总是胜过你写你自己的东西,但对于较小的领域 - 这并非总是如此。

至于缓冲器大小得好,C / C ++库将提供一个默认读取从IO_BUFSIZ在gcc的来源缓冲器。所述常数可作为BUFSIZ在C / C ++。 (与gcc8192字节,VS cl.exe512字节),所以从文件读取时,I / O功能将有可供使用BUFSIZ字符,而不必返回到磁盘。你可以用你的优势。所以,不管你是在一次处理一个字符,或者从文件中读取到一个100K大小的缓冲区,磁盘I / O调用次数是相同的。 (这是一个位反直觉)

读入缓冲区,然后调用strtoksscanf是有效的,但试图伊克速度的每一点从你读的时候,都涉及穿越您已经阅读人物,至少,第二次,并用条件语句和支票都提供了 - 你可以做的更好一点。

我同意托尼的回答全情投入,你只需要尝试不同的方法,每一个时间,以确定哪些组合将最适合您的数据。

在查看数据,是一个短char label然后混合floatint值到每个记录的末尾(1000或更小的),一个优化想到的是简单地处理的标签,然后把剩下的值作为float。整数的float表示是准确的在你的值的范围,所以你基本上可以处理的简化形式的读取和转换(和存储)。

假设你不知道你有记录的数目,也不是你有以下各label字段的数量,你需要开始用一种很普通的读,将动态的记录分配存储空间,如需要,而且,第一个记录,分配尽可能多的字段存储可能需要的,直到已经确定在每个记录中的字段的数目 - 从该点上,可以分配为每个记录中的字段的确切数目 - 和验证,每个记录包含相同的字段数。

既然你正在寻找的速度,一个简单的C程序中读取和分配存储可以提供C ++实现的优势(这当然会最大限度地降低存储分配)。

作为第一个尝试,我将接近该文件的读出与像character-oriented依靠fgetc函数底层BUFSIZ读缓冲器以有效地处理磁盘I / O,然后简单地写的状态下循环,以解析来自每个值记录到存储一个stuct

简单例子给你测试,并与其他程序比较将类似于以下。如果你是在Unix / Linux中,你可以使用clock_gettime纳秒时间,在Windows,你需要为QueryPerformanceCounter微秒时间。读程序本身可能是:

#include <stdio.h>
#include <stdlib.h>     /* for calloc, strtof */
#include <string.h>     /* for memset */
#include <errno.h>      /* strtof validation */

#define LABEL      3    /* label length (+1 for nul-character */
#define NRECS      8    /* initial number of records to allocate */
#define NFLDS  NRECS    /* initial number of fields to allocate */
#define FLDSZ     32    /* max chars per-field (to size buf) */

typedef struct {
    char label[LABEL];  /* label storage */
    float *values;      /* storage for remaining values */
} record_t;

/* realloc function doubling size allocated */
void *xrealloc (void *ptr, size_t psz, size_t *nelem);

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

    int lblflag = 1, n = 0; /* label flag, index for buf */
    size_t col = 0,         /* column index */
           idx = 0,         /* record index */
           ncol = 0,        /* fixed number of cols - 1st rec determines */
           nflds = NFLDS,   /* tracks no. of fields allocated per-rec */
           nrec = NRECS;    /* tracks no. of structs (recs) allocated */
    char buf[FLDSZ] = "";   /* fixed buffer for field parsing */
    record_t *rec = NULL;   /* pointer to record_t structs */
    FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin; /* file or stdin */

    if (!fp) {  /* validate file open for reading */
        fprintf (stderr, "error: file open failed '%s'.\n", argv[1]);
        return 1;
    }

    /* allocate/validate initial storage for nrec record_t */
    if (!(rec = calloc (nrec, sizeof *rec))) {
        perror ("calloc-rec");
        return 1;
    }

    /* allocate/validate initial storage for nflds values */
    if (!(rec[idx].values = calloc (nflds, sizeof *rec[idx].values))) {
        perror ("calloc-rec[idx].values");
        return 1;
    }

    for (;;) {                          /* loop continually until EOF */
        int c = fgetc (fp);             /* read char */
        if (c == EOF)                   /* check EOF */
            break;
        if (c == ',' || c == '\n') {    /* field separator or \n reached */
            char *p = buf;              /* ptr for strtof validation */
            buf[n] = 0;                 /* nul-terminate buf */
            n = 0;                      /* reset buf index zero */
            if (!lblflag) {             /* not lblflag (for branch prediction) */
                errno = 0;              /* zero errno */
                rec[idx].values[col++] = strtof (buf, &p);  /* convert buf */
                if (p == buf) {     /* p == buf - no chars converted */
                    fputs ("error: no characters converted.\n", stderr);
                    return 1;
                }
                if (errno) {        /* if errno - error during conversion */
                    perror ("strof-failed");
                    return 1;
                }
                if (col == nflds && !ncol)  /* realloc cols for 1st row a reqd */
                    rec[idx].values = xrealloc (rec[idx].values, 
                                            sizeof *rec[idx].values, &nflds);
            }
            else {                      /* lblflag set */
                int i = 0;
                do {    /* copy buf - less than 16 char, loop faster */
                    rec[idx].label[i] = buf[i];
                } while (buf[i++]);
                lblflag = 0;            /* zero lblflag */
            }
            if (c == '\n') {        /* if separator was \n */
                if (!ncol)          /* 1st record, set ncol from col */
                    ncol = col;
                if (col != ncol) {  /* validate remaining records against ncol */
                    fputs ("error: unequal columns in file.\n", stderr);
                    return 1;
                }
                col = 0;            /* reset col = 0 */
                lblflag = 1;        /* set lblflag 1 */
                idx++;              /* increment record index */
                if (idx == nrec)    /* check if realloc required */
                    rec = xrealloc (rec, sizeof *rec, &nrec);
                /* allocate values for next record based on now set ncol */
                if (!(rec[idx].values = calloc (ncol, sizeof *rec[idx].values))) {
                    perror ("calloc-rec[idx].values");
                    return 1;
                }
            }
        }
        else if (n < FLDSZ) /* normal char - check index will fit */
            buf[n++] = c;   /* add char to buf */
        else {  /* otherwise chars exceed FLDSZ, exit, fix */
            fputs ("error: chars exceed FLDSZ.\n", stdout);
        }
    }
    if (fp != stdin) fclose (fp);   /* close file if not stdin */
    /* add code to handle last field on non-POSIX EOF here */
    if (!*rec[idx].label) free (rec[idx].values);  /* free unused last alloc */

    printf ("records: %zu   cols: %zu\n\n", idx, ncol); /* print stats */

    for (size_t i = 0; i < idx; i++) {      /* output values (remove) */
        fputs (rec[i].label, stdout);
        for (size_t j = 0; j < ncol; j++)
            printf (" %3g", rec[i].values[j]);
        free (rec[i].values);               /* free values */
        putchar ('\n');
    }
    free (rec);     /* free structs */

    return 0;
}

/** realloc 'ptr' of 'nelem' of 'psz' to 'nelem * 2' of 'psz'.
 *  returns pointer to reallocated block of memory with new
 *  memory initialized to 0/NULL. return must be assigned to
 *  original pointer in caller.
 */
void *xrealloc (void *ptr, size_t psz, size_t *nelem)
{   void *memptr = realloc ((char *)ptr, *nelem * 2 * psz);
    if (!memptr) {
        perror ("realloc(): virtual memory exhausted.");
        exit (EXIT_FAILURE);
    }   /* zero new memory (optional) */
    memset ((char *)memptr + *nelem * psz, 0, *nelem * psz);
    *nelem *= 2;
    return memptr;
}

实施例使用/输出

$ ./bin/readlargecsvbuf dat/large.csv
records: 10   cols: 26

a1   1   1 3.5   5   1   1   1   0   0   6   0 155  21 142  22  49   1   9   1   0   0   0   0   0   0   0
a1  10   2   5   5   1   1   2   0   0  12   0  50  18 106  33 100  29  45   9   8   0   1   1   0   0   0
a1  19   3   5   5   1   1   3   0   0  18   0  12  12  52  40  82  49  63  41  23  16   8   2   0   0   0
a1  28   4 5.5   5   1   1   4   0   0  24   0   2   3  17  16  53  53  63  62  43  44  18  22   4   0   4
a1  37   5   3   5   1   1   5   0   0   6   0 157  22 129  18  57  11   6   0   0   0   0   0   0   0   0
a1  46   6 4.5   5   1   1   6   0   0  12   0  41  19 121  31  90  34  37  15   6   4   0   2   0   0   0
a1  55   7 5.5   5   1   1   7   0   0  18   0  10   9  52  36  86  43  67  38  31  15   5   7   1   0   1
a1  64   8 5.5   5   1   1   8   0   0  24   0   0   3  18  23  44  55  72  57  55  43   8  19   1   2   3
a1  73   9 3.5   5   1   1   9   1   0   6   0 149  17 145  21  51   8   8   1   0   0   0   0   0   0   0
a1  82  10 4.5   5   1   1  10   1   0  12   0  47  17 115  35  96  36  32  10   8   3   1   0   0   0   0

这可能会或可能不会比你用的是什么显著快,但它是值得的比较 - 我怀疑它可能提供一点改进。


1
投票

最后,我使用内存映射文件来解决我的问题,表现远好于我使用的fscanf因为我在MS Windows上工作,所以我用斯蒂芬Brumme的“便携式存储器映射C ++类” http://create.stephan-brumme.com/portable-memory-mapping/因为我没有需要处理的文件( S)> 2 GB,我实现更简单。对于超过2GB的文件,请访问网站,看如何处理。

下面就请找我的代码:

// may tried RandomAccess/SequentialScan
MemoryMapped MemFile(FilterBase.BaseFileName, MemoryMapped::WholeFile, MemoryMapped::RandomAccess);

// point to start of memory file
char* start = (char*)MemFile.getData();
// dummy in my case
char* tmpBuffer = start;

// looping counter
uint64_t i = 0;

// pre-allocate result vector
MyVector.resize(300000);

// Line counter
int LnCnt = 0;

//no. of field
int NumOfField=43;
//delimiter count, num of field + 1 since the leading and trailing delimiter are virtual
int DelimCnt=NoOfField+1;
//Delimiter position. May use new to allocate at run time
// or even use vector of integer
// This is to store the delimiter position in each line
// since the position is relative to start of file. if file is extremely
// large, may need to change from int to unsigner, long or even unsigned long long
static  int DelimPos[DelimCnt];

// Max number of field need to read usually equal to NumOfField, can be smaller, eg in my case, I only need 4 fields
// from first 15 field, in this case, can assign 15 to MaxFieldNeed
int MaxFieldNeed=NumOfField;
// keep track how many comma read each line
int DelimCounter=0;
// define field and line seperator
char FieldDelim=',';
char LineSep='\n';

// 1st field, "virtual Delimiter" position
DelimPos[CommaCounter]=-1
DelimCounter++;

// loop through the whole memory field, 1 and only once
for (i = 0; i < MemFile.size();i++)
{
  // grab all position of delimiter in each line
  if ((MemFile[i] == FieldDelim) && (DelimCounter<=MaxFieldNeed)){
    DelimPos[DelimCounter] = i;
    DelimCounter++;
  };

  // grab all values when end of line hit
  if (MemFile[i] == LineSep) {
    // no need to use if (DelimCounter==NumOfField) just assign anyway, waste a little bit
    // memory in integer array but gain performance 
    DelimPos[DelimCounter] = i;
    // I know exactly what the format is and what field(s) I want
    // a more general approach (as a CSV reader) may put all fields
    // into vector of vector of string
    // With *EFFORT* one may modify this piece of code so that it can parse
    // different format at run time eg similar to:
    // fscanf(fstream,"%d,%f....
    // also, this piece of code cannot handle complex CSV e.g.
    // Peter,28,157CM
    // John,26,167CM
    // "Mary,Brown",25,150CM
    MyVector.StrField = string(strat+DelimPos[0] + 1, strat+DelimPos[1] - 1);
    MyVector.IntField = strtol(strat+DelimPos[3] + 1,&tmpBuffer,10);
    MyVector.IntField2 = strtol(strat+DelimPos[8] + 1,&tmpBuffer,10);
    MyVector.FloatField = strtof(start + DelimPos[14] + 1,&tmpBuffer);
    // reset Delim counter each line
    DelimCounter=0
    // previous line seperator treat as first delimiter of next line
    DelimPos[DelimCounter] = i;
    DelimCounter++
    LnCnt++;
  }
}
MyVector.resize(LnCnt);
MyVector.shrink_to_fit();
MemFile.close();
};

有了这一段代码,我把手57秒2100页的文件(6.3 GB)! (I在它编码CSV格式和只抓住从每行4个值)。 THX所有的人的帮助下,你都激励我在solveing这个问题。


0
投票

主要是要避免复制。

如果你能负担得起的内存整个文件加载到一个数组,然后直接使用数组,不将其转换回一个字符串流,因为那让另一个副本,只是处理大的缓冲区!

在另一方面,需要你的机器腾出你的配置足够的RAM,并可能页一些RAM磁盘,这将是缓慢的过程。另一种方法是加载文件大块,识别在该块的线的上下部分线在所述块的端部加载文件的下一部分来连接到该部分线(一涡卷和读取之前只拷贝)。

另一种选择是,大多数操作系统提供一个内存映射文件视图,这意味着操作系统不会文件复制给你。这些更多的限制(必须使用固定的块大小和偏移量),但会更快。

您可以使用像strtok_r方法将文件分成行,并线分成领域,虽然你需要处理逃脱场标志 - 你需要做的反正。它是可以编写就像strtok的,但返回string_view样的范围,而不是实际插入空字节的tokeniser。

最后,你可能需要一些领域的字符串转换为数字形式或以其他方式对其进行解释。理想的情况是不使用istringstream,因为这使得该字符串的另一个副本。如果必须,也许手艺自己流缓冲直接使用string_view,并将其连接到一个IStream?

因此,这应该显著减少数据复制的事情量,应该看到一个加速。

要知道,你只能直接访问您的文件会的字段和线读取窗口。当您缠绕和阅读您有任何引用到的数据是无用的垃圾。


0
投票

1,为什么在整个文件中读出比整个文件无法读取会表现更差?

三个字:locality of reference

片现代CPU的操作是可笑的快,到在很多情况下,一个程序需要执行的CPU周期数只对程序的整体性能影响很小的点。相反,往往需要完成任务的时间大部分或全部由该内存子系统可以提供数据给CPU,或者(更糟糕)的速度,硬盘可以提供数据到内存子系统的速度决定。

计算机设计者试图掩盖CPU的速度和RAM的速度(和RAM的速度和磁盘速度的进一步巨头差异)通过缓存之间的差异巨大;例如,当CPU首先要访问的RAM特定的4kB页上的数据,这将有坐下来玩弄其大拇指的(什么似乎对CPU)很长一段时间之前,将数据从RAM传递到中央处理器。但是,第一次痛苦的等待之后,第二个CPU-访问的RAM同一页面内附近的数据将是相当快的,因为在该点的页面缓存内的CPU的片上高速缓存和CPU不再需要等待它被传递。

但CPU的片上高速缓存的(相对)小 - 远不足够大,以适应整个100 + MB的文件。所以,当你加载一个巨大的文件到内存中,你迫使CPU在整个大面积的内存做两遍 - 第一遍阅读中的所有数据,而当你回到解析所有当时的第二传数据。

假设你的程序是内存带宽限制(而这个简单的解析任务,绝对应该是),这意味着大约两倍长跨数据两次扫描将作为一个单一的扫描中所做的一切。

2任何其他更好的字符串分割功能?

我一直有种很喜欢strtok(),因为你可以相当有信心,它不会做任何事情效率低下(如调用malloc()/ free()的)背着你。或者,如果你想获得真正的疯狂,你可以写使用循环一个char *指针和你自己的迷你解析器,但我怀疑这将最终被明显快于基于strtok()环反正。

3 ReadinFile功能需要读取到缓冲器,然后写入到一个字符串流来处理,任何方法来避免这种情况?即直接进入stringstream的

我想说的只是把一段时间() - 环周围q​​azxswpoi,然后每次调用fgets()在一条线上CSV文本的阅读了之后,有一个内部的while() - 周围fgets()循环解析出该行中的字段。为了获得最大的效率,这是很难去错老式的C风格的I / O。

5如何读取整个文件转换成字符串,然后整个字符串分割成以“\ n”载体中,然后用向量分割每个字符串“”来处理?这会不会有更好的表现?什么是极限字符串(最大尺寸)?

我严重怀疑你会得到更好的性能这样做。 String类是不是真的设计的多兆字节的字符串有效地运行。

6或者我应该这样定义一个结构(基于格式)[...]和直接读入载体?这个怎么做 ?

是的,这是一个好主意 - 如果你能在一个单一的通行证,你会出人头地,效率明智做的一切。你应该能够声明(例如)一strtok()和每行你在文件中解析,因为你是把它们解析解析值写入到vector<struct MyStruct>对象(例如与MyStruct),然后经过atoi()对象是完全填充/写入到,MyStruct到矢量的末端。

(比更快的唯一的事情将是摆脱push_back(myStruct)的为好,只是做(不管它是什么,你需要做的)与数据在那里您解析环路内,而不会打扰到存储整个数据集一个大的载体都没有。这可能是一个选项,例如,如果你只需要计算在各个领域的所有项目的总和,但OTOH它可能不适合您的使用情况是可能的)


0
投票

你需要的是内存映射。

你可以找到更多vector<struct MyStruct>

© www.soinside.com 2019 - 2024. All rights reserved.