Skip to content

Improve performance of isinstance() for misses and improve documentation #159580

Open

Description

@eendebakpt

Feature or enhancement

When isinstance(obj, cls) fails the PyObject_TypeCheck() test, object_isinstance() looks up obj.__class__ through the full attribute machinery to handle proxies that override __class__. For ordinary objects that lookup resolves to object's own __class__ getset descriptor, so it calls the getset, type-checks, increfs the type, compares it to Py_TYPE(obj) and decrefs it again, all to confirm what was already known.

This PR checks whether __class__ resolves to object's getset descriptor for a type that uses generic attribute lookup. If so the answer is False without any call. Every isinstance() call that returns False with an exact type as the second argument benefits, including each non-matching entry of a tuple of types, which is the common isinstance(o, (str, int, float, ...)) dispatch pattern.

The isinstance() documentation now states that a __class__ attribute differing from type(object) is also consulted, which was undocumented.

Benchmarks

Running isinstance(point, Other) with point a dataclass

case main PR speedup p10..p90
ctl: a + b (ints) 49.7 ns 49.2 ns 1.00x 0.94..1.08
ctl: s1 + s2 (str) 67.6 ns 67.7 ns 1.00x 0.96..1.08
isinstance(1, int) (exact hit) 55.5 ns 55.2 ns 1.00x 0.95..1.10
isinstance(1, str) (False) 74.1 ns 64.0 ns 1.15x 1.09..1.26
isinstance(obj, Other) (False) 69.7 ns 59.6 ns 1.16x 1.09..1.26
isinstance(1, (str, bytes, float)) (3 misses) 164.8 ns 135.3 ns 1.22x 1.13..1.32
isinstance(1, (str, int)) (miss, hit) 109.5 ns 100.1 ns 1.11x 1.05..1.17

On the free-threaded build the gain is larger under contention. On main the __class__ lookup returns a new reference to the instance's type, so when the instance is of a heap type (any Python class) every thread does an atomic incref/decref on the shared type object. The fast path takes no reference.

threads main PR main / PR
1 7.1 ms 6.3 ms 1.12x
2 8.6 ms 6.9 ms 1.25x
4 22.3 ms 7.4 ms 3.01x
8 56.3 ms 12.3 ms 4.57x

dataclasses.is_dataclass(point), whose first step is a failing isinstance(obj, type):

threads main PR main / PR
1 19.6 ms 16.7 ms 1.18x
2 24.0 ms 21.4 ms 1.12x
4 40.7 ms 37.5 ms 1.08x
8 83.0 ms 75.2 ms 1.10x
Benchmark scripts

Contended runs (free-threaded build):

import time, threading, dataclasses

@dataclasses.dataclass
class Point:
    x: int
    y: int

class Other:
    pass

is_dataclass = dataclasses.is_dataclass
WORK = 100_000

def case_micro():
    pt = Point(1, 2)          # per-thread instance; the shared object is the type
    for _ in range(WORK):
        isinstance(pt, Other)

def case_is_dataclass():
    pt = Point(1, 2)
    for _ in range(WORK):
        is_dataclass(pt)

def run(fn, nthreads):
    ts = [threading.Thread(target=fn) for _ in range(nthreads)]
    t0 = time.perf_counter()
    for t in ts: t.start()
    for t in ts: t.join()
    return time.perf_counter() - t0

for name, fn in (("isinstance(dc, Other)", case_micro), ("is_dataclass(dc)", case_is_dataclass)):
    for n in (1, 2, 4, 8):
        print(name, n, f"{min(run(fn, n) for _ in range(3)) * 1e3:.1f} ms")

Single-threaded table:

import pyperf

runner = pyperf.Runner()
runner.timeit("isinstance(1, int)  (exact hit, control)", "isinstance(x, int)", setup="x = 1")
runner.timeit("isinstance(1, str)  (False)", "isinstance(x, str)", setup="x = 1")
runner.timeit("isinstance(obj, Other)  (False)", "isinstance(c, D)",
              setup="C = type('C', (), {}); D = type('D', (), {}); c = C()")
runner.timeit("isinstance(1, (str, bytes, float))  (3 misses)", "isinstance(x, (str, bytes, float))", setup="x = 1")
runner.timeit("isinstance(1, (str, int))  (miss, hit)", "isinstance(x, (str, int))", setup="x = 1")

Has this already been discussed elsewhere?

No discussion on the optimization, but the behavior of ininstance has been discussed a lot:

Linked PRs

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usagetype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions