在C ++文件中尽可能快地读取键值对

时间:2014-05-28 22:00:07

标签: c++ hashmap containers

我有一个大约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万左右。

从各种评论中收集了一些进一步的信息:

3 个答案:

答案 0 :(得分:4)

正如我所看到的,您的代码存在两个瓶颈。

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使用合适的元素块。请记住,您需要更多的块,而不是将要插入哈希中的元素数量,以避免最大限度的分割。

答案 1 :(得分:2)

一个简单的想法可能是使用C API,这非常简单:

#include <cstdio>

int n;
char s[128];

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

与iostreams库相比,粗略测试显示我的速度相当快。

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

但是,对于一个严肃的解决方案,您应该更进一步思考一种更好的方式来表示您的数据。例如,固定宽度的二进制编码将更加节省空间并且更快解析,因为您不需要提前查看行结尾或解析字符串。

更新:通过一些快速实验,我发现首先将整个文件读入内存然后使用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"
        }
    }
}

答案 2 :(得分:2)

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

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

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

另一件令我烦恼的事情是基于字符串的地图。字符串操作效率不高。 一只手牌包含5张牌。 如果我们保持简单并且不考虑已经绘制的卡片,这意味着52 ^ 5的可能性。

- &GT; 52 ^ 5 = 380.204.032&lt; 2 ^ 32

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

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

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

实际上,我们可以通过对不同的手使用更复杂的地址方案来优化所需的存储空间,因为实际上只有52 * 51 * 50 * 49 * 48 = 311.875.200可能的指针。另外还有订购与提到的无关,但我认为这种节省不值得增加手的编码复杂性。