8

我有一个大文件,我需要读入并从中制作字典。我希望这尽可能快。但是我在 python 中的代码太慢了。这是一个显示问题的最小示例。

先做一些假数据

paste <(seq 20000000) <(seq 2 20000001)  > largefile.txt

现在这是一段最小的 Python 代码,用于读取它并制作字典。

import sys
from collections import defaultdict
fin = open(sys.argv[1])

dict = defaultdict(list)

for line in fin:
    parts = line.split()
    dict[parts[0]].append(parts[1])

时间:

time ./read.py largefile.txt
real    0m55.746s

但是,它不受 I/O 限制:

time cut -f1 largefile.txt > /dev/null    
real    0m1.702s

如果我注释掉该dict行,则需要9几秒钟。似乎几乎所有的时间都花在了dict[parts[0]].append(parts[1])

有什么办法可以加快这个速度吗?如果这会产生很大的不同,我不介意使用 cython 甚至 C。或者熊猫可以在这里提供帮助吗?

这是大小为 10000000 行的文件的配置文件输出。

python -m cProfile read.py test.data         20000009 function calls in 42.494 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 bisect.py:1(<module>)
        1    0.000    0.000    0.001    0.001 collections.py:1(<module>)
        1    0.000    0.000    0.000    0.000 collections.py:25(OrderedDict)
        1    0.000    0.000    0.000    0.000 collections.py:386(Counter)
        1    0.000    0.000    0.000    0.000 heapq.py:31(<module>)
        1    0.000    0.000    0.000    0.000 keyword.py:11(<module>)
        1   30.727   30.727   42.494   42.494 read.py:2(<module>)
 10000000    4.855    0.000    4.855    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
 10000000    6.912    0.000    6.912    0.000 {method 'split of 'str' objects}
        1    0.000    0.000    0.000    0.000 {open}

更新。 我们可以假设parts[1] 是一个整数,parts[0] 是一个固定长度的短字符串。

我的假数据不是很好,因为每个键只能得到一个值。这是一个更好的版本。

perl -E 'say int rand 1e7, $", int rand 1e4 for 1 .. 1e7' > largefile.txt

我要做的唯一操作是查询一个键以返回与其关联的值列表。

4

4 回答 4

8

如果您想要评论中所说的内容,那么您可以在 pandas 中轻松完成:假设您有一个布局相同但条目重复的文件,因为在您的示例中,您将所有重复项添加到列表中:

1 1
2 2
1 3
3 4
1 5
5 6

然后您可以读取和操作数据:

In [1]: df = pd.read_table('largefile.txt', header=None, index_col=0)
In [2]: df.loc[2]
Out[2]:
1    2
Name: 2, dtype: int64

In [3]: df.loc[1]
Out[3]:
   1
0
1  1
1  3
1  5

Pandas 将所有内容存储在索引的 DataFrames 和 Series 对象中,因此不要过多关注输出,第一列是索引,第二列是重要的,它将为您提供所需的数字。

不过,您可以使用 pandas 做更多事情......例如,您可以按文件中的第一列分组并执行聚合:

In [64]: df = pd.read_table('largefile.txt', header=None).groupby(0)
In [65]: df.sum()
Out[65]:
   1
0
1  9
2  2
3  4
5  6
In [66]: df.mean()
Out[66]:
   1
0
1  3
2  2
3  4
5  6    
In [67]: df[0].count()
Out[67]:
0
1    3
2    1
3    1
5    1
dtype: int64

我知道这不是如何加快字典速度的答案,但根据您在评论中提到的内容,这可能是另一种解决方案。

编辑- 添加时间

与最快的字典解决方案和将数据加载到 pandas DataFrame 相比:

test_dict.py

import sys
d = {}
with open(sys.argv[1]) as fin:
    for line in fin:
        parts = line.split(None, 1)
        d[parts[0]] = d.get(parts[0], []) + [parts[1]]

test_pandas.py

import sys
import pandas as pd
df = pd.read_table(sys.argv[1], header=None, index_col=0)

在linux机器上计时:

$ time python test_dict.py largefile.txt
real    1m13.794s
user    1m10.148s
sys     0m3.075s

$ time python test_pandas.py largefile.txt
real    0m10.937s
user    0m9.819s
sys     0m0.504s

编辑:对于新的示例文件

In [1]: import pandas as pd
In [2]: df = pd.read_table('largefile.txt', header=None,
                           sep=' ', index_col=0).sort_index()
In [3]: df.index
Out[3]: Int64Index([0, 1, 1, ..., 9999998, 9999999, 9999999], dtype=int64)
In [4]: df[1][0]
Out[4]: 6301
In [5]: df[1][1].values
Out[5]: array([8936, 5983])
于 2013-08-09T08:01:23.900 回答
4

以下是我设法获得的一些快速性能改进:

使用普通dict而不是defaultdict,并更改d[parts[0]].append(parts[1])d[parts[0]] = d.get(parts[0], []) + [parts[1]],将时间缩短 10%。我不知道它是否消除了对 Python__missing__函数的所有调用,没有就地改变列表,或者其他值得称赞的东西。

只是setdefault在平原上使用dict而不是defaultdict也将时间减少了 8%,这意味着它是额外的 dict 工作而不是就地附加。

同时,将 替换为split()有助于split(None, 1)9%。

在 PyPy 1.9.0 而不是 CPython 2.7.2 中运行将时间缩短了 52%;PyPy 2.0b 增加 55%。

如果不能使用 PyPy,CPython 3.3.0 将时间缩短了 9%。

在 32 位模式而不是 64 位模式下运行会增加 170% 的时间,这意味着如果您使用的是 32 位,则可能需要切换。


dict 需要超过 2GB 来存储(在 32 位中略少)这一事实可能是问题的很大一部分。唯一真正的选择是将所有内容存储在磁盘上。(在现实生活中的应用程序中,您可能希望管理内存缓存,但在这里,您只是生成数据并退出,这使事情变得更简单。)这是否有帮助取决于许多因素。我怀疑在带有 SSD 且 RAM 不多的系统上,它会加快速度,而在带有 5400rpm 硬盘驱动器和 16GB RAM 的系统上(就像我目前使用的笔记本电脑一样)它不会……不过要看你系统的磁盘缓存等等,谁知道呢,不用测试。

没有快速而肮脏的方法将字符串列表存储在基于磁盘的存储中(shelve可能会浪费更多的时间用于酸洗和解酸而不是保存),但将其更改为仅连接字符串并使用gdbm将内存使用量保持在 200MB 以下并完成大约在同一时间,并且有一个很好的副作用,如果您想多次使用数据,您可以将它们永久存储。不幸的是,plain olddbm不起作用,因为默认页面大小对于这么多条目来说太小了,而且 Python 接口没有提供任何覆盖默认值的方法。

切换到只有非唯一键和值列的简单 sqlite3 数据库并执行此:memory:操作花费了大约 80% 的时间,而在磁盘上花费了 85% 的时间。我怀疑将事物非规范化以使用每个键存储多个值无济于事,实际上会使事情变得更糟。(不过,对于许多现实生活中的用途,这可能是一个更好的解决方案。)


同时,环绕cProfile 你的主循环:

         40000002 function calls in 31.222 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1   21.770   21.770   31.222   31.222 <string>:2(<module>)
 20000000    2.373    0.000    2.373    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
 20000000    7.079    0.000    7.079    0.000 {method 'split' of 'str' objects}

所以,这是你花在 上的时间的三分之一,花在string.split10%上的时间append,其余的都花在cProfile看不到的代码上,这里包括迭代文件和defaultdict方法调用。

切换到普通dict的 with setdefault(记住,要快一点)显示在 中花费了 3.774 秒setdefault,所以这大约是 15% 的时间,或者大概是defaultdict版本的 20% 左右。据推测,该__setitem__方法不会比过去setdefault更糟defaultdict.__getitem__

但是,我们在这里可能看不到 malloc 调用所花费的时间,它们可能是性能的很大一部分。要对此进行测试,您需要一个 C 级分析器。所以让我们回到那个。

同时,至少一些剩余时间可能也被线分割占用了,因为这必须花费与空间分割相同的顺序,对吧?但我不知道有什么方法可以显着改善这一点。


最后,C 级分析器将在这里提供帮助,但在我的系统上运行可能对您的系统没有多大帮助,所以我将把它留给您。


我的系统上最快的版本取决于我运行的 Python,但要么是这样:

d = {}    
for line in fin:
    parts = line.split(None, 1)
    d[parts[0]] = d.get(parts[0], []) + [parts[1]]

或这个:

d = {}    
for line in fin:
    parts = line.split(None, 1)
    d.setdefault(parts[0], []).append(parts[1])

......他们都非常接近对方。

gdbm的方案,速度差不多,优缺点比较明显,是这样的:

d = gdbm.open(sys.argv[1] + '.db', 'c')
for line in fin:
    parts = line.split(None, 1)
    d[parts[0]] = d.get(parts[0], '') + ',' + parts[1]

(显然,如果您希望能够重复运行此程序,则需要添加一行来删除任何预先存在的数据库——或者,如果它适合您的用例,则更好地检查其时间戳与输入文件的时间戳并跳过如果它已经是最新的,则整个循环。)

于 2013-08-06T17:55:51.217 回答
3

这是给任何感兴趣的人的快速 C 版本。我机器上的标题时间:

Python(>5Gb 内存)

time ./read.py  largefile.txt

real    0m48.711s
user    0m46.911s
sys     0m1.783s

C(~1.9Gb 内存)

gcc -O3 read.c -o read
time ./read largefile.txt

real    0m6.250s
user    0m5.676s
sys     0m0.573s

所以在 C 语言中大约快 7.8 倍。:)

