-
-
Notifications
You must be signed in to change notification settings - Fork 32k
gh-92041: Improve performance of inspect.getmodule #92042
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Every change to Python requires a NEWS entry. Please, add it using the blurb_it Web app or the blurb command-line tool. |
Every change to Python requires a NEWS entry. Please, add it using the blurb_it Web app or the blurb command-line tool. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Is the new case also covered by tests?
The recursion-break test suggested code objects should be distinguished by object instance rather than by their hash alone. I'm using id & hash together as best effort to uniquely identify code objects that have no module. I assume identity comparison with weakref would be the ideal solution, but the performance is O(N) rather than O(1), with N = len(sys.modules). The identity approximation seemed a reasonable trade-off to me. |
9bbf165
to
725a326
Compare
Rebased past some bad upstream that broke CI. You can view the weakref implementation I tested here.. Also worth noting, the tests I've shown were run by pasting into interactive console. With same test code in a module that is executed, it will run faster (though still slower than with this fix) -- |
Also it seems as though the CI has stalled since last update. Not sure if I need to make some random change and force a re-run, or if it's not running because I'm a "first-time contributor" -- the description is a bit vague. 4 expected checks, 2 workflows awaiting approval.. not sure what a 'workflow' vs a 'check' is. |
Reminder, columns are stack depth, rows are len(sys.modules), numbers are milliseconds. Without changes:
With this PR changes:
Also testing with weakref version:
Interesting! With this PR changes:
With weakref version:
I will take a closer look at what else |
Also the With this PR changes:
With weakref impl:
At higher module counts, either implementation uses less microseconds than the current implementation in milliseconds.. |
The docs indicate weakref doesn't work with code objects. They appear to work with simple tests in interactive console. However, debugging the weakref implementation I linked earlier, the _moduleless cache fills with dead weakrefs indexed by the same code object id(). The current use of id ^ hash seems optimal. I have previously explored defining _moduleless as an LRU cache. When testing I got the sense |
Hmm, looks like I need a maintainer to approve running tests. I can't seem to trigger them despite pressing all the buttons. |
@mdeck could you merge/rebase to current main? Perhaps that will trigger the tests to build |
Misc/NEWS.d/next/Library/2022-04-29-04-24-06.gh-issue-92041.B9JTOl.rst
Outdated
Show resolved
Hide resolved
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
Here, sys._getframe() is moduleless, and is cached:
# test3.py
import gc
import inspect
import random
import time
import sys
def run_test(module):
def print_line(h, *vals):
if not h:
print("%15s" % h, *["%9i" % v for v in vals], " <- stack depth")
else:
print("%15s" % h, *["%6.0f us" % v for v in vals])
#
len_sys_modules = [100, 1_000, 10_000]
def add_modules(n):
while len(sys.modules) < n:
sys.modules[f"foo_{random.randint(0,2**64)}"] = module
#
depths = [2, 8, 64]
def nest(depth):
if depth > 0:
return nest(depth-1)
t = time.time()
_ = inspect.stack()
dur = time.time() - t
return dur * 1000 * 1000
#
gc.collect()
if hasattr(inspect, "_moduleless"):
print(f"len moduleless: {len(inspect._moduleless)}")
inspect._moduleless.clear()
#
orig_modules = sys.modules.copy()
print_line("", *depths)
for len_modules in len_sys_modules:
add_modules(n=len_modules)
times = [nest(depth) for depth in depths]
print_line(len(sys.modules), *times)
sys.modules = orig_modules.copy()
print("len sys.modules")
run_test(random)
run_test(sys)
import resource
print(resource.getrusage(resource.RUSAGE_SELF)) |
getmodule maintains a cache of module names & associated file names.
Whenever it encounters a module filename not in its cache, it iterates
over sys.modules and repopulates its cache.
An object which has no associated module filename will trigger this
repopulation loop every time getmodule is called. The cost is easily
seen with inspect.getstack