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

时间:2018-04-24 13:09:55

标签: c++ performance csv split

我需要阅读许多大的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 :: split,boost :: tokenizer和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

下面请找一种文件的样本内容,我有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)并使用split来标记每一行, 任何类似于getline for string的函数?例如getline_str?以便 我可以直接读到字符串

5如何将整个文件读入一个字符串,然后将整个字符串拆分为向量&#39; \ n&#39;然后将每个字符串拆分为&#39;,&#39;处理?这会表现得更好吗?字符串的限制(最大尺寸)是多少?

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

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

直接读入矢量?怎么做?

非常感谢。

Regds

林志峰

6 个答案:

答案 0 :(得分:2)

每当你不得不关心表现时,尝试替代方案并衡量他们的表现是件好事。有些人帮助实现您在下面的问题中提出的一个选项....

给出您想要阅读的每个结构,例如您的示例......

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

...您可以使用fscanf阅读和解析字段。不幸的是,它是一个不支持std::string的C库函数,所以你需要为每个字符串字段创建一个创建字符数组缓冲区,然后从那里复制到你的结构中#39 ; s领域。全部,像:

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);

(只需在格式字符串中输入正确数量的%d并完成其他ItRemain索引。

另外,我不情愿推荐它,因为它可能会遇到更高级的编程,但是映射文件和编写自己的解析的内存很有可能比{{ 1}}上面的方法(但同样,你知道,直到它在你的硬件上测量)。如果你是一个科学家试图做一些严肃的事情,可能会与专业的程序员配对,为你完成这件事。

答案 1 :(得分:1)

尝试制作快速输入例程时的一个基本考虑因素是避免多次从文件中读取和处理每个字符。当转换为数值时,这是不可能的,因为转换例程将重新扫描字符,但总的来说就是目标。您还应该尝试限制函数调用的数量和尽可能多的开销。当操作大于16-32个字符的字段时,字符串和转换函数优化几乎总是优于您自己编写的字段,但对于较小的字段 - 这并不总是正确的。

就缓冲区大小而言,C / C ++库将提供从gcc源中的IO_BUFSIZ派生的默认读缓冲区。该常量在C / C ++中以BUFSIZ形式提供。 (gcc8192个字节,VS cl.exe512个字节),因此从文件读取时,I / O函数将为{{1可以使用的字符而无需返回磁盘。你可以利用这个优势。因此,无论您是一次处理一个字符,还是从文件读取到100k大小的缓冲区,磁盘I / O调用的数量都是相同的。 (这有点违反直觉)

读入缓冲区,然后调用BUFSIZstrtok是有效的,但是当试图从读取中获取每一点速度时,两者都涉及遍历已经读过的字符,至少,第二次,并且条件和检查都提供 - 你可以做得更好。

我全心全意地同意Tony的回答,您只需尝试不同的方法,对每个方法进行计时,以确定哪种组合最适合您的数据。

在查看您的数据时,将sscanf char缩短为labelfloat混合int或更少的值到最后对于每条记录,我想到的一个优化就是简单地处理标签,然后将剩余的值视为1000。整数的float表示将精确地超出您的值的范围,因此您基本上可以以简化形式处理读取和转换(和存储)。

假设您不知道您拥有的记录数量,以及每个float后面的字段数量,您需要从相当通用的读取开始,根据需要动态分配存储记录。并且,对于第一个记录,在确定每个记录中的字段数之前,为可能需要的字段分配存储 - 从那时起,您可以为每个记录分配确切数量的字段 - 并验证每条记录的字段数是否相同。

由于您正在寻找速度,因此读取和分配存储的简单C例程可以提供C ++实现的优势(它肯定会最小化存储分配)。

作为第一次尝试,我会使用label函数(例如character-oriented依赖于基础fgetc读取缓冲区来读取文件,以有效地处理磁盘I / O ,然后简单地编写一个 state-loop 来将每条记录中的值解析为BUFSIZ进行存储。

您测试和与其他例程进行比较的简短示例将类似于以下内容。如果您在Unix / Linux机器上,可以使用stuct进行纳秒计时,在Windows上,您将需要clock_gettime进行微秒计时。读取例程本身可以是:

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;
}

这可能会或可能不会比您使用的速度快得多,但值得进行比较 - 因为我怀疑它可能会有所改善。

答案 2 :(得分:1)

最终我使用内存映射文件来解决我的问题,性能比我使用fscanf要好得多 由于我在MS Windows上工作,所以我使用Stephan Brumme的“Portable Memory Mapping C ++ Class” http://create.stephan-brumme.com/portable-memory-mapping/ 因为我不需要处理文件&gt; 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)! (我在其中编码CSV格式,只从每行中获取4个值)。 感谢所有人的帮助,你们都激励我解决这个问题。

答案 3 :(得分:0)

主要是你想避免复制。

如果你能负担得起内存将整个文件加载到一个数组中,那么就直接使用那个数组,不要将它转换回字符串流,因为这会产生另一个副本,只需要处理大缓冲区!

另一方面,这需要你的机器为你的分配释放足够的RAM,并且可能将一些RAM写入磁盘,这将很慢处理。另一种方法是以大块加载文件,识别该块中的行,并仅在块的末尾复制部分行,然后加载文件的下一部分以连接到该部分行(换行和读取) )。

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

您可以使用strtok_r等方法将文件拆分为行,将行划分为字段,但需要处理转义字段标记 - 无论如何都需要这样做。可以编写一个像strtok一样工作的tokeniser,但返回类似string_view的范围,而不是实际插入空字节。

最后,您可能需要将某些字段字符串转换为数字形式或以其他方式解释它们。理想情况下不要使用istringstream,因为它会生成字符串的另一个副本。如果你必须,或许可以制作你自己的streambuf直接使用string_view,并将它附加到istream?

因此,这应该会显着减少正在进行的数据复制,并且应该看到加速。

请注意,您只能直接访问文件读取窗口中的字段和行。当你包装并阅读任何参考文献时,你的数据都是无用的垃圾。

答案 4 :(得分:0)

  

1为什么读取整个文件的性能会比整体读取的差   文件?

三个字:locality of reference

现代CPU的片上操作非常快,在许多情况下,程序执行所需的CPU周期数对程序的整体性能影响非常小。相反,通常完成任务所花费的时间大部分或完全取决于RAM子系统向CPU提供数据的速度,或者(甚至更糟)硬盘可以向RAM子系统提供数据的速度

计算机设计人员试图隐藏CPU速度和RAM速度之间的巨大差异(以及RAM速度和磁盘速度之间的巨大差异)通过缓存;例如,当一个CPU首先想要访问特定的4kB RAM页面上的数据时,它需要在数据交付之前的很长一段时间内坐下并转动它(大概是CPU)从RAM到CPU。但是在第一次痛苦的等待之后,第二次CPU访问RAM内同一页面内的附近数据的速度非常快,因为此时页面缓存在CPU的片上高速缓存中,CPU不再存在必须等待它交付。

但CPU的片上缓存(相对)很小 - 远远不足以容纳整个100 + MB文件。因此,当你将一个巨大的文件加载到RAM中时,你强迫CPU在大面积的内存中进行两次传递 - 第一次读取所有数据,然后在你回到解析时再次传递所有数据。

假设您的程序RAM带宽有限(并且对于这个简单的解析任务肯定应该是这样),这意味着对数据进行两次扫描所需的时间大约是在单次扫描中执行所有内容的两倍。

  

2还有其他更好的字符串拆分功能吗?

我总是喜欢strtok(),因为你可以非常自信它不会做任何低效的事情(比如你背后的电话malloc()/ free())。或者如果你想变得非常疯狂,你可以使用char *指针和for循环来编写自己的迷你解析器,尽管我怀疑它最终会明显快于基于strtok()的速度。无论如何都要循环。

  

3 ReadinFile函数需要读取缓冲区然后写入a   stringstream要处理,任何避免这种情况的方法?即直接进入   字符串流

我说只需放一会儿() - 循环fgets(),然后在每次调用fgets()之后读取一行CSV文本,就有一个内在的时间( )-loop strtok()以解析该行内的字段。为了最大限度地提高效率,使用老式的C风格I / O很难出错。

  

5如何将整个文件读入字符串然后拆分整个文件   将字符串转换为带有&#39; \ n&#39;然后用向量分割向量中的每个字符串   &#39;,&#39;处理?这会表现得更好吗?什么是限制(最大   大小)字符串?

我严重怀疑你会做得更好。字符串类并非真正设计为在多兆字节字符串上有效运行。

  

6或者我应该定义这样的结构(基于格式)[...]并直接读入矢量?怎么做?

是的,这是一个好主意 - 如果你能在一次通过中完成所有事情,你将会提前,效率明智。您应该能够声明(例如)vector<struct MyStruct>并且对于您在文件中解析的每一行,在解析它们时将解析后的值写入MyStruct对象(例如atoi() }),然后在MyStruct对象完全填充/写入后,push_back(myStruct)到向量的末尾。

(唯一比那更快的事情就是摆脱vector<struct MyStruct>,并且只需要(无论你需要做什么)在解析循环中使用数据,而不必费心将整个数据集存储在一个大的向量中。这可能是一个选项,例如,如果你只需要计算每个字段中所有项目的总和,但OTOH可能不适用于你的用例)

答案 5 :(得分:0)

您需要的是内存映射。

您可以找到更多here