-
-
Notifications
You must be signed in to change notification settings - Fork 309
Performance improvements to counter context.clone slowdown #1009
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
Conversation
This change abuses mutable references to create a sort of interior mutable cell shared between a context and all of its clones. The idea is that when a node is inferred at the toplevel, it is called with context = None, creating a new InferenceContext and starting a count from zero. However, when a context is cloned we re-use the cell and cause the count in the "parent" context to be incremented when nodes are inferred in the "child" context.
hippo91
left a comment
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.
| e.g. the bound node of object.__new__(cls) is the object node | ||
| """ | ||
| return _INFERENCE_CACHE |
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.
If i understand well, here we will store every inferred node in a global variable that will exists throughout the life of the program.
It will definitely allow cpu time sparing but what about memory consumption?
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.
Is the number of inferred node limit sufficient for the speed up ? There are really big code bases using pylint and having a cache can affects them to the point it's not possible to run pylint on them at all.
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.
If i understand well, here we will store every inferred node in a global variable that will exists throughout the life of the program.
It will definitely allow cpu time sparing but what about memory consumption?
Your understanding is correct. It could theoretically grow without bound, but I suppose many other parts of astroid (such as astroid.MANAGER.astroid_cache) can as well. I regularly see pylint/astroid runs in the hundreds of MiB, but without a benchmark, I'm not sure there's a meaningful way to answer this question.
I tried running pylint (commit 9361fa0630ce920081b57865be7882a39e9da1e3) on itself with and without these astroid changes.
With wip/performance (0d7d798):
26.77user 0.25system 0:27.08elapsed 99%CPU (0avgtext+0avgdata 231516maxresident)k
3312inputs+248outputs (3major+81935minor)pagefaults 0swaps
Without (d3fea8f):
45.15user 0.24system 0:45.43elapsed 99%CPU (0avgtext+0avgdata 235892maxresident)k
0inputs+336outputs (0major+83907minor)pagefaults 0swaps
So memory usage (max RSS) went down from 235MB -> 231MB
Is the number of inferred node limit sufficient for the speed up ? There are really big code bases using pylint and having a cache can affects them to the point it's not possible to run pylint on them at all.
They address different problems: the inferred node limit is to stop pathological cases, like really large files. The inferred node cache helps with re-use, like when scanning a package with a lot of files. The main problem I saw with the inference cache was that it was re-built from scratch for every node so the hit-rate was very low, and cloning it introduced a large number of small copies which was inefficient time-wise. In fact, it was so inefficient that just removing it altogether produces a small but noticeable (~2%) speed-up on astroid 2.5.6 (middle of 3 runs):
astroid-2.5.6 (36dda3f):
38.57user 0.26system 0:38.86elapsed 99%CPU (0avgtext+0avgdata 231020maxresident)k
0inputs+72outputs (0major+81740minor)pagefaults 0swaps
With no-cache.patch applied:
37.77user 0.25system 0:38.05elapsed 99%CPU (0avgtext+0avgdata 230788maxresident)k
0inputs+72outputs (0major+81551minor)pagefaults 0swaps
no-cache.patch:
diff --git a/astroid/context.py b/astroid/context.py
index 18220ec..73f1f4d 100644
--- a/astroid/context.py
+++ b/astroid/context.py
@@ -27,11 +27,10 @@ class InferenceContext:
"lookupname",
"callcontext",
"boundnode",
- "inferred",
"extra_context",
)
- def __init__(self, path=None, inferred=None):
+ def __init__(self, path=None):
self.path = path or set()
"""
:type: set(tuple(NodeNG, optional(str)))
@@ -64,14 +63,6 @@ class InferenceContext:
e.g. the bound node of object.__new__(cls) is the object node
"""
- self.inferred = inferred or {}
- """
- :type: dict(seq, seq)
-
- Inferred node contexts to their mapped results
- Currently the key is ``(node, lookupname, callcontext, boundnode)``
- and the value is tuple of the inferred results
- """
self.extra_context = {}
"""
:type: dict(NodeNG, Context)
@@ -102,7 +93,7 @@ class InferenceContext:
starts with the same context but diverge as each side is inferred
so the InferenceContext will need be cloned"""
# XXX copy lookupname/callcontext ?
- clone = InferenceContext(self.path, inferred=self.inferred)
+ clone = InferenceContext(self.path)
clone.callcontext = self.callcontext
clone.boundnode = self.boundnode
clone.extra_context = self.extra_context
diff --git a/astroid/node_classes.py b/astroid/node_classes.py
index 7faf681..089b579 100644
--- a/astroid/node_classes.py
+++ b/astroid/node_classes.py
@@ -358,11 +358,6 @@ class NodeNG:
yield from self._infer(context, **kwargs)
return
- key = (self, context.lookupname, context.callcontext, context.boundnode)
- if key in context.inferred:
- yield from context.inferred[key]
- return
-
generator = self._infer(context, **kwargs)
results = []
@@ -378,7 +373,6 @@ class NodeNG:
# Cache generated results for subsequent inferences of the
# same node using the same context
- context.inferred[key] = tuple(results)
return
def _repr_name(self):
Pierre-Sassoulas
left a comment
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.
👍
|
I'll take a look at it later today |
cdce8p
left a comment
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.
I can confirm the speed improvements. However this change causes some StopIteration exceptions when testing against HomeAssisant: https://github.com/cdce8p/ha-core/runs/2753897777?check_suite_focus=true#step:7:55
|
@cdce8p, I was able to fix the but these don't seem to be related. I've raised pylint-dev/pylint#4546 since I was able to replicate a false-positive with astroid 2.5.6. |
|
@nelfin Thanks for fixing the issue! Indeed the |
Pierre-Sassoulas
left a comment
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.
I think this is mergeable, as the caching also got better. Do you agree @hippo91 ?
|
Yes it seems to be ok. |
|
Thank you a lot for this, @nelfin , great performance improvement, and the information you gave were super detailed and informative 🔥 |
|
Yep thanks a lot @nelfin ! Great job! |
|
I can only agree, thanks @nelfin 🚀 |
Steps
Description
This adds a couple of changes to
InferenceContextto eliminate copies of theinferrednodes cache and limit the total number of inferred nodes in a context and all of its clones (preventing deep inference chains).Type of Changes
Related Issue
Ref #1003