我应该补充一点,我的 seq 版本不会在不将命令更改为的情况下创建可用列表:

paste <(seq -f "%.0f" 20000000) <(seq -f "%.0f" 2 20000001)  > largefile.txt

下面的代码,必须归功于 Vijay Mathew,他将 C 编程语言第 6.6 节中的 dict 示例复制到他的示例中(我复制到下面的答案中): 在 C 中实现字典的快速方法

====== 编辑 ====== (13/08/2013)

继我的回答的评论 #2 之后,我已将代码更新为代码清单 2 中的代码,以允许单个键具有多个值,并且还开始使用更新的 perl 代码生成测试文件(因此大小只有一半大约一半的执行时间)。

标题时间是:

Python(>5Gb 内存)

time ./read.py  largefile.txt

real    0m25.925s
user    0m25.228s
sys     0m0.688s

C(~1.9Gb 内存)

gcc -O3 read.c -o read
time ./read largefile.txt

real    0m3.497s (although sub 3 seconds is possible by reducing the hash size?!?!?)
user    0m3.183s
sys     0m0.315s

因此,尽管 panda 可能很接近,但 C 语言的速度大约快了 7.4 倍。

然而,关于大小的这一点很重要。我可以通过将散列大小减小到一个非常小的数字来“作弊”,这对于多值字典会以查找为代价来提高插入速度。因此,要真正测试这些实现中的任何一个,您确实还需要测试查找速度。

