I Tried to Reduce Pylint Memory Usage
rtpg.co
rtpg.co
A few years ago I got fed up with how slow Pylint was, so I made some changes to improve its performance. The main issue was that the tree traversal code was written generically. It was nice and elegant in terms of readability, but it meant that there was a lot of unnecessary runtime type-checking. It also meant that a lot of work was getting done for no reason. For example, if you are trying to apply lint rules for assign statements, you only want to check places where assign statement can legally occur. But Pylint was checking every single node for assign statements, including places where they cannot occur, such as inside function calls. Breaking up that generic logic into specialized instances had an enormous impact on performance.
Here are the PRs that implemented these changes:
https://github.com/PyCQA/astroid/pull/497
https://github.com/PyCQA/astroid/pull/519
https://github.com/PyCQA/astroid/pull/552I've worked around the python linter and it is written in C. The bits in python (especially our custom rules) were horribly slow.
Lint is building a tree of the code and going through it recursively. Going through a large tree in pure python is horribly slow, it has to interpret every single line of code again and resolve every variable again, because they could change anytime, the overhead is insane.
It's the one thing that python cannot do, parsing tree. If python had a JIT it might be doable but currently it isn't.
The contextvars polyfill brings in the one thing I really need from Py 3.7.
I have a toolkit that puffs up XML, JSON, whatever files into an RDF graph that has extra blank nodes that let you annotate anything. A year ago I was complaining about how slow it was, with PyPy it is 5 times faster and I am not complaining.
I hear people get similar speed-ups for branchy monte carlo simulations too.
Compiles mypy-annotated Python to a Python C extension. Claims a ~4x speedup, but still quite unstable/buggy, so who knows how much performance they'll have to give up for working software. Fingers crossed!
For instance, pipelining an operation to allow multiple CPUs to work on a problem at once can reduce the pressure on the tall tent pole, be it IO bottlenecks or one phase. Time is still dictated by the tallest tent poles, but not also by the fourth through tenth tallest poles.
But now everything is happening at once, which means all of the temporary data structures exist in memory in parallel, instead of sequentially.
Heavily paraphrasing someone else: The simple things get taken care of early. If we're standing around talking about problems in a successful tool, they're complicated. Although I disagree with that person on one point: we can also be talking about problems that were complicated to diagnose (but straightforward to fix).
An Exception has the traceback, and the traceback has all the frames of the call stack, and each frame has a reference to the local variables in that frame.
Keeping an Exception around past its time can yield a huge tangle of circular references that puts a lot of pressure on the GC.
I reduced memory utilization dramatically by deleting 1 line of code.
If you were in the position of having an app that was set up like this, it would be nice to know you had the option of trimming the Exception's footprint while still allowing it to retain its Exception-ness.
In the end I ended up adding __slots__ to two classes and the memory usage shrunk by 2/3 (there were a lot of those two objects).
I also managed to double the speed by re-writing (re-wording really) a bit of string handling which just happened to be the bottleneck. Can't remember the details now but it was a trivial code change.
Profiling for the win!
We can work at a higher level.
However, the old salts who had to care about every byte are rightly horrified at the lack of frugality today.
If you are on a cloud, you should always consider using cloud native functions and services. Keeping your toe out of the lock-in is expensive :(
That said, I have not seen this done with lisp. I would assume it would look a lot like this.
Though theoretically any interpreted language gives you all of this, you actually might not have access to the internals in practice. JS (well, node.js and browsers) doesn’t expose GC internals (partly due to a spec requirement that the language be deterministic), and barely gives you good exception introspection.
There is also the ergonomic advantage of no static typing: you just get a reference and can figure out the type later. Very useful when poking around unknown objects. I imagine an API like this in Java would be much more verbose and requiring a lot of casts etc.
So, yeah, if your want is to inspect using the host language, I can see how this makes sense. That said, as you noted in the post, you really just want to get to the root asap. Which is why most visualizations will start from there. :D
https://github.com/borkdude/clj-kondo/issues/1036
The reason of the memory leak was: I used a memoized function on some argument that should be GC-ed after one run, but as memoized functions store their arguments for future comparison, it was kept in memory forever.