读入大文件并制作字典

时间:2013-08-06 17:13:23

标签: python c performance pandas cython

我有一个大文件,我需要阅读并从中制作字典。我希望这个尽可能快。但是我在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]是一个整数,而part [0]是一个短的固定长度字符串。

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

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

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

4 个答案:

答案 0 :(得分:7)

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

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对象中,这些对象都被编入索引,因此不会对输出产生太大影响,第一列是索引,第二列是重要的列,它将为您提供所需的数字。 / p>

您可以使用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])

答案 1 :(得分:3)

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

使用普通dict代替defaultdict,并将d[parts[0]].append(parts[1])更改为d[parts[0]] = d.get(parts[0], []) + [parts[1]],将时间缩短10%。我不知道它是否正在消除对Python __missing__函数的所有调用,而不是在原地改变列表,或其他值得信任的东西。

在普通setdefault而不是dict上使用defaultdict也会将时间缩短8%,这意味着它是额外的字典工作而不是就地附加。< / p>

与此同时,将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的系统上并没有太多的内存,它会加快速度,而在配备5400rpm硬盘和16GB内存的系统上(就像我目前正在使用的笔记本电脑那样)它不会......但是,根据你的系统的磁盘缓存等,谁知道,没有测试。

没有快速和简单的方法来存储基于磁盘的存储中的字符串列表(shelve可能会浪费更多的时间来进行酸洗和去除,而不是保存),而是将其更改为仅连接字符串并使用gdbm将内存使用率保持在200MB以下,大约在同一时间内完成,并且具有良好的副作用,如果您想多次使用这些数据,则必须持久存储它们。不幸的是,普通的旧dbm不起作用,因为默认页面大小对于这么多条目来说太小了,并且Python接口没有提供任何覆盖默认值的方法。

切换到一个简单的sqlite3数据库,该数据库只有非唯一的Key和Value列并且在: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.split上的时间的三分之一,花费在append上花费了10%,剩下的花费了cProfile无法看到的代码,其中包括两者迭代文件和defaultdict方法调用。

使用dict切换到常规setdefault(请记住,速度稍快一点)显示setdefault花费了3.774秒,因此约占15%的时间,或者大概是defaultdict版本约为20%。预先__setitem__方法不会比setdefaultdefaultdict.__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]

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

答案 2 :(得分:3)

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

Python(&gt; 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示例复制到他的示例中(我复制到下面的答案中): Quick Way to Implement Dictionary in C

======编辑======(2013年8月13日)

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

标题时间是:

Python(&gt; 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

所以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;
}

答案 3 :(得分:0)

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

由于您的键是“几乎”连续整数的字符串,因此您可以通过按顺序插入dict中的元素来加速创建dict。它将减少字典冲突。请参阅python dict implementation

上的评论
  

未来的主要微妙之处:大多数哈希计划依赖于“好”   哈希函数,在模拟随机性的意义上。 Python没有:   它最重要的哈希函数(对于字符串和整数)非常有用   常见的常见情况:

     
    
      
        

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

      
    
  
     

这不一定是坏事!相反,在一张2 ** i的表格中,   将低位i位作为初始表索引是极其重要的   快速,并且对于由a索引的dicts完全没有冲突   连续的整数范围。键是大致相同的   “连续”字符串。所以这给出了比随机更好的行为   常见的情况,这是非常可取的。

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