代码 2(多值字典)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct nlist { /* table entry: */
    struct nlist *next; /* next entry in chain */
    char *name; /* defined name */
    char *defn; /* replacement text */
};

#define HASHSIZE 10000001
static struct nlist *hashtab[HASHSIZE]; /* pointer table */

/* hash: form hash value for string s */
unsigned hash(char *s)
{
    unsigned hashval;
    for (hashval = 0; *s != '\0'; s++)
      hashval = *s + 31 * hashval;
    return hashval % HASHSIZE;
}

/* lookup: look for s in hashtab */
struct nlist *lookup(char *s)
{
    struct nlist *np;
    for (np = hashtab[hash(s)]; np != NULL; np = np->next)
        if (strcmp(s, np->name) == 0)
          return np; /* found */
    return NULL; /* not found */
}

struct nlist * lookup_all(char *key)
{
    struct nlist *np, *np2, *ret;
    unsigned hashval = hash(key);

    ret = NULL;

    for (np = hashtab[hashval]; np != NULL; np = np->next) {
      if (strcmp(key, np->name) == 0) {
        np2 = malloc(sizeof(*np2));
        np2->name = np->name;
        np2->defn = np->defn;
        np2->next = ret;
        ret = np2;
      }
    }
    return ret; /* not found */
}

/* install: put (name, defn) in hashtab */
struct nlist *install(char *name, char *defn)
{
    struct nlist *np, *np2;
    unsigned hashval = hash(name);;
    //if ((np = lookup(name, hashval)) == NULL) { /* not found */

    np = (struct nlist *) malloc(sizeof(*np));
    if (np == NULL || (np->name = strdup(name)) == NULL)
      return NULL;
    np->next = hashtab[hashval];
    hashtab[hashval] = np;

    if ((np->defn = strdup(defn)) == NULL)
       return NULL;
    return np;
}
#ifdef STRDUP
char *strdup(char *s) /* make a duplicate of s */
{
    char *p;
    p = (char *) malloc(strlen(s)+1); /* +1 for .\0. */
    if (p != NULL)
       strcpy(p, s);
    return p;
}
#endif /* STRDUP */

