非常快的文本文件处理(C++)

11

我写了一个在GPU上处理数据的应用程序。代码能够正常运行,但是读取输入文件的部分(大约3GB,文本)是我应用程序的瓶颈。(从硬盘读取很快,但逐行处理缓慢)。

我使用getline()读取一行并将第1行复制到一个vector中,将第2行复制到另一个vector中,跳过第3和第4行。接下来对剩下的1100万行做同样的操作。

我尝试了几种方法来以最快的时间获取文件:

我发现最快的方法是使用boost::iostreams::stream

其他方法包括:

  • 以gzip格式读取文件,以最小化IO,但比直接读取要慢。
  • 使用read(filepointer, chararray, length)将文件复制到RAM中,并使用循环区分行(也比boost慢)

有什么建议可以使它运行得更快吗?

void readfastq(char *filename, int SRlength, uint32_t blocksize){
    _filelength = 0; //total datasets (each 4 lines)
    _SRlength = SRlength; //length of the 2. line
    _blocksize = blocksize;

    boost::iostreams::stream<boost::iostreams::file_source>ins(filename);
    in = ins;

    readNextBlock();
}


void readNextBlock() {
    timeval start, end;
    gettimeofday(&start, 0);

    string name;
    string seqtemp;
    string garbage;
    string phredtemp;

    _seqs.empty();
    _phred.empty();
    _names.empty();
    _filelength = 0;

            //read only a part of the file i.e the first 4mio lines
    while (std::getline(in, name) && _filelength<_blocksize) {
        std::getline(in, seqtemp);
        std::getline(in, garbage);
        std::getline(in, phredtemp);

        if (seqtemp.size() != _SRlength) {
            if (seqtemp.size() != 0)
                printf("Error on read in fastq: size is invalid\n");
        } else {
            _names.push_back(name);

            for (int k = 0; k < _SRlength; k++) {

                //handle special letters
                                    if(seqtemp[k]== 'A') ...
                                    else{
                _seqs.push_back(5);
                                    }

            }
            _filelength++;
        }
    }

编辑:

源文件可在https://docs.google.com/open?id=0B5bvyb427McSMjM2YWQwM2YtZGU2Mi00OGVmLThkODAtYzJhODIzYjNhYTY2 下载。

由于存在一些指针问题,我更改了函数 readfastq以读取文件。因此,如果调用readfastq,则 blocksize(按行计的块大小)必须大于要读取的行数。

解决方案:

我找到了一个解决方案,将读取文件的时间从60秒减少到16秒。我删除了内部循环处理特殊字符,并在GPU上处理该操作。这样可以减少读入时间,并仅对GPU运行时间进行轻微增加。

感谢您的建议。

void readfastq(char *filename, int SRlength) {
    _filelength = 0;
    _SRlength = SRlength;

    size_t bytes_read, bytes_expected;

    FILE *fp;
    fp = fopen(filename, "r");

    fseek(fp, 0L, SEEK_END); //go to the end of file
    bytes_expected = ftell(fp); //get filesize
    fseek(fp, 0L, SEEK_SET); //go to the begining of the file

    fclose(fp);

    if ((_seqarray = (char *) malloc(bytes_expected/2)) == NULL) //allocate space for file
        err(EX_OSERR, "data malloc");


    string name;
    string seqtemp;
    string garbage;
    string phredtemp;

    boost::iostreams::stream<boost::iostreams::file_source>file(filename);


    while (std::getline(file, name)) {
        std::getline(file, seqtemp);
        std::getline(file, garbage);
        std::getline(file, phredtemp);

        if (seqtemp.size() != SRlength) {
            if (seqtemp.size() != 0)
                printf("Error on read in fastq: size is invalid\n");
        } else {
            _names.push_back(name);

            strncpy( &(_seqarray[SRlength*_filelength]), seqtemp.c_str(), seqtemp.length()); //do not handle special letters here, do on GPU

            _filelength++;
        }
    }
}

1
你所说的“ram”是指PC内存还是显卡内存? - stijn
2
请注意,string::empty()vector::empty() 是对容器状态的只读测试。也许你想使用.clear()方法? - André Caron
1
可能是什么是C++中高性能顺序文件I/O的最快方法?的重复问题。 - Ben Voigt
1
_seqs.empty(); 只会返回 true。默认构造函数会创建一个空字符串,而 bool std::string::empty() constvoid std::string::clear() 是不同的。 - MSalters
2
我不知道你要解决的问题是什么,但是你确定在如此大的数据集和明显时间关键的应用程序中使用文本文件是正确的方式吗? - Gabriel Schreiber
显示剩余3条评论
5个回答

7

首先,您可以使用文件映射而非将文件读入内存进行操作。您只需将程序构建为64位以适应3GB的虚拟地址空间(对于32位应用程序,用户模式下只能访问2GB)。或者您可以分块映射和处理文件。

接下来,我认为您的瓶颈在于“复制行到向量”。处理向量涉及动态内存分配(堆操作),这在关键循环中会严重影响性能。如果是这种情况,则要么避免使用向量,要么确保它们在循环外声明。后者有所帮助,因为当您重新分配/清除向量时,它们不会释放内存。

请发布您的代码(或其中一部分)以获取更多建议。

编辑:

看起来您所有的瓶颈都与字符串管理有关。

  • std::getline(in, seqtemp); 读入 std::string 涉及动态内存分配。
  • _names.push_back(name); 这更糟糕。首先将 std::string 按值放入 vector 中。这意味着字符串被复制,因此另一个动态分配/释放发生。此外,当最终重新分配 vector 时,所有包含的字符串都会再次复制,拥有所有后果。

我建议不要使用标准格式化文件 I/O 函数(Stdio/STL)或 std::string。为了获得更好的性能,您应该使用指向字符串的指针(而非复制的字符串),这是可能的如果您映射整个文件。此外,您还需要实现文件解析(划分为行)。

就像这段代码一样:

class MemoryMappedFileParser
{
    const char* m_sz;
    size_t m_Len;

public:

    struct String {
        const char* m_sz;
        size_t m_Len;
    };

    bool getline(String& out)
    {
        out.m_sz = m_sz;

        const char* sz = (char*) memchr(m_sz, '\n', m_Len);
        if (sz)
        {
            size_t len = sz - m_sz;

            m_sz = sz + 1;
            m_Len -= (len + 1);

            out.m_Len = len;

            // for Windows-format text files remove the '\r' as well
            if (len && '\r' == out.m_sz[len-1])
                out.m_Len--;
        } else
        {
            out.m_Len = m_Len;

            if (!m_Len)
                return false;

            m_Len = 0;
        }

        return true;
    }

};

1
我不同意。我认为可以使用标准库编写得很好(但是OP没有发布足够的代码来提供上下文)。通过上述操作,您只会使代码变得非常脆弱。 - Martin York
我不知道你所说的“脆弱”是什么意思。在我看来,代码要么正确,要么不正确。但无论如何,我不会坚持己见。如果你有一个可以完成相同功能的“标准”库/函数,那么欢迎使用。 - valdo
3
易碎意味着很容易以一种会使代码不正确的方式错误地使用。当然,如果按照预期使用,它会正常工作。但是,脆弱的代码难以维护和使用,并且在原作者之后(因为只有他们知道如何正确地使用)会变得糟糕。 - Martin York
好的,如果不喜欢“脆弱”的代码,请发表您的建议,使用“标准”库来编写。 - valdo

5
如果_seqs_namesstd::vectors,并且您可以在处理整个3GB数据之前猜测它们的最终大小,那么您可以使用reserve来避免在循环中推送新元素时进行大部分内存重新分配。
需要注意的是,向量实际上会在主存储器中产生文件部分的另一个副本。因此,除非您拥有足够大的主存储器来存储文本文件以及向量及其内容,否则您可能会遇到一些页面错误,这也对程序的速度产生了显著影响。

1
我在循环之前为向量和字符串预留了空间,节省了60秒中的2秒。 - mic

2

根据使用getline,您显然在使用<stdio.h>

也许通过使用fopen(path, "rm");打开文件,会有所帮助,因为m告诉(这是GNU扩展)使用mmap进行读取。

也许使用setbuffer设置一个大缓冲区(即半兆字节)也可能有所帮助。

可能,使用readahead系统调用(或许在单独的线程中)可以有所帮助。

但这些都只是猜测。您真的应该进行测量。


4
C++ 中也有一个名为 getline 的函数。 - Some programmer dude

2

一般建议:

  • 编写最简单、最直接、最干净的方法,
  • 测量,
  • 测量,
  • 测量,

如果所有其他方法都失败了:

  • 按页面对齐的块顺序读取原始字节 (read(2))。这样做可以让内核的预读对你产生好处。
  • 重复使用相同的缓冲区以最小化缓存刷新。
  • 避免复制数据,在原地解析,传递指针 (和大小)。

mmap(2) -ing 文件的 [部分] 是另一种方法。这也避免了内核与用户空间之间的复制。


是的,我认为手动操作并没有真正的优势。任何本地性能提升都被维护成本和在更大规模上算法改进所抵消。 - Martin York
你也期望得到报酬吗? :) - Nikolai Fetissov
我想说的是,你使用结构体和指针以及大小(如@valdo所示)进行优化还为时过早。如果代码只是打印结果,那么很好(它可能会加速应用程序)。但是,如果需要操作字符串,则需要进行复制。因此,你所做的只是将成本从加载函数移动到操作函数中(整体上应用程序性能不会有所改善)。此外,手动编写处理字符串的技术会使代码更加脆弱。 - Martin York
但是如果不了解应用程序的更多信息,很难确定最佳前进方法。也许可以改进输入/输出序列化格式;也许您提出的页面对齐块是可以的,但是如果没有更多信息,我们会在考虑应用程序其他部分的成本之前优化本地方式。 - Martin York
这就是为什么我说“一般建议”的原因。我承认我忘记了将“测量”作为第一个要点。我会添加的。 - Nikolai Fetissov
显示剩余2条评论

0

根据您的磁盘速度,使用非常快的解压算法可能会有所帮助,例如fastlz(至少还有其他两种可能更有效的算法,但是由于GPL许可证问题,可能存在问题)。

此外,使用C++数据结构和函数可以增加速度,因为您可以也许实现更好的编译器时间优化。走C的方式并不总是最快的!在某些恶劣条件下,使用char*需要解析整个字符串才能达到\0,导致性能灾难。

对于解析数据,使用boost::spirit::qi也可能是最优化的方法http://alexott.blogspot.com/2010/01/boostspirit2-vs-atoi.html


瓶颈不在于硬盘本身的IO(高性能RAID系统)。尝试使用压缩文件的方法,但读取未压缩的数据速度更慢(请参见初始帖子)。 - mic
GZip注重压缩性能,而不是压缩速度。QuickLZ或FastLZ则注重解压缩速度。请参考它们的基准测试http://www.quicklz.com/。但当然,如果您以每秒300Mb的速度读取数据,那么这可能无法提供帮助。 - Tristram Gräbener

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