【问题标题】:Reading key-value pairs as fast as possible in C++ from file在 C++ 中从文件中尽可能快地读取键值对
【发布时间】:2014-07-18 07:48:18
【问题描述】:

我有一个大约 200 万行的文件,如下所示:

2s,3s,4s,5s,6s 100000
2s,3s,4s,5s,8s 101
2s,3s,4s,5s,9s 102

第一个逗号分隔部分表示奥马哈扑克结果,而后一个分数是牌的示例“值”。在 C++ 中尽可能快地读取此文件对我来说非常重要,但我似乎无法让它比使用基本库的 Python 中的简单方法(4.5 秒)更快。

使用 Qt 框架(QHash 和 QString),我能够在发布模式下在 2.5 秒内读取文件。但是,我不想拥有 Qt 依赖项。目标是允许使用这 200 万行进行快速模拟,即 some_container["2s,3s,4s,5s,6s"] 以产生 100(尽管如果应用翻译功能或任何不可读的格式也可以加快阅读速度,也可以)。

我当前的实现非常慢(8 秒!):

std::map<std::string, int> get_file_contents(const char *filename)
{
    std::map<std::string, int> outcomes;
    std::ifstream infile(filename);

    std::string c;
    int d;

    while (infile.good())
    {
        infile >> c;
        infile >> d;
        //std::cout << c << d << std::endl;
        outcomes[c] = d;
    }
    return outcomes;
}

如何尽快将这些数据读入某种键/值散列

注意:前 16 个字符始终存在(卡片),而分数可以达到 100 万左右。

从各种 cmet 收集的一些进一步信息:

【问题讨论】:

  • 如果快速访问如此重要,为什么要将这些数据存储为文本?
  • @KerrekSB 不幸的是,我不知道替代方案。请注意,这是一场友谊赛;该解决方案必须是独立的,并且不能连接到数据库。
  • 那么,文件生成超出了您的控制范围?在这种情况下,您可以使用的最快数据结构是带有预分配块的 unordered_map。
  • 如果可以,以固定宽度的二进制表示形式存储数据。
  • @PascalvKooten:无需学习 CS 即可成为一名程序员。我推荐看this video series

标签: c++ hashmap containers


【解决方案1】:

在我看来,您的代码存在两个瓶颈。

1 瓶颈

我相信文件读取是那里最大的问题。 Having a binary file is the fastest option。您不仅可以在单个操作中使用原始 istream::read 直接在数组中读取它(这非常快),而且如果您的操作系统支持,您甚至可以将文件映射到内存中。这是一个link,它提供了有关如何使用内存映射文件的非常丰富的信息。


2 瓶颈

std::map 通常使用self-balancing BST 实现,它将按顺序存储所有数据。这使得插入成为O(logn) 操作。您可以将其更改为 std::unordered_map,它使用 hash table 代替。如果大肠杆菌数量较少,hash table 的插入时间将保持不变。由于您需要读取的元素数量是已知的,您可以在插入元素之前reserve 适当数量的块。请记住,您需要比将插入哈希中的元素数量更多的块,以避免最大数量的 colisions。

【讨论】:

  • 谢谢。我只是想知道您如何一次读取文件,因为它应该以键值组合的形式读取?听起来好像与向量无关?
  • 将其映射到内存并从那里填充 unordered_map。磁盘-> 内存传输是您最大的问题。我的意思是:您将从内存中的数组或直接从 std:istream 中读取内容。使用 std::ifstream 解析文本会很慢。
【解决方案2】:

Ian Medeiros 已经提到了两个主要的瓶颈。

关于数据结构的一些想法:

不同卡片的数量是已知的:每13张卡片有4种颜色 -> 52张卡片。 所以一张卡需要少于 6 位来存储。您当前的文件格式当前使用 24 位(包括逗号)。 因此,通过简单地枚举卡片并省略逗号,您可以节省约 2/3 的文件大小,并允许您确定一张卡片,每张卡片只能读取一个字符。 如果您想保持文件文本为基础,您可以使用 a-m、n-z、A-M 和 N-Z 作为四种颜色。

另一个让我烦恼的是基于字符串的地图。字符串操作效率低下。 一只手有5张牌。 如果我们保持简单并且不考虑已经抽出的牌,这意味着 52^5 个可能性。

--> 52^5 = 380.204.032

这意味着我们可以用一个 uint32 数字枚举所有可能的手牌。通过定义卡片的特殊排序方案(因为顺序无关紧要),我们可以为手牌分配一个数字,并将该数字用作地图中的键,这比使用字符串快得多。

