【问题标题】:Embedding big data file into executable binary将大数据文件嵌入可执行二进制文件
【发布时间】:2016-11-02 14:55:26
【问题描述】:

我正在开发一个应该作为单个可执行二进制文件发布的 C++11 应用程序。或者,用户可以提供自己的 CSV 数据文件以供应用程序使用。为简化起见,假设每个元素的格式为key,value\n。我创建了一个结构,例如:

typedef struct Data {
    std::string key;
    std::string value;

    Data(std::string key, std::string value) : key(key), value(value) {}
} Data;

默认情况下,应用程序应使用在单个头文件中定义的数据。我制作了一个简单的 Python 脚本来解析默认的 CSV 文件并将其放入头文件中,例如:

#ifndef MYPROJECT_DEFAULTDATA
#define MYPROJECT_DEFAULTDATA

#include "../database/DefaultData.h"

namespace defaults {
    std::vector<Data> default_data = {
        Data("SomeKeyA","SomeValueA"),
        Data("SomeKeyB","SomeValueB"),
        Data("SomeKeyC","SomeValueC"),

        /* and on, and on, and on... */

        Data("SomeKeyASFHOIEGEWG","SomeValueASFHOIEGEWG")
    }
}

#endif //MYPROJECT_DEFAULTDATA

唯一的问题是,那个文件很大。我说的是 116'087 (12M) 行大,将来可能会被更大的文件取代。当我包含它时,我的 IDE 正在尝试解析它并更新索引。它让一切都慢下来,以至于我几乎写不出任何东西。

我正在寻找一种方法:

  1. 阻止我的 IDE (CLion) 解析它或
  2. 在 cmake 中进行一个开关,仅将此文件用于发布可执行文件或
  3. 以某种方式将数据直接注入可执行文件

【问题讨论】:

  • 而且您不能将实际的“默认值”CSV 文件与可执行文件一起发送,因此如果没有加载其他数据文件就可以读取它?那么如何创建一个包含文件实际内容的长字符串,就像你的向量一样包含它,并且在启动时解析该字符串?
  • 所以你想给你的用户一个单一的可执行文件,但你需要为每个用户做不同的构建来合并他们的数据? (在他们给你数据文件之后)?你真正想解决什么问题?通常将数据存储在数据文件/数据库中(出于某种原因
  • 关于“性能”,启动时间有那么重要吗?还是更重要的是它一旦开始就“执行”?该程序多久启动一次?一天几次?一天一次?一个星期一次?启动后会运行多久?几分钟?小时?天?我只是问,因为我知道如何将文件“嵌入”到可执行文件中的任何其他方法都依赖于存储原始未解析数据。也许您生成的数据源文件应该在外部构建到一个单独的库中,并且 IDE 没有真正接触它?
  • 使用add_custom_commandadd_custom_target 命令管理外部“库”(相对)容易。您实际上并不需要一个库,只需要一个添加到主构建中的目标文件。只要您不打开自动生成的源文件,并将其放在从 CLion 中排除的单独目录中,那么您应该没有问题。
  • @Someprogrammerdude:我没有说vector 会分配不止一次。 vector 中的每个 std::string 可能有自己的分配,具体取决于字符串的大小和 std::string 实现上 SSO 的存在。另外,每个 string 构造 also 都会从字符串文字中复制出来。

标签: c++11 cmake initialization clion compile-time


【解决方案1】:

由于您的构建过程已经包含一个预处理过程,它从 CSV 生成 C++ 代码,这应该很容易。

第1步:将大部分生成的数据放在.cpp文件中,而不是头文件。

第 2 步:生成您的代码,使其不使用 vectorstring

以下是如何执行这些操作:

struct Data
{
    string_view key;
    string_view value;
};

您将需要string_view 或类似类型的实现。虽然它是在 C++17 中标准化的,但它不依赖于 C++17 的特性。

至于数据结构本身,这是在标头中生成的:

namespace defaults {
    extern const std::array<Data, {{GENERATED_ARRAY_COUNT}}> default_data;
}

{{GENERATED_ARRAY_COUNT}} 是数组中的项目数。这就是所有生成的标头应该公开的内容。生成的 .cpp 文件有点复杂:

static const char ptr[] =
    "SomeKeyA" "SomeValueA"
    "SomeKeyB" "SomeValueB"
    "SomeKeyC" "SomeValueC"
    ...
    "SomeKeyASFHOIEGEWG" "SomeValueASFHOIEGEWG"
;

namespace defaults 
{
  const std::array<Data, {{GENERATED_ARRAY_COUNT}}> default_data =
  {
      {{ptr+{{GENERATED_OFFSET}}, {{GENERATED_SIZE}}}, {ptr+{{GENERATED_OFFSET}}, {{GENERATED_SIZE}}}},
      {{ptr+{{GENERATED_OFFSET}}, {{GENERATED_SIZE}}}, {ptr+{{GENERATED_OFFSET}}, {{GENERATED_SIZE}}}},
      ...
      {{ptr+{{GENERATED_OFFSET}}, {{GENERATED_SIZE}}}, {ptr+{{GENERATED_OFFSET}}, {{GENERATED_SIZE}}}},
  };
}

ptr 是一个字符串,它是所有单个字符串的串联。无需在各个字符串之间放置空格或 \0 字符或其他任何内容。但是,如果您确实需要将这些字符串传递给以 NULL 结尾的字符串的 API,则必须将它们复制到 std::string 中,或者让生成器在每个生成的子字符串之后粘贴 \0 字符。

重点是ptr 应该是一个巨大的字符数据块。

{{GENERATED_OFFSET}} 和 {{GENERATED_SIZE}} 是表示单个子字符串的巨大字符数据块中的偏移量和大小。

此方法将解决您的两个问题。它在加载时会快得多,因为它执行零动态分配。并将生成的字符串放入 .cpp 文件中,从而使您的 IDE 配合使用。

【讨论】:

    猜你喜欢
    • 2012-05-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-02
    • 1970-01-01
    • 2018-04-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多