了解性能差异

时间:2013-07-14 14:24:16

标签: python python-2.7

回答this question我遇到了一个有趣的情况2类似的代码片段表现完全不同。我在这里只是要了解其原因,并提高我对此类案例的直觉。

我将改编Python 2.7的代码片段(在Python 3中,性能差异是相同的)。

from collections import OrderedDict
from operator import itemgetter
from itertools import izip

items = OrderedDict([('a', 10), ('b', 9), ('c', 4), ('d', 7), ('e', 3), ('f', 0), ('g', -5), ('h', 9)])

def f1():
    return min(items, key=items.get)

def f2():
    return min(items.iteritems(), key=itemgetter(1))[0]


from timeit import Timer
N = 100000

print(Timer(stmt='f1()', setup='from __main__ import f1').timeit(number = N))
print(Timer(stmt='f2()', setup='from __main__ import f2').timeit(number = N))

输出:

0.603327797248
1.21580172899

第一个解决方案必须在OrderedDictionary中进行查找,以便为每个value获取key。第二个解决方案只是遍历OrderedDictionary键值对,它们必须打包成元组。

第二种解决方案慢2倍。

为什么?

我最近看过this video,其中Raymond Hettinger说Python倾向于重复使用元组,因此没有额外的分配。

那么,这个性能问题归结为什么呢?


我想详细说明我为什么要问。

第一个解决方案是字典查找。它意味着接受key哈希,然后通过此哈希查找bin,然后从该bin获取密钥(希望不会发生密钥冲突),然后获取与该密钥相关联的值。

第二个解决方案只是通过所有箱子并产生这些箱子中的所有钥匙。它一个接一个地遍历所有的箱子而没有计算的开销。是的,它必须访问与这些键关联的值,但该值只是键的一步,而第一个解决方案必须通过hash-bin-key-value链来获取需要它的值。每个解决方案都必须获取值,第一个获取它通过hash-bin-key-value链,第二个获取它在访问key时再跟随一个指针。第二种解决方案的唯一开销是它必须将该值与密钥一起存储在元组中。事实证明,这种存储是开销的主要原因。鉴于所谓的“元组重用”(参见上面提到的视频),我仍然不完全理解为什么会如此。

在我看来,第二种解决方案必须与密钥一起保存价值,但它避免了我们必须进行哈希bin密钥计算和访问以获得该密钥的值。

4 个答案:

答案 0 :(得分:6)

性能差异主要由OrderedDict引起。
OrderedDict使用dict的{​​{1}}和get,但重新定义了自己的__iter__iteritems

__getitem__

看看我们找到了什么: def __iter__(self): 'od.__iter__() iter(od)' # Traverse the linked list in order. root = self.__root curr = root[1] # start at the first node while curr is not root: yield curr[2] # yield the curr[KEY] curr = curr[1] # move to next node def iteritems(self): 'od.iteritems -> an iterator over the (key, value) pairs in od' for k in self: yield (k, self[k]) 您的第二个解决方案无法帮助我们避免哈希bin密钥计算。 虽然self[k]生成的迭代器,更确切地说,如果dictitemsitems.iteritems().next()生成的迭代器不会进行此计算。

此外,dict也更贵。

iteritems

输出

from timeit import Timer
N = 1000

d = {i:i for i in range(10000)}

def f1():
    for k in d: pass

def f2():
    for k in d.iterkeys(): pass

def f3():
    for v in d.itervalues(): pass

def f4():
    for t in d.iteritems(): pass

print(Timer(stmt='f1()', setup='from __main__ import f1').timeit(number=N))
print(Timer(stmt='f2()', setup='from __main__ import f2').timeit(number=N))
print(Timer(stmt='f3()', setup='from __main__ import f3').timeit(number=N))
print(Timer(stmt='f4()', setup='from __main__ import f4').timeit(number=N))

0.256800375467 0.265079360645 0.260599391822 0.492333103788 'dictiter_iternextkeyiterkeys'dictiter_iternextvalue相比,itervalues'dictiter_iternextitem还有其他部分。

iteritems

我认为元组创建可能会降低性能。

Python确实倾向于重用元组 tupleobject.c显示


    if (result->ob_refcnt == 1) {
        Py_INCREF(result);
        Py_DECREF(PyTuple_GET_ITEM(result, 0));
        Py_DECREF(PyTuple_GET_ITEM(result, 1));
    } else {
        result = PyTuple_New(2);
        if (result == NULL)
            return NULL;
    }
    di->len--;
    key = ep[i].me_key;
    value = ep[i].me_value;
    Py_INCREF(key);
    Py_INCREF(value);
    PyTuple_SET_ITEM(result, 0, key);
    PyTuple_SET_ITEM(result, 1, value);

