【问题标题】:C++: How to read a lot of data from formatted text files into program?C++:如何从格式化的文本文件中读取大量数据到程序中?
【发布时间】:2019-11-10 16:59:04
【问题描述】:

我正在为特定的流体问题编写 CFD 求解器。至此每次运行仿真都会生成网格,当改变几何形状和流体属性时,需要重新编译程序。

对于单元格数量少的小型问题,它工作得很好。但是对于超过 100 万个细胞的情况,并且需要经常更改流体属性,效率非常低。

显然,我们需要将模拟设置数据存储在配置文件中,并将几何信息存储在格式化的网格文件中。

  1. Simulation.config 文件
% Dimension: 2D or 3D
N_Dimension= 2
% Number of fluid phases
N_Phases=  1
% Fluid density (kg/m3)
Density_Phase1= 1000.0
Density_Phase2= 1.0
% Kinematic viscosity (m^2/s)
Viscosity_Phase1=  1e-6
Viscosity_Phase2=  1.48e-05
...
  1. Geometry.mesh 文件
% Dimension: 2D or 3D
N_Dimension= 2
% Points (index: x, y, z)
N_Points= 100
x0 y0
x1 y1
...
x99 y99
% Faces (Lines in 2D: P1->p2)
N_Faces= 55
0 2
3 4
...
% Cells (polygons in 2D: Cell-Type and Points clock-wise). 6: triangle; 9: quad
N_Cells= 20
9 0 1 6 20
9 1 3 4 7
...
% Boundary Faces (index)
Left_Faces= 4
0
1
2
3
Bottom_Faces= 6
7
8
9
10
11
12
...

将配置和网格信息写入格式化文本文件很容易。问题是,我们如何有效地将这些数据读入程序?我想知道是否有任何易于使用的 c++ 库来完成这项工作。

【问题讨论】:

  • 由于您的输入是文本,因此没有有效的方法(或者非常有效的方法不适用)。例如,对于数字,它们必须被解析然后转换为内部格式。现在,如果你的文件是二进制格式,那效率会更高。
  • 人类可读的数据格式有很多,例如 XML、HTML 和 INI。有库可以输入这些格式的数据;搜索互联网。
  • 请不要重新发明轮子。使用现有文件格式而不是自定义文件格式。看看en.m.wikipedia.org/wiki/Polygon_mesh#File_Formatshdfgroup.org/solutions/hdf5en.m.wikipedia.org/wiki/YAML。这些格式将提供用于存储和读取的库,并且通常比以往任何时候都更加努力地提高效率和容错能力。不要通过自己实施来放慢自己的速度......
  • 只需使用 wavefront .obj 和许多可用于读取和写入该格式的库之一。解决您的问题,还允许您使用任何现成的 CAD 程序来构建您的模型。
  • @3Dave 入口、出口、墙壁等物理边界信息对于模拟非常重要,我敢打赌,一些现有的网格格式(obj、off、stl)不允许包含这些。跨度>

标签: c++ parsing config configuration-files mesh


【解决方案1】:

好吧,好吧 您可以基于有限元集合、字典、一些正则表达式实现自己的 API,毕竟,根据一些国际标准应用投注练习。

或者你可以看看:

GMSH_IO

OpenMesh:

我刚刚在 C++ OpenGL 项目的最后一个实现中使用了 OpenMesh。

【讨论】:

  • OpenMesh 为 obj、off、ply 和 stl 网格格式提供了一些读取器/写入器。我需要的是一个简单的配置文件解析器:name=value 条目和简单的自定义网格格式。
【解决方案2】:

作为第一次迭代解决方案,只是获得一些可以容忍的东西 - 采用 @JosmarBarbosa 的 suggestion 并为您的数据类型使用已建立的格式 - 这可能还有免费的开源库供您使用。一个例子是在亚琛工业大学开发的OpenMesh。它支持:

  • 表示任意多边形(一般情况)和纯三角形网格(提供更高效、更专业的算法)
  • 顶点、半边、边和面的显式表示。
  • 快速访问社区,尤其是一环社区(见下文)。
  • [自定义]

但如果您真的需要加快网格数据的读取速度,请考虑执行以下操作:

  1. 将有限大小的元数据与更大的、无限大小的网格数据分开;
  2. 将有限大小的元数据放在一个单独的文件中,并以您喜欢的方式读取它,没关系。
  3. 将网格数据排列为多个固定大小元素或固定大小结构(例如单元、面、点等)的数组。
  4. 将每个固定宽度的网格数据数组存储在自己的文件中 - 无需在任何地方使用流式传输单个值:直接按原样读取或写入数组。 Here's an example of how a read would look。通过查看文件大小或元数据,您将知道读取的适当大小。

最后,您可以完全避免显式读取,并为每个数据文件使用内存映射。见

fastest technique to read a file into memory?

注意事项/注意事项:

  • 如果您在具有特定值的不同内存布局(例如 little-endian 与 big-endian)的系统上写入和读取二进制数据 - 您需要在内存中随机排列字节。另请参阅 this SO question 关于字节序。
  • 尽可能优化读取速度可能不值得。您应该考虑Amdahl's law,并仅将其优化到不再占您整体执行时间的重要部分的程度。最好减少几个百分点的执行时间,但获得人类可读的数据文件,这些文件可以与支持已建立格式的其他工具一起使用。

