为什么检查isinstance(某事,Mapping)这么慢?

时间:2017-02-21 21:54:43

标签: python python-internals

我最近比较了collections.Countersorted的性能进行比较检查(如果某些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)这么慢?

1 个答案:

答案 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,然后缓存后续加速的结果。