【问题标题】:Why is checking isinstance(something, Mapping) so slow?为什么检查 isinstance(something, Mapping) 这么慢?
【发布时间】:2017-07-11 17:58:30
【问题描述】:

我最近比较了 collections.Countersorted 的性能以进行比较检查(如果某些迭代包含相同数量的相同元素),而 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-check 之后不再调用该方法。那么为什么检查isinstance(iterable, Mapping) 这么慢呢?

【问题讨论】:

  • 所以你的问题真的归结为为什么isinstance 对抽象基类的检查很慢?因为我不认为这是可迭代的和Mapping 特定的。
  • @Mitch 可能,实际上似乎其他collections.abc 类在isinstance 检查中同样慢。你知道是什么让 abc 的这些检查如此缓慢吗? :)
  • 我现在正在研究__instancecheck__ 的实现,似乎没有发生什么太时髦的事情——当你错过缓存时性能会更差。打算再考虑一下。
  • isinstance 带有 ABCMeta 类是 Python 级别的操作,不幸的是。在ABCMeta.__instancecheck__ 中进行了许多检查,传递除映射以外的任何内容都将采用最慢的可用路径。

标签: python python-internals


【解决方案1】:

性能实际上只是与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]

包含我们的 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。在我们没有命中负缓存的情况下(例如 Series 的第一次迭代),Python 使用

进行常规子类检查
cls.__subclasscheck__(subclass)

这可能慢,诉诸子类挂钩和递归子类检查seen here,然后缓存结果以供后续加速。

【讨论】:

  • 这是一个愚蠢的问题,但我尝试在 Python 控制台中运行%lprun -f abc.ABCMeta.__instancecheck__ isinstance(list(), collections.abc.Mapping),但它不起作用......你如何让它显示每行代码的时间?
  • @dtgq %lprun 来自 line_profiler 工具,您必须按照链接中的说明将其加载到您的 IPython 扩展中。
猜你喜欢
  • 2011-03-11
  • 2017-01-21
  • 2020-03-19
  • 2014-03-12
  • 2011-02-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多