int main(int argc, char *argv[]) {

    FILE *fp;
    char str1[20];
    char str2[20];
    int size = 0;
    int progress = 0;
    struct nlist *result;

    fp = fopen(argv[1],"r");
    if(fp==NULL) {return 1;}

    fseek(fp, 0, SEEK_END);
    size = ftell(fp);
    rewind(fp);

   while(size != ftell(fp)) {
        if(0==fscanf(fp, "%s %s",str1,str2))
            break;
        (void)install(str1,str2);
    }
    printf("Done\n");
    fclose(fp);

    // Test a lookup to see if we get multiple items back.    
    result = lookup_all("1");
    while (result) {
        printf("Key = %s Value = %s\n",result->name,result->defn);
        result = result->next;
    }

    return 0;
}

代码 1(单值字典)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct nlist { /* table entry: */
    struct nlist *next; /* next entry in chain */
    char *name; /* defined name */
    char *defn; /* replacement text */
};

#define HASHSIZE 10000001
static struct nlist *hashtab[HASHSIZE]; /* pointer table */

/* hash: form hash value for string s */
unsigned hash(char *s)
{
    unsigned hashval;
    for (hashval = 0; *s != '\0'; s++)
      hashval = *s + 31 * hashval;
    return hashval % HASHSIZE;
}

/* lookup: look for s in hashtab */
struct nlist *lookup(char *s)
{
    struct nlist *np;
    for (np = hashtab[hash(s)]; np != NULL; np = np->next)
        if (strcmp(s, np->name) == 0)
          return np; /* found */
    return NULL; /* not found */
}

/* install: put (name, defn) in hashtab */
struct nlist *install(char *name, char *defn)
{
    struct nlist *np;
    unsigned hashval;
    if ((np = lookup(name)) == NULL) { /* not found */
        np = (struct nlist *) malloc(sizeof(*np));
        if (np == NULL || (np->name = strdup(name)) == NULL)
          return NULL;
        hashval = hash(name);
        np->next = hashtab[hashval];
        hashtab[hashval] = np;
    } else /* already there */
        free((void *) np->defn); /*free previous defn */
    if ((np->defn = strdup(defn)) == NULL)
       return NULL;
    return np;
}
#ifdef STRDUP
char *strdup(char *s) /* make a duplicate of s */
{
    char *p;
    p = (char *) malloc(strlen(s)+1); /* +1 for .\0. */
    if (p != NULL)
       strcpy(p, s);
    return p;
}
#endif /* STRDUP */

int main(int argc, char *argv[]) {

    FILE *fp;
    char str1[20];
    char str2[20];
    int size = 0;
    int progress = 0;

    fp = fopen(argv[1],"r");
    if(fp==NULL) {return 1;}

    fseek(fp, 0, SEEK_END);
    size = ftell(fp);
    rewind(fp);

   while(size != ftell(fp)) {
        if(0==fscanf(fp, "%s %s",str1,str2))
            break;
        //printf(">%s<>%s<\n",str1,str2);
        (void)install(str1,str2);
        ++progress;
        if((progress % 100000)==0)
            printf("Progress = %d\n",progress);
    }
    printf("Done\n");
    fclose(fp);

    return 0;
}
于 2013-08-09T13:43:13.703 回答
0

您仍然可以在其他优化之上添加额外的优化:

由于您的键是“几乎”连续整数的字符串,您可以通过按顺序在 dict 中插入元素来加快 dict 的创建。它将减少字典冲突。请参阅有关python dict 实现的评论

未来的主要微妙之处:从模拟随机性的意义上说,大多数散列方案都依赖于具有“良好”的散列函数。Python 没有:它最重要的散列函数(用于字符串和整数)在常见情况下非常有规律:

map(hash, (0, 1, 2, 3)) [0, 1, 2, 3] map(hash, ("namea", "nameb", "namec", "named")) [-1658398457, - 1658398460,-1658398459,-1658398462]

这不一定是坏事!相反,在大小为 2**i 的表中,将低位 i 位作为初始表索引非常快,并且对于由连续范围的 int 索引的 dicts 根本没有冲突。当键是“连续的”字符串时,情况大致相同。因此,这在常见情况下提供了优于随机的行为,这是非常可取的。

因此,如果您可以对文件进行预处理以对其进行排序,那么 python 的执行速度会快得多。

于 2013-08-18T07:06:38.917 回答