我最近比较了collections.Counter
与sorted
的性能进行比较检查(如果某些iterable包含相同数量的相同元素),而Counter
的大可迭代性能是通常优于sorted
,对于短迭代来说,它要慢得多。
使用line_profiler
瓶颈似乎是isinstance(iterable, collections.Mapping)
- 签入Counter.update
:
%load_ext line_profiler # IPython
lst = list(range(1000))
%lprun -f Counter.update Counter(lst)
给了我:
Timer unit: 5.58547e-07 s
Total time: 0.000244643 s
File: ...\lib\collections\__init__.py
Function: update at line 581
Line # Hits Time Per Hit % Time Line Contents
==============================================================
581 def update(*args, **kwds):
601 1 8 8.0 1.8 if not args:
602 raise TypeError("descriptor 'update' of 'Counter' object "
603 "needs an argument")
604 1 12 12.0 2.7 self, *args = args
605 1 6 6.0 1.4 if len(args) > 1:
606 raise TypeError('expected at most 1 arguments, got %d' % len(args))
607 1 5 5.0 1.1 iterable = args[0] if args else None
608 1 4 4.0 0.9 if iterable is not None:
609 1 72 72.0 16.4 if isinstance(iterable, Mapping):
610 if self:
611 self_get = self.get
612 for elem, count in iterable.items():
613 self[elem] = count + self_get(elem, 0)
614 else:
615 super(Counter, self).update(iterable) # fast path when counter is empty
616 else:
617 1 326 326.0 74.4 _count_elements(self, iterable)
618 1 5 5.0 1.1 if kwds:
619 self.update(kwds)
因此即使长度为1000次迭代,它也需要超过15%的时间。对于更短的可迭代物(例如20个项目,它增加到60%)。
我首先认为它与collections.Mapping
使用__subclasshook__
的方式有关,但该方法在第一次isinstance
之后未被调用 - 再检查。那么为什么检查isinstance(iterable, Mapping)
这么慢?
答案 0 :(得分:7)
性能实际上只与ABCMeta's __instancecheck__
中的一组检查相关联,isinstance
调用这些检查。
最重要的是,这里目睹的糟糕表现并不是一些缺失优化的结果,而是isinstance
的结果,其中抽象基类是Python级操作,如Jim所述。缓存了正面和负面结果,但即使使用缓存结果,您也只需在每个循环中查看几微秒,就可以遍历ABCMeta类的__instancecheck__
方法中的条件。
考虑一些不同的空结构。
>>> d = dict; l = list(); s = pd.Series()
>>> %timeit isinstance(d, collections.abc.Mapping)
100000 loops, best of 3: 1.99 µs per loop
>>> %timeit isinstance(l, collections.abc.Mapping)
100000 loops, best of 3: 3.16 µs per loop # caching happening
>>> %timeit isinstance(s, collections.abc.Mapping)
100000 loops, best of 3: 3.26 µs per loop # caching happening
我们可以看到性能差异 - 是什么原因造成的?
对于词典
>>> %lprun -f abc.ABCMeta.__instancecheck__ isinstance(dict(), collections.abc.Mapping)
Timer unit: 6.84247e-07 s
Total time: 1.71062e-05 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
178 def __instancecheck__(cls, instance):
179 """Override for isinstance(instance, cls)."""
180 # Inline the cache checking
181 1 7 7.0 28.0 subclass = instance.__class__
182 1 16 16.0 64.0 if subclass in cls._abc_cache:
183 1 2 2.0 8.0 return True
184 subtype = type(instance)
185 if subtype is subclass:
186 if (cls._abc_negative_cache_version ==
187 ABCMeta._abc_invalidation_counter and
188 subclass in cls._abc_negative_cache):
189 return False
190 # Fall back to the subclass check.
191 return cls.__subclasscheck__(subclass)
192 return any(cls.__subclasscheck__(c) for c in {subclass, subtype})
列表
>>> %lprun -f abc.ABCMeta.__instancecheck__ isinstance(list(), collections.abc.Mapping)
Timer unit: 6.84247e-07 s
Total time: 3.07911e-05 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
178 def __instancecheck__(cls, instance):
179 """Override for isinstance(instance, cls)."""
180 # Inline the cache checking
181 1 7 7.0 15.6 subclass = instance.__class__
182 1 17 17.0 37.8 if subclass in cls._abc_cache:
183 return True
184 1 2 2.0 4.4 subtype = type(instance)
185 1 2 2.0 4.4 if subtype is subclass:
186 1 3 3.0 6.7 if (cls._abc_negative_cache_version ==
187 1 2 2.0 4.4 ABCMeta._abc_invalidation_counter and
188 1 10 10.0 22.2 subclass in cls._abc_negative_cache):
189 1 2 2.0 4.4 return False
190 # Fall back to the subclass check.
191 return cls.__subclasscheck__(subclass)
192 return any(cls.__subclasscheck__(c) for c in {subclass, subtype})
我们可以看到,对于一个字典,映射抽象类' _abc_cache
>>> list(collections.abc.Mapping._abc_cache)
[dict]
包括我们的字典,因此检查会尽早发生短路。对于列表显然,正缓存不会被命中,但映射的_abc_negative_cache
包含列表类型
>>> list(collections.abc.Mapping._abc_negative_cache)
[type,
list,
generator,
pandas.core.series.Series,
itertools.chain,
int,
map]
以及现在的pd.Series类型,因为使用isinstance
多次调用%timeit
。如果我们没有点击负面缓存(就像系列的第一次迭代一样),那么Python将使用
cls.__subclasscheck__(subclass)
可能远更慢,使用子类钩子和递归子类检查seen here,然后缓存后续加速的结果。