这种优化只是意味着Python不会从头开始构建一些元组。但仍有许多工作要做。


案例:dict

如果将/* Speed optimization to avoid frequent malloc/free of small tuples */ #ifndef PyTuple_MAXSAVESIZE #define PyTuple_MAXSAVESIZE 20 /* Largest tuple to save on free list */ #endif #ifndef PyTuple_MAXFREELIST #define PyTuple_MAXFREELIST 2000 /* Maximum number of tuples of each size to save */ #endif 替换为OrderedDict,我认为第二种解决方案通常略胜一筹。
Python字典是使用哈希表实现的。所以查找速度很快。查找的平均时间复杂度为O(1),而最差的是O(n) 1 。 第一个解决方案的平均时间复杂度与第二个解决方案的时间复杂度相同。它们都是O(n)。 因此,第二种解决方案没有优势或者有时甚至更慢,特别是当输入数据很小时。 在这种情况下,由dict引起的额外费用无法得到补偿。

iteritems

输出

from collections import OrderedDict
from operator import itemgetter
from timeit import Timer
from random import randint, random

N = 100000
xs = [('a', 10), ('b', 9), ('c', 4), ('d', 7), ('e', 3), ('f', 0), ('g', -5), ('h', 9)]

od = OrderedDict(xs)
d = dict(xs)

def f1od_min():
    return min(od, key=od.get)

def f2od_min():
    return min(od.iteritems(), key=itemgetter(1))[0]

def f1d_min():
    return min(d, key=d.get)

def f2d_min():
    return min(d.iteritems(), key=itemgetter(1))[0]

def f1od():
    for k in od: pass

def f2od():
    for t in od.iteritems(): pass

def f1d():
    for k in d: pass

def f2d():
    for t in d.iteritems(): pass

print 'min'
print(Timer(stmt='f1od_min()', setup='from __main__ import f1od_min').timeit(number=N))
print(Timer(stmt='f2od_min()', setup='from __main__ import f2od_min').timeit(number=N))
print(Timer(stmt='f1d_min()', setup='from __main__ import f1d_min').timeit(number=N))
print(Timer(stmt='f2d_min()', setup='from __main__ import f2d_min').timeit(number=N))
print
print 'traverse'
print(Timer(stmt='f1od()', setup='from __main__ import f1od').timeit(number=N))
print(Timer(stmt='f2od()', setup='from __main__ import f2od').timeit(number=N))
print(Timer(stmt='f1d()', setup='from __main__ import f1d').timeit(number=N))
print(Timer(stmt='f2d()', setup='from __main__ import f2d').timeit(number=N))

然后将<{1}}和min 0.398274431527 0.813040903243 0.185168156847 0.249574387248 <-- dict/the second solution traverse 0.251634216081 0.642283865687 0.0565099754298 0.0958057518483 替换为

N

输出

xs

现在用

替换N = 50 xs = [(x, randint(1, 100)) for x in range(100000)] min 1.5148923257 3.47020082161 0.712828585756 0.70823812803 <-- dict/the second solution traverse 0.975989336634 2.92283956481 0.127676073356 0.253622387762
N

输出

xs

最后,第二种解决方案开始闪耀。


第一种解决方案的最坏情况:哈希冲突

N = 10
xs = [(random(), random()) for x in range(1000000)]

输出

min
6.23311265817
10.702984667
4.32852708934
2.87853889251    <-- dict/the second solution

traverse
2.06231783648
9.49360449443
1.33297618831
1.73723008092

1 为dict对象列出的平均大小写时间假定对象的散列函数足够强大,以使冲突不常见。平均情况假设参数中使用的密钥是从所有密钥集中随机均匀选择的。请参阅TimeComplexity

答案 1 :(得分:3)

对于元组重用,我不相信:

>>> a = (1,2)
>>> b = (1,2)
>>> id(a)
139912909456232
>>> id(b)
139912909456304
>>> 

你可以从int或string看到:

>>> a = 1
>>> b = 1
>>> id(a)
34961336
>>> id(b)
34961336
>>> 
>>> a = 'a'
>>> b = 'a'
>>> id(a)
139912910202240
>>> id(b)
139912910202240
>>> 

