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

Why is checking isinstance(something, Mapping) so slow?

我最近将 collections.Counter 的性能与 sorted 的性能进行比较检查(如果某些可迭代包含具有相同数量的相同元素),而 [=14= 的大可迭代性能] 通常比 sorted 好,它对于短迭代来说要慢得多。

使用 line_profiler 瓶颈似乎是 isinstance(iterable, collections.Mapping)-check in 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-check 之后不再调用该方法。那么为什么检查 isinstance(iterable, Mapping) 这么慢?

性能实际上仅与 ABCMeta's __instancecheck__, which is called by isinstance 中的一组检查有关。

最重要的是,此处出现的性能不佳并不是缺少优化的结果,而只是 isinstance 的结果,抽象基数 classes 是 Python 级操作,如 Jim 所述。正面和负面结果都会被缓存,但即使有缓存结果,您每次循环也需要花费几微秒的时间来遍历 ABCMeta class 的 __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})

我们可以看到对于一个dict,Mapping abstract classes' _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 类型,因为用 %timeit 多次调用 isinstance。在我们没有命中负缓存的情况下(比如系列的第一次迭代),Python 求助于常规的 subclass 检查

cls.__subclasscheck__(subclass)

可以 far 慢,求助于 subclass 钩子和递归 subclass 检查 seen here,然后缓存结果用于后续加速。