如果我们有足够的内存(1.5 GB),我们甚至不需要映射,但我们可以简单地使用数组。 当然,大多数单元格都没有使用,但访问速度可能非常快。我们甚至可以省略卡片的顺序,因为无论我们填写与否,单元格都是独立存在的。所以我们可以使用它们。但在这种情况下,您不应该忘记填写从文件中读取的手的所有可能排列。

通过这个方案,我们也(可能)可以进一步优化我们的文件读取速度。如果我们只存储手数和评分,那么只需要解析 2 个值。

事实上,我们可以通过为不同的手使用更复杂的寻址方案来优化所需的存储空间,因为实际上只有 52*51*50*49*48 = 311.875.200 可能的手。除了排序之外如上所述无关紧要,但我认为这种节省不值得增加手部编码的复杂性。

【讨论】:

  • 很好地分解了整个问题。我认为,如果他围绕所有给定的答案进行研究,他将获得非常好的实施。
  • 很抱歉之后再提到这一点,但是对于整个实施,我受限于使用 750mb 内存来运行整个程序。所以让我们认为 600mb 内存是最大值。
【解决方案3】:

一个简单的想法可能是使用 C API,它相当简单:

#include <cstdio>

int n;
char s[128];

while (std::fscanf(stdin, "%127s %d", s, &n) == 2)
{
    outcomes[s] = n;
}

与 iostreams 库相比,一个粗略的测试表明对我来说有相当大的加速。

可以通过将数据存储在连续数组中来实现进一步的加速,例如std::pair&lt;std::string, int&gt; 的向量;这取决于您的数据是否已经排序以及您以后需要如何访问它。

不过,对于一个严肃的解决方案,您可能应该退后一步,想出一种更好的方法来表示您的数据。例如,固定宽度的二进制编码会更节省空间且解析速度更快,因为您无需提前查看行尾或解析字符串。

更新:通过一些快速实验,我发现首先将整个文件读入内存然后执行交替的strtok 调用与" ""\n" 作为分隔符;每当一对调用成功时,在第二个指针上应用strtol 以解析整数。这是一个骨架:

#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>

int main()
{
    std::vector<char> data;

    // Read entire file to memory
    {
        data.reserve(100000000);

        char buf[4096];
        for (std::size_t n; (n = std::fread(buf, 1, sizeof buf, stdin)) > 0; )
        {
            data.insert(data.end(), buf, buf + n);
        }
        data.push_back('\0');
    }

    // Tokenize the in-memory data
    char * p = &data.front();
    for (char * q = std::strtok(p, " "); q; q = std::strtok(nullptr, " "))
    {
        if (char * r = std::strtok(nullptr, "\n"))
        {
            char * e;
            errno = 0;
            int const n = std::strtol(r, &e, 10);
            if (*e != '\0' || errno != 0) { continue; }

            // At this point we have data:
            // * the string is "q"
            // * the integer is "n"
        }
    }
}

【讨论】:

  • 是的,文件中已经处于可搜索和排序格式的二进制表示将是可行的方法(如果允许)。注意。如果您最终使用向量,请确保您 reserve() 所需的大小,因为调整向量的大小需要大量数据移动并且是性能杀手。
  • 延伸目标:稍后我将在我知道我的牌的地方运行模拟,我将随机抽取许多敌人的手(减去我已经持有的牌)以确定谁拥有给定桌上的牌更高的分数。我不确定在这里对值进行排序是否会有所帮助,因为根据我拥有的卡片,向量中的某处会有间隙?
  • 另外,我对成对的向量有点困惑。在向量中搜索键会很慢?
  • @PascalvKooten:嗯,在地图中存储数据比在向量中要慢,但是您可以通过快速查找按排序顺序获得它。相比之下,通过字符串比较对包含数百万个元素的向量进行排序可能会很昂贵。因此,请选择最适合您的访问模式的表示形式。
  • @PascalvKooten:对于未排序的向量,您必须进行线性搜索,并且由于您正在进行字符串比较,因此会涉及大量随机内存。 (当然真正的解决方案是没有字符串键。)
猜你喜欢
  • 2016-07-25
  • 2011-06-18
  • 2023-03-03
  • 2013-12-16
  • 1970-01-01
  • 1970-01-01
  • 2016-07-20
  • 2016-08-23
  • 1970-01-01
相关资源
最近更新 更多