Skip to content

Conversation

@nelfin
Copy link
Contributor

@nelfin nelfin commented Jun 3, 2021

Steps

  • For new features or bug fixes, add a ChangeLog entry describing what your PR does.
  • Write a good description on what the PR does.

Description

This adds a couple of changes to InferenceContext to eliminate copies of the inferred nodes cache and limit the total number of inferred nodes in a context and all of its clones (preventing deep inference chains).

Type of Changes

Type
✨ New feature
🔨 Refactoring

Related Issue

Ref #1003

@nelfin nelfin force-pushed the wip/performance branch from d1dc22d to 865ca65 Compare June 3, 2021 22:56
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.
@nelfin nelfin force-pushed the wip/performance branch from 865ca65 to 0d7d798 Compare June 3, 2021 23:14
@nelfin nelfin changed the title WIP: performance improvements to counter context.clone slowdown Performance improvements to counter context.clone slowdown Jun 3, 2021
@nelfin nelfin marked this pull request as ready for review June 3, 2021 23:18
Copy link
Contributor

@hippo91 hippo91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @nelfin for this smart MR!
It seems to be ok. I just have a question regarding memory consumption.
By the way i would really appreciate the review of @cdce8p.
I think it is also necessary to test this MR against pylint but maybe have you already done it.

e.g. the bound node of object.__new__(cls) is the object node
"""
return _INFERENCE_CACHE
Copy link
Contributor

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?

Copy link
Member

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.

Copy link
Contributor Author

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):

@hippo91 hippo91 self-assigned this Jun 5, 2021
Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@cdce8p
Copy link
Member

cdce8p commented Jun 5, 2021

I'll take a look at it later today

Copy link
Member

@cdce8p cdce8p left a 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 cdce8p added this to the 2.5.8 milestone Jun 6, 2021
@nelfin nelfin force-pushed the wip/performance branch from 0d7d798 to 8efd924 Compare June 7, 2021 05:12
@nelfin
Copy link
Contributor Author

nelfin commented Jun 7, 2021

@cdce8p, I was able to fix the StopIteration bubbling up through safe_infer by just catching it, but I haven't been able to come up with a good way to write a regression test for it. Re-running this on your ha-core fork, I got some new messages on homeassistant/components/mysensors/light.py:

************* Module homeassistant.components.mysensors.light
homeassistant/components/mysensors/light.py:157:23: E1120: No value for argument 'iG' in function call (no-value-for-parameter)
homeassistant/components/mysensors/light.py:157:23: E1120: No value for argument 'iB' in function call (no-value-for-parameter)

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.

@cdce8p
Copy link
Member

cdce8p commented Jun 7, 2021

@nelfin Thanks for fixing the issue! Indeed the no-value-for-parameter error seems to be unrelated.

@cdce8p cdce8p added the pylint-tested PRs that don't cause major regressions with pylint label Jun 7, 2021
Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a 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 ?

@hippo91
Copy link
Contributor

hippo91 commented Jun 7, 2021

Yes it seems to be ok.

@Pierre-Sassoulas Pierre-Sassoulas merged commit cf6528c into pylint-dev:master Jun 7, 2021
@Pierre-Sassoulas
Copy link
Member

Thank you a lot for this, @nelfin , great performance improvement, and the information you gave were super detailed and informative 🔥

@hippo91
Copy link
Contributor

hippo91 commented Jun 7, 2021

Yep thanks a lot @nelfin ! Great job!

@cdce8p
Copy link
Member

cdce8p commented Jun 7, 2021

I can only agree, thanks @nelfin 🚀

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

Labels

pylint-tested PRs that don't cause major regressions with pylint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants