读取映射到内存的CSV文件的最简单方法是什么?

时间:2014-05-16 15:59:27

标签: c++ csv boost io memory-mapped-files

当我从C ++(11)中读取文件时,我使用以下方法将它们映射到内存:

boost::interprocess::file_mapping* fm = new file_mapping(path, boost::interprocess::read_only);
boost::interprocess::mapped_region* region = new mapped_region(*fm, boost::interprocess::read_only);
char* bytes = static_cast<char*>(region->get_address());

当我希望逐字节读取时,这很好。但是,我创建了一个csv文件,我想将其映射到内存,读取每一行并分割逗号上的每一行。

我是否可以通过对上述代码进行一些修改来实现此目的?

(我正在映射到内存,因为我的内存非常多,而且我不希望任何磁盘/ IO流的瓶颈)。

2 个答案:

答案 0 :(得分:5)

这是我对“足够快”的看法。它在~1秒内拉过116 MiB的CSV(2.5Mio线 [1] )。

然后可以在零拷贝时随机访问结果,因此没有开销(除非页面被换出)。

  

进行比较:

     
      
  • 比天真的wc csv.txt 快3倍〜对同一档案
  •   
  • 它跟下面的perl one liner一样快(列出了所有行的不同字段数):

    perl -ne '$fields{scalar split /,/}++; END { map { print "$_\n" } keys %fields  }' csv.txt
    
  •   
  • 它只比(LANG=C wc csv.txt)慢,这避免了区域设置功能(大约1.5倍)

  •   

这是解析器的全部荣耀:

using CsvField = boost::string_ref;
using CsvLine  = std::vector<CsvField>;
using CsvFile  = std::vector<CsvLine>;  // keep it simple :)

struct CsvParser : qi::grammar<char const*, CsvFile()> {
    CsvParser() : CsvParser::base_type(lines)
    {
        using namespace qi;

        field = raw [*~char_(",\r\n")] 
            [ _val = construct<CsvField>(begin(_1), size(_1)) ]; // semantic action
        line  = field % ',';
        lines = line  % eol;
    }
    // declare: line, field, fields
};

唯一棘手的事情(也是那里唯一的优化)是从源迭代器构造CsvField的语义动作,匹配字符数。

这是主要的:

int main()
{
    boost::iostreams::mapped_file_source csv("csv.txt");

    CsvFile parsed;
    if (qi::parse(csv.data(), csv.data() + csv.size(), CsvParser(), parsed))
    {
        std::cout << (csv.size() >> 20) << " MiB parsed into " << parsed.size() << " lines of CSV field values\n";
    }
}

打印

116 MiB parsed into 2578421 lines of CSV values

您可以将这些值用作std::string

for (int i = 0; i < 10; ++i)
{
    auto l     = rand() % parsed.size();
    auto& line = parsed[l];
    auto c     = rand() % line.size();

    std::cout << "Random field at L:" << l << "\t C:" << c << "\t" << line[c] << "\n";
}

打印例如:

Random field at L:1979500    C:2    sateen's
Random field at L:928192     C:1    sackcloth's
Random field at L:1570275    C:4    accompanist's
Random field at L:479916     C:2    apparel's
Random field at L:767709     C:0    pinks
Random field at L:1174430    C:4    axioms
Random field at L:1209371    C:4    wants
Random field at L:2183367    C:1    Klondikes
Random field at L:2142220    C:1    Anthony
Random field at L:1680066    C:2    pines

完整工作的示例在 Live On Coliru


[1] 我通过重复附加

的输出来创建文件
while read a && read b && read c && read d && read e
do echo "$a,$b,$c,$d,$e"
done < /etc/dictionaries-common/words

csv.txt,直到计算出250万行。

答案 1 :(得分:1)

只需从内存映射字节创建一个istringstream,然后使用:

解析它
const std::string stringBuffer(bytes, region->get_size());
std::istringstream is(stringBuffer);
typedef boost::tokenizer< boost::escaped_list_separator<char> > Tokenizer;
std::string line;
std::vector<std::string> parsed;
while(getline(is, line))
{
    Tokenizer tokenizer(line);
    parsed.assign(tokenizer.begin(),tokenizer.end());
    for (auto &column: parsed)
    {
        // 
    }
}

请注意,在许多系统中,与顺序读取相比,内存映射不能提供任何速度优势。在这两种情况下,您最终都会逐页读取磁盘中的数据,可能具有相同的预读量,并且两种情况下的IO延迟和带宽都是相同的。无论你有多少记忆或没有赢得任何改变。此外,取决于系统,memory_mapping甚至是只读,可能会导致令人惊讶的行为(例如,保留交换空间),而这些行为有时会让人们忙于排除故障。