修改

对于dict,您的两种方法是相似的。试试吧:

>>> a = {'a':1, 'b':2, 'c':3}
>>> N = 100000
# really quick to use []
>>> Timer(stmt='for x in a: z = a[x]', setup='from __main__ import a').timeit(number=N)
0.0524289608001709
# use get method
>>> Timer(stmt='for x in a: z = a.get(x)', setup='from __main__ import a').timeit(number=N)
0.10028195381164551
# use iterator and []
>>> Timer(stmt='for x in a.iteritems(): z = x[1]', setup='from __main__ import a').timeit(number=N)
0.08019709587097168
# use itemgetter and iterator
>>> b = itemgetter(1)
>>> Timer(stmt='for x in a.iteritems(): z = b(x)', setup='from __main__ import a, b').timeit(number=N)
0.09941697120666504

虽然时间可能会改变,但一般来说都是准确的。使用iteritemsitemgetterget一样快。

但对于OrderedDict,让我们再试一次:

>>> a
OrderedDict([('a', 1), ('c', 3), ('b', 2)])
>>> N = 100000
#Use []
>>> Timer(stmt='for x in a: z = a[x]', setup='from __main__ import a').timeit(number=N)
0.2354598045349121
#Use get
>>> Timer(stmt='for x in a: z = a.get(x)', setup='from __main__ import a').timeit(number=N)
0.21950387954711914
#Use iterator
>>> Timer(stmt='for x in a.iteritems(): z = x[1]', setup='from __main__ import a').timeit(number=N)
0.29949188232421875
#Use iterator and itemgetter
>>> b = itemgetter(1)
>>> Timer(stmt='for x in a.iteritems(): z = b(x)', setup='from __main__ import a, b').timeit(number=N)
0.32039499282836914

您可以看到,对于OrderedDict,使用get,使用iteratoritemgetter的时间会有所不同。

所以,我认为时差是因为OrderedDict的实施。但对不起,我不知道为什么。

答案 2 :(得分:2)

正如您自己提到的,功能之间存在差异。

第一个函数迭代一个字符串列表,对于每个字符串,它会转到字典并查找它以获取值,然后找到最小值并返回。

第二个函数迭代字符串/ int对的元组。然后对于每一个它访问第二个项目(int / value)然后它找到最小值(在这种情况下是一个元组)然后它返回结果第一个项目。

第二个功能是在需要更多处理的对象(元组&gt;字符串)和(元组&gt;整数)以及附加项检索上做更多工作。

你为什么感到惊讶?

答案 3 :(得分:1)

扩展我的previous answer。为了更好地了解正在发生的事情,您始终可以使用dis模块。

>>> import dis
>>> dis.dis(f1)
              0 LOAD_GLOBAL              0 (min)
              3 LOAD_GLOBAL              1 (items)
              6 LOAD_CONST               1 ('key')
              9 LOAD_GLOBAL              1 (items)
             12 LOAD_ATTR                2 (get)
             15 CALL_FUNCTION          257
             18 RETURN_VALUE    
>>> dis.dis(f2)
              0 LOAD_GLOBAL              0 (min)
              3 LOAD_GLOBAL              1 (items)
              6 LOAD_ATTR                2 (iteritems)
              9 CALL_FUNCTION            0
             12 LOAD_CONST               1 ('key')
             15 LOAD_GLOBAL              3 (itemgetter)
             18 LOAD_CONST               2 (1)
             21 CALL_FUNCTION            1
             24 CALL_FUNCTION          257
             27 LOAD_CONST               3 (0)
             30 BINARY_SUBSCR       
             31 RETURN_VALUE   

正如你所看到的,在f2中发生了更多的事情(因此它可能会更慢)

您始终可以使用dis模块检查Python中的任何内容,它通常可以很好地指示哪些内容会更好。


可以使用timeit模块检查某些方法或函数的时序或性能,因为它们在特定类型的输入上执行,但有时可能会关闭时序,因为正在使用的样本数据集是更适合于某个功能而不是另一个功能,例如,在检查排序功能时,已经排序最多的列表将优先于某种类型的功能而不是另一种功能,这可能会更快地对排序较少的列表进行排序,但这些都不是考虑可能在列表内部的不同类型的数据。使用dis模块可以避免大部分内容,因为它能够直接看到幕后幕后(或幕后)正在做什么,这可以更清楚地指示哪种方法最适合执行某些任务