如何在 C++ 中使 csv 文件运行得更快

Posted

技术标签:

【中文标题】如何在 C++ 中使 csv 文件运行得更快【英文标题】:how to make csv file run faster in c++ 【发布时间】:2021-12-29 05:42:12 【问题描述】:

我在 CSV 文件中有一百万个条目,需要加载它。但是,完成加载大约需要 2 分钟。我需要解决这个问题以使数据加载更快。有没有更好的方法可以解决?所以我可以研究并尝试修复它。感谢您的帮助。

CSVReader.h

#pragma once
#include "OrderBookEntry.h"
#include <vector>
#include <string>

class CSVReader

public:
CSVReader();
static std::vector<OrderBookEntry> readCSV (std::string csvFile);
;

CSVReader.cpp

#include "CSVReader.h"
#include <iostream>
#include <fstream>

CSVReader::CSVReader() 



std::vector<OrderBookEntry> CSVReader::readCSV(std::string csvFilename) 
  std::vector<OrderBookEntry> entries;
  std::ifstream csvFilecsvFilename;
  std::string line;
  if (csvFile.is_open()) 
  
      while (std::getline(csvFile, line))   
       
        
        try 
            OrderBookEntry obe = stringsToOBE(tokenise(line, ','));
            entries.push_back(obe);
        
        catch(const std::exception& e)
            std::cout << "CSVReader::readCSV bad data" << std::endl;
        
        
    //end of while

std::cout << "Successfully read " << entries.size() << " entries" << std::endl;
return entries;


std::vector<std::string> CSVReader::tokenise(std::string csvLine, char separator) 
   std::vector<std::string>tokens;
   signed int start, end;
   std::string token;
   start = csvLine.find_first_not_of(separator, 0);
   do 
     end = csvLine.find_first_of(separator, start);
     if (start == csvLine.length() || start == end) break;
     if (end >= 0) token = csvLine.substr(start, end - start);
     else token = csvLine.substr(start, csvLine.length() - start);
     tokens.push_back(token);
     start = end + 1;
    while (end > 0);

   return tokens;


OrderBookEntry CSVReader::stringsToOBE(std::vector<std::string>tokens) 
    double price, amount;
     if (tokens.size() != 5) 
        std::cout << "Bad Input" << std::endl;
        throw std::exception;
    
    try 
        //we have 5 tokens
        price = std::stod(tokens[3]);
        amount = std::stod(tokens[4]);
    
    catch(const std::exception& e)
        std::cout << "Bad Float!" << tokens[3] << std::endl;
        std::cout << "Bad Float!" << tokens[4] << std::endl;
        throw;
    

    OrderBookEntry
    obeprice,amount,tokens[0],tokens[1],OrderBookEntry::stringToOrderBookType(tokens[2]);
    return obe;

 

【问题讨论】:

您需要一次将所有条目加载到主内存中吗? “一百万个条目”听起来您应该使用数据库,而不是电子表格。 了解如何将变量作为引用传递给函数,而不是在它们很大时复制它们。 stringsToOBE 应该是 stringsToOBE(const std::vector&lt;string&gt;&amp; tokens) 1.重新考虑程序的架构。 2. 将复制成本高昂的变量作为 const 引用传递。 1.你写的并不理想,但在现代机器上它应该更快。也许你可以提供一个测试文件。 2. 确保在发布配置中测试您的程序。 Debug 和 Release 配置之间的差异可能很大。 3.你没看懂throw/catch. 4. strtod`不扔。` 如果您知道文件的确切大小以及 std::vector 的大小,据我了解,最有效的方法是在您的 std::vector 上使用保留。因此,如果您有 1,000,000 行“条目”,请执行 entries.reserve(1000000);预分配内存。根据我的经验,这比使用 std::vector.push_back(); 更快。因为 push_back() 必须添加内存 1,000,000 次,而不是使用 reserve() 一次完成所有操作。您还需要一个虚拟计数器,以便每次读取一行并向其中添加数据时都可以通过向量进行索引。 【参考方案1】:

正如 cmets 中提到的,首先应该优化的是按值传递参数。即,应该有const std::string&amp; arg_name 而不是std::string arg_name 等。

那么,std::vector 的使用也不是那么理想。这是因为vector 的性质:它连续存储其元素。这意味着如果在某些时候没有内存可以以这种方式(连续地)附加下一个元素,那么所有现有元素都应该重新分配到一个新位置。因此,如果 vector 的使用不是非常需要,请考虑将其替换为 std::list 或其他内容。

然后,我会像这样简化CSVReader::tokenise 方法:

std::list<std::string> CSVReader::tokenise(const std::string& str, char sep)

  std::list<string> result;
  std::string::size_type cur = 0;
  for(auto pos = str.find_first_of(sep); pos != string::npos; cur = pos + 1, pos = str.find_first_of(sep, cur))
    result.emplace_back(str, cur, pos - cur);
  result.emplace_back(str, cur);

  return result;

然后,我将结合 tokenisestringsToOBE 方法。

一般来说,查看循环中执行的代码。

【讨论】:

std::list 很少比std::vector 好,在向量上调用reserve 会是更好的方法 @AlanBirtles,我建议以list 为例。应根据项目的需要选择容器。【参考方案2】:

想想你在解析一行的时候做了什么样的操作,效率低下的原因是什么。

例如,

std::vector<std::string> CSVReader::tokenise(std::string csvLine, char separator)

它通过复制接受csvLine。复制字符串可能会很慢,因为它可能涉及内存分配。

然后你从一个字符串创建一个字符串向量......创建一个std::string_view的向量不是更好吗?所以你不要复制字符串的每个元素。为了使函数更安全,要求标记化也接受 std::string_view 而不是 std::stringconst std::string&amp;

接下来,通过审核CSVReader::stringsToOBE(std::vector&lt;std::string&gt;tokens), 显然我看到你实际上需要大小为 5。然后将一些元素解析为浮点数并将一些元素复制为字符串,另一个转换为枚举。

要求输入为std::array&lt;std::string_view,5&gt; 不是更有意义吗?编译时强制大小为 5 而不是测试它?为了适应这种变化,您可以将tokenize 转换为接受数字n 作为模板参数的模板,并根据分隔符将std::string_view 转换为std::array&lt;std::string_view,n&gt;,最后验证它是字符串视图。

由于消除了不必要的std::vector,这节省了另一个动态分配。

但是为什么你从ifstream得到一个字符串然后解析它并分别转换每个元素呢?

为什么不直接从流中直接提取元素而不创建中间字符串呢?说,添加函数OrderBookEntry CSVReader::extractOBEElement(std::istream&amp; stream)?并且只需使用流的原生函数,如&gt;&gt;?

它可能会工作并且效率更高,但众所周知流很慢,因此首先生成字符串可能确实更可取。

【讨论】:

以上是关于如何在 C++ 中使 csv 文件运行得更快的主要内容,如果未能解决你的问题,请参考以下文章

如何让我的 python 程序运行得更快?

如何在 Oracle 中使 Select 语句更快

如何使 CSV 上传执行得更快?

如何从 pyspark 数据框中更快地保存 csv 文件?

如何在 Android 中使位图加载更快?

如何让scikit-learn Nearest Neighbors算法运行得更快?