【讨论】:

    【解决方案3】:

    我假设以下回答:

    1. 如果一行的第一个字符是%,那么它将作为注释被忽略。
    2. 任何其他行的结构完全如下:identifier= value

    我提供的代码将按照上述假设正确解析配置文件。这是代码(我希望所有需要的解释都在 cmets 中):

    #include <fstream>          //required for file IO
    #include <iostream>         //required for console IO
    #include <unordered_map>    //required for creating a hashtable to store the identifiers
    
    int main()
    {
        std::unordered_map<std::string, double> identifiers;
    
        std::string configPath;
    
        std::cout << "Enter config path: ";
        std::cin >> configPath;
    
        std::ifstream config(configPath);   //open the specified file
        if (!config.is_open())              //error if failed to open file
        {
            std::cerr << "Cannot open config file!";
            return -1;
        }
    
        std::string line;
        while (std::getline(config, line))  //read each line of the file
        {
            if (line[0] == '%') //line is a comment
                continue;
    
            std::size_t identifierLenght = 0;
            while (line[identifierLenght] != '=')
                ++identifierLenght;
            identifiers.emplace(
                line.substr(0, identifierLenght),
                std::stod(line.substr(identifierLenght + 2))
            ); //add entry to identifiers
        }
    
        for (const auto& entry : identifiers)
            std::cout << entry.first << " = " << entry.second << '\n';
    }
    

    读完标识符后,当然可以对它们做任何你需要做的事情。我只是打印它们作为示例来展示如何获取它们。有关std::unordered_map 的更多信息,请查看here。有关制作解析器的许多非常好的信息,请查看here

    如果您想让您的程序处理输入更快,请在main 的开头插入以下行:std::ios_base::sync_with_stdio(false)。这将使 C++ IO 与 C IO 不同步,从而使其更快。

    【讨论】:

      【解决方案4】:

      假设:

      • 您不想为网格使用现有格式
      • 您不想使用通用文本格式(json、yml、...)
      • 你不想要二进制格式(即使你想要一些高效的东西)

      简而言之,您确实需要自己的文本格式。

      您可以使用任何parser generator 开始。虽然您可能会解析您的配置文件,因为它只使用正则表达式,但从长远来看,它们可能会受到真正的限制。所以我建议使用context-free grammar 解析器,使用Boost spirit::x3 生成。

      AST

      抽象语法树将保存解析器的最终结果。

      #include <string>
      #include <utility>
      #include <vector>
      #include <variant>
      
      namespace AST {
          using Identifier = std::string; // Variable name.
          using Value = std::variant<int,double>; // Variable value.
          using Assignment = std::pair<Identifier,Value>; // Identifier = Value.
          using Root = std::vector<Assignment>; // Whole file: all assignments.
      }
      

      解析器

      语法说明:

      #include <boost/fusion/adapted/std_pair.hpp>
      #include <boost/spirit/home/x3.hpp>
      
      namespace Parser {
          using namespace x3;
      
          // Line: Identifier = value
          const x3::rule<class assignment, AST::Assignment> assignment = "assignment";
          // Line: comment
          const x3::rule<class comment> comment = "comment";
          // Variable name
          const x3::rule<class identifier, AST::Identifier> identifier = "identifier";
          // File
          const x3::rule<class root, AST::Root> root = "root";
          // Any valid value in the config file
          const x3::rule<class value, AST::Value> value = "value";
      
          // Semantic action
          auto emplace_back = [](const auto& ctx) {
              x3::_val(ctx).emplace_back(x3::_attr(ctx));
          };
      
          // Grammar
          const auto assignment_def = skip(blank)[identifier >> '=' >> value];
          const auto comment_def = '%' >> omit[*(char_ - eol)];
          const auto identifier_def = lexeme[alpha >> +(alnum | char_('_'))];
          const auto root_def = *((comment | assignment[emplace_back]) >> eol) >> omit[*blank];
          const auto value_def = double_ | int_;
      
          BOOST_SPIRIT_DEFINE(root, assignment, comment, identifier, value);
      }
      

      用法

      // Takes iterators on string/stream...
      // Returns the AST of the input.
      template<typename IteratorType>
      AST::Root parse(IteratorType& begin, const IteratorType& end) {
          AST::Root result;
          bool parsed = x3::parse(begin, end, Parser::root, result);
          if (!parsed || begin != end) {
              throw std::domain_error("Parser received an invalid input.");
          }
          return result;
      }
      

      Live demo

      进化

      • 要更改允许空格的位置,请在 xxxx_def 表达式中添加/移动 x3::skip(blank)
      • 当前文件必须以换行符结尾。重写 root_def 表达式可以解决这个问题。
      • 您肯定想知道为什么在无效输入上解析失败。请参阅error handling tutorial
      • 您只需几条规则即可解析更复杂的内容:

        //                                               100              X_n        Y_n
        const auto point_def = lit("N_Points") >> ':' >> int_ >> eol >> *(double_ >> double_ >> eol)
        

      【讨论】:

        【解决方案5】:

        如果您不需要特定的文本文件格式,但拥有大量数据并且关心性能,我建议您改用一些现有的数据序列化框架。

        例如Google 协议缓冲区允许使用很少的代码进行高效的序列化和反序列化。该文件是二进制文件,因此通常比文本文件小得多,并且二进制序列化比解析文本要快得多。它还支持结构化数据(数组、嵌套结构)、数据版本控制和其他好东西。

        https://developers.google.com/protocol-buffers/

        【讨论】:

          猜你喜欢
          • 2023-03-31
          • 2017-08-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-04-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多