Skip to content

Redundant / unexpected function call _Py_Dealloc for MacOS ARM #154401

Description

@johng

Bug report

Bug description:

_Py_Dealloc(PyObject *op)
{
    PyTypeObject *type = Py_TYPE(op);
    unsigned long gc_flag = type->tp_flags & Py_TPFLAGS_HAVE_GC;
    destructor dealloc = type->tp_dealloc;
    PyThreadState *tstate = _PyThreadState_GET();
    intptr_t margin = _Py_RecursionLimit_GetMargin(tstate);
    if (margin < 2 && gc_flag) {
        _PyTrash_thread_deposit_object(tstate, (PyObject *)op);
        return;
    }

In _Py_Dealloc margin is calculated regardless of gc_flag flag value, when False we can easily short circuit. In x86 this fortunately is a noop due to the inlining however when compiled on MacOS Arm calculating the _PyThreadState_GET results in a function call.

macOS / arm64 — 9 instructions, a call, and a forced stack frame:
asm
stp  x29, x30, [sp, #-16]!          ; must save LR — the function is no longer a leaf!
mov  x29, sp
adrp x0, _tls_var@TLVPPAGE          ; ┐ address of the TLV descriptor
ldr  x0, [x0, _tls_var@TLVPPAGEOFF] ; ┘
ldr  x8, [x0]                       ; x8 = descriptor->thunk
blr  x8                             ; ← CALL the tlv thunk
ldr  x0, [x0]                       ; load the value
ldp  x29, x30, [sp], #16
ret

Linux / arm64 — 4 instructions, no call, leaf function (same ISA as above!):
asm
mrs  x8, TPIDR_EL0                  ; read the thread-pointer register
add  x8, x8, :tprel_hi12:tls_var    ; ┐ + link-time-constant offset
add  x8, x8, :tprel_lo12_nc:tls_var ; ┘
ldr  x0, [x8]                       ; load the value
ret

Gating the margin calculation under the gc_flag led to a small speedup on various small benchmarks.

bench            cleanFT   cleanFT+A2   Δ%min
------------------------------------------------
spectralnorm     115.458   112.095    -2.91
attr_method      163.626   159.857    -2.30
dict_ops         165.932   162.425    -2.11
nbody            108.780   106.605    -2.00
fannkuch         173.800   172.958    -0.48
json_roundtrip   159.722   159.215    -0.32
binary_trees      89.958    90.641    +0.76
fib              151.742   153.787    +1.35

Bench

Details
import gc, json, sys, time
perf = time.perf_counter


def bench_nbody(loops=40000):
    PI = 3.141592653589793; SOLAR_MASS = 4 * PI * PI; DAYS = 0.01
    bodies = {
        'sun': ([0.0,0.0,0.0],[0.0,0.0,0.0],SOLAR_MASS),
        'j': ([4.84143144246472090e+00,-1.16032004402742839e+00,-1.03622044471123109e-01],
              [1.66007664274403694e-03*365.24,7.69901118419740425e-03*365.24,-6.90460016972063023e-05*365.24],9.54791938424326609e-04*SOLAR_MASS),
        's': ([8.34336671824457987e+00,4.12479856412430479e+00,-4.03523417114321381e-01],
              [-2.76742510726862411e-03*365.24,4.99852801234917238e-03*365.24,2.30417297573763929e-05*365.24],2.85885980666130812e-04*SOLAR_MASS),
        'u': ([1.28943695621391310e+01,-1.51111514016986312e+01,-2.23307578892655734e-01],
              [2.96460137564761618e-03*365.24,2.37847173959480950e-03*365.24,-2.96589568540237556e-05*365.24],4.36624404335156298e-05*SOLAR_MASS),
        'n': ([1.53796971148509165e+01,-2.59193146099879641e+01,1.79258772950371181e-01],
              [2.68067772490389322e-03*365.24,1.62824170038242295e-03*365.24,-9.51592254519715870e-05*365.24],5.15138902046611451e-05*SOLAR_MASS),
    }
    syst = list(bodies.values()); pairs = [(syst[i], syst[j]) for i in range(len(syst)) for j in range(i+1, len(syst))]
    for _ in range(loops):
        for (([x1,y1,z1],v1,m1),([x2,y2,z2],v2,m2)) in pairs:
            dx=x1-x2; dy=y1-y2; dz=z1-z2
            mag=DAYS*((dx*dx+dy*dy+dz*dz)**(-1.5))
            b1m=m1*mag; b2m=m2*mag
            v1[0]-=dx*b2m; v1[1]-=dy*b2m; v1[2]-=dz*b2m
            v2[0]+=dx*b1m; v2[1]+=dy*b1m; v2[2]+=dz*b1m
        for (r,v,m) in syst:
            r[0]+=DAYS*v[0]; r[1]+=DAYS*v[1]; r[2]+=DAYS*v[2]
    return syst[0][0][0]


def bench_fannkuch(n=9):
    perm1=list(range(n)); count=list(range(n)); max_flips=0; m=n-1; r=n; check=0
    perm=perm1[:]
    while True:
        while r != 1: count[r-1]=r; r-=1
        perm=perm1[:]; flips=0; k=perm[0]
        while k:
            perm[:k+1]=perm[k::-1]; flips+=1; k=perm[0]
        if flips>max_flips: max_flips=flips
        while True:
            if r==n: return max_flips
            perm1.insert(r, perm1.pop(0)); count[r]-=1
            if count[r]>0: break
            r+=1


def bench_spectralnorm(n=200):
    def A(i,j): return 1.0/((i+j)*(i+j+1)//2+i+1)
    def mulAv(v):
        return [sum(A(i,j)*v[j] for j in range(len(v))) for i in range(len(v))]
    def mulAtv(v):
        return [sum(A(j,i)*v[j] for j in range(len(v))) for i in range(len(v))]
    u=[1.0]*n; v=[]
    for _ in range(10):
        v=mulAtv(mulAv(u)); u=mulAtv(mulAv(v))
    vBv=sum(u[i]*v[i] for i in range(n)); vv=sum(v[i]*v[i] for i in range(n))
    return (vBv/vv)**0.5


def bench_fib(n=29, loops=5):
    def fib(k):
        if k < 2: return k
        return fib(k-1)+fib(k-2)
    s=0
    for _ in range(loops): s+=fib(n)
    return s


def bench_binary_trees(depth=12):
    def make(d):
        if d==0: return (None,None)
        return (make(d-1),make(d-1))
    def check(node):
        l,r=node
        return 1 if l is None else 1+check(l)+check(r)
    total=0
    for _ in range(4):
        for d in range(4,depth,2):
            iters=2**(depth-d+4)
            for _ in range(iters): total+=check(make(d))
    return total


def bench_json_roundtrip(loops=1200):
    import json as _j
    data=[{"id":i,"name":"item%d"%i,"vals":[i,i*2,i*3],"ok":(i%2==0)} for i in range(200)]
    s=0
    for _ in range(loops):
        text=_j.dumps(data); back=_j.loads(text); s+=len(back)
    return s


def bench_dict_ops(loops=7000):
    s=0
    for _ in range(loops):
        d={str(i): i for i in range(300)}
        for k in d: s+=d[k]
    return s


def bench_attr_method(loops=1200000):
    class Vec:
        __slots__=("x","y","z")
        def __init__(s,x,y,z): s.x=x; s.y=y; s.z=z
        def add(s,o): return Vec(s.x+o.x, s.y+o.y, s.z+o.z)
        def dot(s,o): return s.x*o.x+s.y*o.y+s.z*o.z
    a=Vec(1.0,2.0,3.0); b=Vec(4.0,5.0,6.0); acc=0.0
    for _ in range(loops):
        c=a.add(b); acc+=c.dot(a)
    return acc


BENCHES = {
    "nbody":         (bench_nbody, {}),
    "fannkuch":      (bench_fannkuch, {}),
    "spectralnorm":  (bench_spectralnorm, {}),
    "fib":           (bench_fib, {}),
    "binary_trees":  (bench_binary_trees, {}),
    "json_roundtrip":(bench_json_roundtrip, {}),
    "dict_ops":      (bench_dict_ops, {}),
    "attr_method":   (bench_attr_method, {}),
}


def run(repeat):
    out={}
    for name,(fn,kw) in BENCHES.items():
        fn(**kw)  # warmup / specialize
        samples=[]
        for _ in range(repeat):
            gc.collect(); gc.disable()
            t0=perf(); fn(**kw); samples.append(perf()-t0)
            gc.enable()
        samples.sort()
        out[name]={"min":samples[0],"median":samples[len(samples)//2]}
    return out


if __name__ == "__main__":
    repeat=int(sys.argv[1]) if len(sys.argv)>1 else 5
    print("RESULT "+json.dumps(run(repeat)))

CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions