Skip to content

[mypyc] Cache code objects used for traceback entries - #22476

Open
Jahongir-Qurbonov wants to merge 1 commit into
python:masterfrom
Jahongir-Qurbonov:mypyc-ft-exception-leak
Open

Jahongir-Qurbonov wants to merge 1 commit into
python:masterfrom
Jahongir-Qurbonov:mypyc-ft-exception-leak

Conversation

@Jahongir-Qurbonov

Copy link
Copy Markdown
Contributor

CPy_AddTraceback creates a new code object for every traceback entry added by native code. On free-threaded builds, code objects use deferred reference counting, so only the cyclic GC can free them. The GC only runs when the eval breaker is handled, which never happens in a native loop that raises and catches exceptions without calling interpreted code. As a result, each iteration leaks a dead code object until the native function returns.

For example, on 3.14t this grows RSS by about 500 bytes per iteration, which is about 1.4 GB for 3M iterations in a single call:

def f(n: int) -> int:
    cnt = 0
    for i in range(n):
        try:
            raise TypeError("x")
        except TypeError:
            cnt += 1
    return cnt

This PR caches the code objects in a small direct-mapped table keyed by filename, function name and line, guarded by a mutex on free-threaded builds. That fixes the memory growth. It also makes raising exceptions in native code faster on every build, because creating a code object is relatively expensive: the loop above runs about 3.6x faster on 3.14t and 2.2x faster on 3.14.

Added a run test that checks the traceback entries are still correct, that code objects are reused, and that a raise/catch loop doesn't create code-object garbage.

`CPy_AddTraceback` created a new code object for every traceback entry
added in native code. On free-threaded builds code objects use deferred
reference counting, so they are only freed by the cyclic GC, and the GC
only runs when the eval breaker is handled. A native loop that raises
and catches exceptions without calling interpreted code never gets
there, so each iteration leaked a dead code object until the native
function returned. For example, a loop doing
`try: raise TypeError("x") except TypeError: ...` grew RSS by ~500
bytes per iteration (~1.4 GB for 3M iterations in a single call).

Cache the code objects in a small direct-mapped table keyed by
filename, function name and line, protected by a mutex on free-threaded
builds. This fixes the memory growth, and it also makes raising
exceptions in native code faster on all builds, since creating a code
object is relatively expensive (the loop above got ~3.6x faster on
3.14t and ~2.2x faster on 3.14).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant