我有一个大文件,我需要阅读并从中制作字典。我希望这个尽可能快。但是我在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
我要做的唯一操作是查询一个键以返回与之关联的值列表。
答案 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__
方法不会比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]
(显然,如果你想能够重复运行,你需要添加一行来删除任何预先存在的数据库 - 或者,更好的是,如果它适合你的用例,检查它对输入文件的时间戳如果它已经是最新的,则跳过整个循环。)
答案 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执行速度会快得多。