Do you write data science code? Do you struggle loading large amounts of data or wonder what parts
of your code use the maximum amount of memory? Maybe you just want to require smaller compute
resources, servers, RAM, and so on. If so, this episode is for you. We have Itamar Turner-Trowing,
creator of the Python data science memory profiler Phil, here to talk about memory usage
and data science. This is Talk Python to Me, episode 274, recorded July 8th, 2020.
Welcome to Talk Python to Me, a weekly podcast on Python, the language, the libraries, the ecosystem,
and the personalities. This is your host, Michael Kennedy. Follow me on Twitter where I'm @mkennedy.
Keep up with the show and listen to past episodes at talkpython.fm, and follow the show on Twitter
via at Talk Python. This episode is brought to you by Linode and us. Do you want to learn Python,
but you can't bear to subscribe to yet another service? At Talk Python Training, we hate subscriptions
too. That's why our course bundle gives you full access to the entire library of courses
for one fair price. That's right. With the course bundle, you save 70% off the full price of our
courses, and you own them all forever. That includes courses published at the time of the purchase,
as well as courses released within about a year of the bundle. So stop subscribing and start learning
at talkpython.fm/everything.
Hey, Tamar. Welcome to Talk Python to Me.
Hi, great to be here.
Yeah, it's great to have you here. I'm excited to talk about Python and memory.
Yeah, me too.
Yeah, and I think it's something that doesn't really get as much coverage as I think it deserves in the
Python space. You know, if you're a Java developer or a .NET developer, people go on and on and on about
optimizing the GC and tweaking this thing or that thing or your code or algorithms for memory management.
If you're a C developer, you're constantly in fear of memory leaks and memory management. And in Python,
we get it just kind of coast.
Or not. And so my motivation for getting into this was doing some scientific computing with basically a giant pile of images,
and we'd have to extract information from them. And I initially just focused on getting it working.
And then one day, I said, Okay, we're running this on these cloud computers. And it's taking, you know, 18 hours to process the data. Like most of the CPUs are idle, because you're using so much memory. I wonder if this is a problem.
And so I did some math, and I talked to management about our expected revenue. And it turns out we were going to spend like 70% of our expected revenue just on cloud computing, given my current implementation, which wouldn't have let any, there wouldn't be any left over for it.
Were they excited about that? Or were they not so excited?
I didn't mention this. I went and optimized it. And then I just, like, then I sent emails to my manager saying, Look, look, look, the great work I did.
Exactly.
But I hadn't done any optimization.
Yeah, yeah, yeah, yeah. That's very, very cool.
And so and reducing the memory, like meant that you could use a lot more CPUs, because that was the bottleneck initially. Like we had this cloud VM that was like mostly just sitting idle, because you just need so
much RAM for each of the threads or processes.
Right. You can get a high memory version of a cloud computer, but it still, there is that tradeoff, right? You want to take full advantage of the CPUs there. And obviously, less memory is better. And also just it might mean fewer cloud computers to manage.
Yeah. And if you think about your computer, if you look at like the usage of your computer, much of the time, your computer usage is going to be like you're using 1% of the CPU is just sitting there. And your RAM, if you're like, a lot of people say like a gigabytes of RAM, their computer, your RAM is going to be like three quarters percent full, 75% full.
And basically, it's just that proportionally RAM is much more expensive than computing. And so you don't have as much of a just look at all the CPU guy, like memory tends to be resource constrained, and then the failure modes are you run out of memory and like your computer's wedged or you lost your data.
Right, right. You run out of CPU, it goes slower.
Yeah. So the failure modes are much worse.
Interesting. Yeah, well, it's going to be really fun to dig into it. And I think it's an interesting angle of just the Python ecosystem that people don't spend that much time obsessing about the memory, but it's important. And it's interesting. And we're going to spend some time obsessing about it for the next hour. So for sure. Before we do, let's get into your story, though. How'd you get into programming a Python?
I got into programming back in the mid 90s, and my parents were this business creating multimedia CD-ROMs, which was exciting new technology in the mid 1990s. And so I ended up just doing coding for them. I got into Python a few years later, when I discovered SOAP, the Z-O-P framework, which at the time was like really huge in the Python world. Like, you go to Python conferences, there'd be like a whole track on SOAP.
And then I just stuck around and ended up using Python for lots of things like distributed computing, worked on Twisted for many years, and scientific computing, and just a variety of different things.
Yeah, very cool. And what do you do day to day?
I've been doing training, stuff with Docker and packaging for Python. I'm hoping to eventually teach some stuff about Python memory, and then have some products, do a little consulting on the side and start thing.
Yeah, very cool. Is this training in person? Is it online? What is it like?
Originally, this was in person training. I was supposed to have like, open enrollment class right after Taicon in Pittsburgh, for example. And nowadays, it's over Zoom. Because what do you do?
Yeah, because the world is crazy. It's absolutely crazy. Yeah. Okay. Well, cool. That's a lot of fun. I did that for like 10 years and really enjoyed my time doing in-person training. Luckily, there were no pandemics.
Zoom actually works.
It definitely got disrupted with other things, but not too much. Yeah, we did some stuff over, I think, GoToMeeting, GoToWebinar, which there was no Zoom. So that's what we were using. It was pretty good, actually. Yeah, it's not a bad story.
All right. So speaking of obsessing with Python memory, let's just get started off with a little bit of an overview of how Python memory works. So I feel like Python memory lives a little bit in between the C++ world, where it's very explicit, and the Java.net GC world, where it's not even deterministic. What's the story?
As a payload, this actually depends on which Python interpreter you're using. If you're using PyPy, P-Y-P-Y, it's actually basically like Java or .NET. If you're using CPython, which most people do, it's a little bit different.
And the basic idea is that every Python object has a reference counter. And so when you get a reference to an object, it gets incremented by one. You remove a reference, it gets decremented. So when you append your object to a list, that's an extra reference. If you destroy the list, that reference goes down to...
If the reference goes down to zero, the object is not being used by anyone. There's no references to it. So it can immediately be freed up and deallocated.
The problem with reference counting is that it's not... It doesn't cover all cases. If you have a circular... Set of circular references, the objects will never hit reference count to zero.
So if you take a list and then you append it to itself, it's going... Because it has a reference to itself, its reference count is never going to hit zero, even if you don't have any other references to it.
So in addition to the reference counting, Python also has a garbage collection system, which every... I think it's based on how many bytecodes have run.
It will go and look for objects that are in this little loop by themselves, but not being used in any actual code and get rid of them too.
Right. And I think the GC is also generational, like the other main ones, say Java and .NET as well.
Yeah. And I don't quite remember how this works.
So, you know, a totally, maybe more maniacal example might be if you're studying some kind of graph theory type object, like a network of things or a network of relationships among people or something like that, where it doesn't even have to be one thing pointing back at itself.
It could be thing A points at B, B points at C and D, D points back at F, but F points at A.
If you can make a circle following that chain, reference counting breaks.
Yeah. And so then you fall back to GC, the garbage collection and...
Right. But I would say for the most part that just knowing the GC is there to kind of catch that edge case is really all most people need to know, right?
Because the primary story is this reference counting story. What do you think?
Unless you're using PyPy, because then there's no reference counting. It's only garbage collection.
Yeah. But I'm thinking most people are running CPython. Maybe they're using some data science libraries, especially in the context of using your tool that we're going to talk about.
It feels like it's definitely in the data science side of things. In that world, in the CPython world, then it's probably reference counting that you care the most about.
Yeah. And I mean, just a fairly high level understanding that as long as something's referring to your object, it will exist. If the references go away, it will either immediately or eventually disappear and get deallocated.
That's pretty much all you need to know the vast majority of the time.
Yep.
And the vast majority of time, that's enough, but not always.
Not always. So we're going to talk about a project that you started called PhilFIL that is about profiling memory allocations for data pipeline type of scenarios in particular is optimized for that.
Although I suspect you could use it for a lot of different things.
I think so, yeah.
But let's start the story by just talking about some memory challenges, I guess we could call them.
So you wrote a cool blog post article called Clinging to Memory, How Python Function Calls Can Increase Your Memory Usage.
Yeah.
That's a pretty interesting one. Tell us the general idea here.
And so this is something I encountered in the real world, so it can impact you.
And this is more of an issue in the kind of applications where you're processing large amounts of data.
So like one object might be like four gigabytes of RAM.
Like if it's like, if objects live slightly longer and they're like, you know, a dictionary of three entries and there's only one dictionary, you don't really care how long it lives because it's not using.
Are you using 2.7 or 2.701 megabytes for this working memory?
Nobody cares.
Yeah.
When you have like an array that's like four gigabytes or 20 gigabytes, like this can have very significant impacts.
If an array lives even slightly longer than it needs to.
And so the idea is if you have a function and you create something in it and then you pass that object to another function that you're calling, you have function f and you're creating this.
You have this large array, you pass it to g.
If you have a local variable inside of f that the parent function still refers to that array.
Like the parameter that accepted the data, for example.
Yeah.
Then that reference within that function call is a reference.
It means reference count's not going to hit zero.
Even if g like uses that array and then throws it away and doesn't care about it anymore.
The parent function still has reference to that array.
And so you can end up with these situations where if you read the code, you know that you are never going to use this data again.
There is no way you can use it.
But from Python's perspective, because there's a local variable, then the function frame that's referring to it, that object is going to persist until that function returns or if there was an exception that exits.
Right.
Because everything that was loaded up in that function got defined.
And so here's all the variables of the function.
And reference counting, they're still pointing at things until those variables go away, right?
And they go away when the function returns.
Yeah.
And you can imagine like if you go into PDB, like you can actually travel up and down the stack and like you can go up to like the parent function and see the local variables.
They're still there.
Like you can still go in the debugger prompt, just go up two frames to the parent caller and you'll still see the local variable pointing to your large object.
And so you can restructure your code in various ways to deal with this.
And the way I ended up actually doing it was basically copying this idiom from C++ where you have this object whose only job is to own another object.
It has that.
You end up with only one reference to the large area that you care about, which is from inside the owner object.
Then you pass the owner object around.
And when you know that you don't need that data anymore, you tell the owner object, clear your contents.
And then that one reference goes away and memory is freed.
So you sort of just interesting situation where every once in a while you actually have to fall back to the manual memory management techniques that you have to use all the time in languages like C or C++.
Right.
You know what's interesting is I see examples of code like this.
And then you'll see other people lamenting the fact that code is written this way.
And they'll say, you should never write code this way.
It's not necessary in Python because it has automatic memory management.
Or you should never do this like halfway through randomly set a variable to none and then keep going.
Why would you ever do that?
That's like you don't need to do that.
Right.
If you're not going to use it again.
Oh, except when that was costing you an extra gig of memory.
All of a sudden, this kind of nonstandard pattern, it turns out to be really valuable.
Right.
It's the difference between it works or it doesn't work or it's a thousand versus two hundred dollars a cloud compute or whatever.
Right.
This portion of Talk Python to Me is brought to you by Linode.
Whether you're working on a personal project or managing your enterprise's infrastructure, Linode has the pricing, support, and scale that you need to take your project to the next level.
With 11 data centers worldwide, including their newest data center in Sydney, Australia, enterprise-grade hardware, S3-compatible storage, and the next-generation network, Linode delivers the performance that you expect at a price that you don't.
Get started on Linode today with a $20 credit and you get access to native SSD storage, a 40 gigabit network, industry-leading processors, their revamped cloud manager at cloud.linode.com, root access to your server, along with their newest API and a Python CLI.
Just visit Talk Python.com.com when creating a new Linode account and you'll automatically get $20 credit for your next project.
Oh, and one last thing.
They're hiring.
Go to linode.com slash careers to find out more.
Let them know that we sent you.
Yeah.
Having never done scientific computing before this job I was at a couple years ago, it was an interesting experience learning a different – because the domain is different, like you have different constraints and different goals, and some of the ways you write software end up being different.
Unless you're doing large-scale data processing most of the time in Python, you just don't think of any of these things.
Like, you might have to worry about memory leaks, but that's a different sort of – much of the time, that's a different set of problems, where, like, you don't think about the fact that an object being alive for five more milliseconds might cost you, like, another $100,000 if you're scaling up.
Yeah, for sure.
It's interesting.
Another solution that you proposed – well, you proposed three solutions.
One is this ownership story.
One was maybe only applicable for very limited small functions, but you could just have no local variables and just basically chain one function call into another.
Yeah.
The intermediate one, though, seems possible, possibly reasonable as well, which is to reuse the local variable.
So you're going to load up some data, and then you're going to maybe make some changes, which will copy the data.
Instead of having data one, data two, data three, you just say data equals load it, data equals modify the data, data equals modify the data again.
And that way, at least as you go through these steps, after each one, it's, you know, released the memory from the prior, potentially.
Yeah.
And one of the things about sort of data processing applications, they often have these sort of idioms where you're, like, doing a series of steps.
And this is where, like, keeping old copies of the data around tends to end up cumulatively being very expensive in terms of memory because it's a series of steps.
Once you've done step one, you don't really care about the initial input.
Once you've done step two, you don't care about that one.
So just explicitly overriding the previous step is another way to do this.
I can see somebody looking at this in a code review and going, why are you doing this?
These data mean different things.
One should be initial data.
The other should be, you know, grouped by state.
And the third should be some other thing.
Like, you're naming these wrong.
You know what I mean?
That's what I was kind of hinting at is, like, sometimes you need to break the rules to break through to, like, a better outcome.
Yeah.
And in general, pretty much every best practice is very situation-specific.
And sometimes it's the vast majority.
But...
Yeah, that's a really good point.
A lot of times when you hear advice like that, it's spoken as if it was absolute.
But there's an implicit context, right?
Like you said, when you don't really care about memory and that kind of stuff, you just said you just go and write the code.
But, you know, it probably means implicitly what I care about is readability.
And what I care about is maintainability.
And I just want to optimize it to be as clean and pure as possible, which is fine.
But if pure doesn't work and not clean totally works, like, forget the clean.
We don't care anymore.
I want it to work.
That's more important.
Like, functioning is primary here.
Yeah.
And then there's, like, places like MicroPython where you're running on little embedded devices with very little RAM.
And then some of the problems that you have in large data processing are translated down to very small programs.
That's an interesting example for sure because, again, if you didn't care about that extra meg of RAM, but all of a sudden you only have half a meg, now you really care about it.
Yeah.
I do want to throw out something from Philip Guo over at pythontutor.com.
So if you want to understand, like, a lot of these relationships and how objects refer back to each other, he's got a really cool visualization.
I think when you're over there, you have to check.
There's, like, a checkbox at the bottom.
Let me pull it up.
Under the way it renders objects, I think you have to flip it.
From inline primitives to, say, render all objects on the heap like Java and Python do.
Anyway, if you want to, like, show that off or visualize that, that's a really cool quick one.
Also, if you want to observe reference counting without changing reference counting.
Because, like, you might want to say, how do I know if there's a reference to this?
You can't store a variable and point at it and say, now we're going to ask because you've now changed it, right?
Have you done anything with weak references?
Weak ref?
I'm not sure I've ended up using them in scientific computing.
I've definitely done them and used them in some places with, like, asynchronous programming servers.
Yeah.
Yeah, you could use them for, like, caches that can kind of, like, auto expire and stuff as well.
But they're really good for, I could create a weak reference to an object.
Then you can ask, how many things point at this?
And even if you know something points at it, knowing whether that's one or two might help you get a different understanding, right?
You're like, oh, I thought there was only one pointer.
Why are there two pointers to this thing?
Where did that second one come from?
And so you can ask interesting questions without changing the reference counting with weak references.
It's really easy.
Yeah.
And there's an API, sys.get refers.
It gives you the objects that refers to an object.
But then, yeah, you inevitably add the current function frame as an additional reference, and you have to discount it.
Right, right.
You threw also get size of in here as well.
What's the story of the get size of?
The function call thing is sort of just an example of places where sort of automatic memory management gets in your way.
But there are more fundamental limits or problems you end up with when using Python in memory intensive situations, which you need to understand.
And one of them is just that Python objects use a surprising amount of memory for what information that they store.
So pretty much every, if you look at the implementation of the CPython interpreter, every object has an addition to whatever data you need to actually store the object itself.
It has, on a 64-bit machine, which is most of them these days, it has a pointer to the class or the C type for the class.
So that's another eight bytes.
And then it has the reference count.
So that's another eight bytes.
And I think if you have, if it's object sports garbage collection, it's even more.
And so if you check the system get size of as a nice utility that lets you use, tell me, tell you how many bytes object uses.
I don't think that actually traverses the object tree, right?
Like if this thing, if it's a list and the list points at things and those points at those.
Yeah.
I think it's just how much is like that, the immediate thing that that variable value points at, right?
Yeah.
Okay.
That's what I thought.
Yeah.
I can check what you're talking.
And if you check the, how much memory, like an integer uses, like the number one, it takes 28 bytes.
Yeah.
So if you think about like how you'd represent numbers in memory, like unless you have really large numbers where you obviously need more, 64 bits is sort of, will get you some really big numbers.
So if you're using a list with a million integers, that's way like I did the math and I think it was 35, a list with a million integers in it is 35 megabytes of RAM.
If you allocated that in a CRA, it would be eight megabytes of RAM.
So you're using four and a half times as much memory just because you're using Python objects.
In another example, while we're talking about it is the character A. So in C, the character A would be what, four bytes or something like that?
If you're using UTF-8, you can probably get it down to one byte.
Yeah. Yeah. You could definitely make it smaller if you do it right.
Yeah.
In Python, it's 50.
Yeah.
So also get size of, it does some interesting stuff. So if I give it a list of like a million items, it'll say the size is 800,000. It's not quite a million. Maybe it's 100,000. I think it's 100,000.
But if I give it a list, which has the number one and also contains within that list, that list of 100,000 items, the size is 72. So yeah, you got to be real careful. It's, it's, it doesn't tell you the whole story, but it does.
Yeah, exactly. But it gives you a sense of, oh, like the letter A is 50 and the number one is 28. The memory that we use per representation and data in Python is fairly expensive, I think is the takeaway, right?
Yeah. So if you have like a common, one place where people hit this is like, you're reading in some data and then like you're creating like a list per thing for like a reading in some like rows of data from the CSV or something.
And you're turning into like, here's a list and then like there's a dictionary with like a bunch of keys for each one or an object for each entry.
And you end up with like a massive amount of, considering the information you're storing, you end up with a huge amount of just overhead from creating all those different Python objects.
And so like one situation you end up in Python running out of memory is if you're doing like data processing and it's just like, you just have 10 gigabytes of data you loaded.
It's going to be a lot of memory, but sometimes it's not actually that much data if you store it on disk or if you store it in the appropriate CXC object.
And it's just a lot of data because you create a lot of Python objects.
And so it's using like five times as much memory as the actual information.
Right, right. So maybe load it into NumPy or Pandas or something like that instead of into a native Python dictionary or something.
Yeah. So if you think about Python lists, which has a bunch of Python integers in it.
And so each of those Python integers has like 28 bytes of RAM.
A NumPy array, it's basically has the same Python overhead, but only once at the beginning where it says, I'm an array.
And I store 64-bit integers.
And then the storage is just, it's not a generic pointer to generic Python.
Right, right. It's eight bytes per entry.
Yeah.
Yeah. And so the information that would take 35 megabytes in a Python list will be eight megabytes in a NumPy array.
Yeah. Another one that, I mean, moving to some of these libraries that support it more efficiently, certainly in the data science world make a lot of sense.
But also something that makes a lot of sense that I think people may be overlooking is using different algorithms or different ways of processing.
Like one really simple way is like I need to load, compute a bunch of stuff and give it back as a collection.
So I'm going to create a list, fill it up and return it. Right.
That loads, you know, maybe a million items in a memory and all the cost and overhead of that.
And then you give it over to be processed and off it goes.
Alternatively, add a yield instead of a list and just do a generator and you process them one at a time.
Because probably what you're going to do when you get that list back is go through the list one at a time. Right.
And that uses one one millionth of the memory or something to that effect. Right.
It doesn't, it only loads one in memory at a time and not all of them.
There's things like that as well that you can do.
Yeah.
If processing them one at a time in order makes sense.
If you need to seek around and say, well, what the third one compared to the first one is, then forget it.
Yeah.
Like the three basic techniques usually are batching and streaming with that generator is like a sort of batches of one.
A size of one.
Yeah.
And then there's compression where you have the same memory and same data semantically, but with less overhead.
So like switching from Python lists to NumPy arrays in some sense compression.
If you know your numbers are only going to go up to 100, you can use a 8-bit NumPy array.
And then like you've cut your memory by like 80% at no cost because you have exact same information.
And then the final technique is indexing where you need to load only some of the data.
Then you can sort of arrange your data.
So you can only, you only need to load that part.
So like if you're doing accounting, like if you have one file for every month of the year, like you can just load July's file and then you don't have to worry about the data in the other months.
Yeah.
Very cool summary.
So that's the picture.
That's the memory story.
That's some of the challenges you might hit and some of the potential solutions that you might come up against.
But at some point you might just need to know like, okay, this is about as good as it's going to get, but I still need to understand the memory better.
Or I'm running out of memory.
Why?
Where?
Or maybe you want to take the lazy approach.
Maybe you want to start from, well, I know I have this problem of using too much memory.
I know one of these things that these guys talked about will possibly solve it, but where should I focus my attention?
Right.
I've got a thousand lines of Python.
Maybe only three need to be changed.
Which are the three?
Right.
So you probably want to profile it somehow.
Answer the question, like where is the memory coming from?
What's the problem?
It's very difficult to optimize something if you can't measure it.
Like the example we gave with functions keeping local variables, keeping things alive.
Like I would never, now that I know it's a problem I've encountered, I might be able to look for it.
But at time, like it was, I believe it was something like extra 10 gigabytes of RAM or something.
And I don't think I ever would have spotted it just by reading the code.
Yeah.
Because it looks perfect.
It's clean.
It's readable.
It's optimized exactly for the scenario that you most of the time optimize it for.
So it doesn't look broken.
Yeah.
If you want to understand why something is using too much resources, like you need to measure it.
I built a profiler for a memory profiler called Phil, Phil, F-I-L, which is designed to solve this problem because I hadn't tried other tools available.
I decided they weren't sufficient.
Yeah.
So Phil, I think is really interesting.
And the thing that made it connect for me at first, I was like, well, we already have some pretty interesting ones.
I mean, you've got built-in C profile.
I think that only does CPU profiling, not memory.
We have memory underscore profiler, which will do memory profiling.
Yeah.
We have Austin.
Yeah.
We have Austin.
Are you familiar with Austin?
I've not used it.
I've used a Pi instrument and I know about Pi top, Pi spy, and they're all sampling profilers.
Right.
And Austin's pretty interesting as well.
But Phil, the way that you laid it out is really a lot of these profilers are either general purpose or they're built around the idea of working on servers.
And long living processes that do short amounts of work many, many times.
Right.
Like a web server or something like that.
And that's a pretty different scenario of I have a script.
I need to run it once in order and then look at where the memory comes from.
Right.
Yeah.
So memory profiler is the tool I ended up using when I was trying to use memory usage.
And memory profiler will do this thing where it gives you, like you write on a function that says this function added 10K in memory or 100 megabytes in memory or whatever.
And if you're trying to find a memory leak, this is actually pretty useful.
Like you can say, like I call this function, now my memory usage higher.
And so what happened?
So you can just figure out this function is where your memory is leaking.
But the thing that I was trying to do and what data processing applications, as you mentioned, are trying to do is reduce your peak memory.
The idea is that you're running this process.
It's going to load in some data.
It's going to process it.
And then it's going to write it out.
And it's going to exit.
And the peak memory usage is what determines how much hardware you need or virtual hardware.
Like it doesn't matter if like 99% of the time it's only using 100 megabytes, if 1% of the time it needs 60 gigabytes of RAM.
Like it's that peak moment in time that you need to.
That's what you have to put it in for.
Yeah.
The high watermark.
It's like you're building a dam.
Like you figure out what the highest flood you get is.
And the thing about memory profiler, like you can run it on your function and it'll say this line of code added zero megabytes of RAM.
Like it measured before, measured after.
They're the same.
So no memory was added.
But that doesn't tell you.
It's fine, right?
Right.
But it may be that it allocated 20 gigabytes of RAM, did something, and then deallocated.
And so you have to, the memory profiler, like recursively go through your whole code base function by function until you find that one line of code that's spiking things.
And so you can use it to figure out the peak memory, but it is a very manual, tedious process.
Yeah.
And once your code base is hard enough, it can become quite difficult.
And another big distinction between servers and data pipelines is how much you care about memory leaks.
As long as it's a small memory leak, like if you're doing like a process that runs for an hour and it leaked 100k, like after an hour, it'll just exit.
If you have, if you're leaking 100k an hour, but your process, you have like 10 processes and they're running for a year, 100k may still not be a problem.
But like there are some thresholds where for a server, like it accumulates and your server crashes.
And for a batch process, so long it's not impacting the peak, you don't care.
Right.
Well, imagine you leak only one kilobyte of memory, but it's in the context of a web request and you're getting 100,000 web requests an hour.
All of a sudden, your server is toast, right?
Whereas if you call the function once and you leak a kilobyte and you're doing like a top to bottom run at once data pipeline, who cares?
Doesn't matter, right?
It's lost in the void there.
So I think also just the focus of what you care about is really different.
You don't generally have these huge spikes in server type applications.
You can if you're doing like reporting or other weird stuff, but like standard data driven stuff, it's pretty flat line.
Yeah.
And it turns out that if you think about it, a memory leak, a tool that can find peak memory can also find memory leaks.
Because if you have a memory leak, peak memory is always like right now.
Yeah, exactly.
If you just run for a while and peak memory, eventually like your memory is overwhelmed by the leak.
And then you dump the memory then.
And so that moment is peak memory.
So a tool that can find peak memory can deal with leaks, but it deals with leaks can't necessarily help you with peak memory.
So it's actually a more general concept.
Talk Python to me is partially supported by our training courses.
How does your team keep their Python skills sharp?
How do you make sure new hires get started fast and learn the Pythonic way?
If the answer is a series of boring videos that don't inspire or a subscription service you pay way too much for and use way too little, listen up.
At Talk Python Training, we have enterprise tiers for all of our courses.
Get just the one course you need for your team with full reporting and monitoring.
Or ditch that unused subscription for our course bundles, which include all the courses and you pay about the same price as a subscription once.
For details, visit training. talkpython.fm/business or just email sales at talkpython.fm.
Another thing I like to do is relate quantum mechanics back to programming ideas.
And I think they're really relevant in both profiling and debugging.
And that is the idea I'm thinking of is the observer effect, that by observing some phenomenon, you might actually change it, right?
Maybe the tool you're using to measure it actually makes some difference.
Or in quantum mechanics, like just insane, bizarre observer effect.
Things happen that, again, shouldn't, but it does.
One of the challenges I see around profiling is, especially instrumenting style profilers,
is you run it because it's too slow, you won't understand the performance.
So you apply the profiler to it.
Now it's 10 times slower or 20 times slower, but not evenly, right?
Like if it's in a really tight loop, that part slows down more than if you're calling like a C function that you're not technically profiling that part,
but it's still slow.
That might not really slow down at all.
So you might exaggerate different parts of it as well.
And it sounds to me like Phil doesn't have much of this observer problem.
Yeah.
So the observer problems tend to be worse in CPU profiling, because as you said, like the act of profiling can change how fast the process runs or which parts of the code run faster.
So C profile suffers from this, because it's adding overhead per Python function.
And so code that has a lot of Python functions will be slower than code that has less Python functions, even if the actual runtime is the same.
So the ads are already unevenly.
And the solution on the hand CPU profile is actually sampling.
We only like every thousand times a second, you see what's running right now.
And tools, I believe Austin works that way and PySpy and PyInstrument.
All right.
It's more like a helicopter parent.
Like, what are you doing?
What are you doing?
Instead of actually walking along every step, just constantly asking.
Yeah.
And so then it gets a chance to run faster or whatever when it's not asking.
Yeah.
The impact is quite minimal.
And because slower CPU functions will show up more when you're just peaking every once in a while, like statistically it'll converge.
You'll get an overview of where performance is being spent that isn't exactly right, but it's close enough that it doesn't matter that it's not exact.
So that's CPU.
In memory, sampling might work well for something like a memory leak.
Because with a memory leak, like eventually all your memory usage is this one function being called over and over.
So if you only check some of the time, it's like eventually you'll catch it.
But if you care about the peak, you have to maybe not have to capture all the allocations, but like you may have like one specific allocation that's like 20 gigabytes.
That's what causes your peak.
And if your sampling doesn't catch it, then the sampling, the profiling is useless.
And so effectively, one way or another, you have to track every memory allocation if you actually want to find peak memory.
So the implementation approach, whereas sampling is like the superior approach for CPU, if you will care about a high watermark or peak memory, instrumentation is often the only way to go.
If you have uneven allocation patterns, which is the case in data processing applications.
Right.
Yeah.
And it sounds like maybe a 50% speed hit is what the docs say.
That doesn't sound too bad.
Yeah.
I mean, it's probably slower in some cases and faster than others.
This is what you like if you run PyStone.
Yeah.
It's not like a thousand percent or something like that.
Right.
Yeah.
And I spent basically, once your profile is slow enough, people just don't use it because they don't have the patience.
Yeah.
So a lot of the effort I put, like, the basic idea of what it does is not that sophisticated.
It's basically, like, you intercept all memory allocations and keep track.
And then whenever you hit a new peak, you store a copy of that so that you know that's the peak.
It's just if you want to do that with low overhead, that takes work.
Right.
Absolutely.
So one of the challenges is the reason you're using the profiler probably is because you have a lot of data and you built it in some small scenario and then you run it in the real scenario, then it actually is not doing as well as you'd hoped.
That's exactly when you need to be able to run it with the profiler.
And you need it to work fast, I guess is what I'm saying, to really use it in real scenarios.
Yeah.
And another thing I've done to handle that, which, and this is a new project, so this is all, like, work in progress.
But I know at least, like, I've gotten at least one success story of someone saying they found a, like, in within minutes, they found a memory issue they wouldn't have found otherwise.
So I know it's useful for some people and other people have bugs.
But another feature that I've added is when the worst case scenario for running out of memory is your program just crashes.
And this can be as bad as, like, your computer just wedges altogether, which is not uncommon.
Like, just everything's become so utterly slow that, like, yes, if you left it alone for a day, it'd come back, but you put files to restart it.
Or you get, like, or it just crashes.
And you can do a core dump, but, like, the core dump doesn't tell you.
In theory, it has information you want.
Yeah, in practice, that's a whole other level right there.
Yeah.
Or it actually does not have the information you want, to come to think of it.
Another thing, a feature I've added is Phil makes some attempts to handle out-of-memory crashes.
Like, if you run out of memory, it'll say, like, okay, you just got a failed allocation, so I'm going to try to deallocate all the large allocations that I know about just to free up some memory.
And it has, like, this emergency stash of, like, 16 megabytes that, like, just allocates up front and select it.
It breaks the glass.
It allocates that memory, so it's a bit more.
It lets it go and then starts tearing stuff down as hard as it can.
Yeah.
And then it tries to dump a report of, like, this is what your memory usage was.
And it won't always work, and I suspect it needs a bunch more work.
Like, it needs a bunch of optimization, because I feel it dumping the report from field text memory.
But the idea, like, my goal, at least, is that when you run out of memory, instead of just a crash, you'll actually get some feedback that will help you diagnose the problem.
Yeah, that's really, really cool.
I don't know how CPython, a Cprofile, excuse me, I don't know exactly how deep its reach is.
But in Cprofile, if I'm trying to look at, say, data science stuff, and I'm calling a library, and it's using its internal malloc and its internal C stuff to manage the memory down in the C layer, I don't know if Cprofile will check that.
You know, if it's doing, like, crazy Fortran stuff or other allocations, who knows?
So CPprofile, I mean, it's giving you CPU, but it's...
Yeah, I'm sorry, Memory Profiler, the one that does memory one.
Yeah, yeah.
So Python actually has a memory profiler thing, TraceMalloc, but it only knows about Python memory APIs.
Like, if you're using an arbitrary C++ library, you won't know about it.
Which is common in the data science world, right?
I mean, that's exactly where a lot of the action is.
Yeah.
Yeah, Memory Profiler has a bunch of different ways it can work, but it can actually...
The most general way it works is, like, at the beginning of the line of code, the end of the line of code, it checks just how much memory that process is using.
And so it'll work with any allocation, but it has the other downsides that we talked about earlier.
So Memory Profiler can actually...
The reason I was using it was because it can actually catch any allocation from any C library.
I see.
Painfully, for purposes of reducing memory usage.
Yeah, for sure.
And so my goal with Phil was to not just be tied to Python code allocations and be able to just generically support anything that any third-party library is using.
Yeah.
I think that's a lot of things that I've done.
I think that I've done.
because the operating system will cleverly load and unload stuff from disk on demand.
And so it is affecting how much memory you use, but the OS will sort of optimize it for you.
So it's not clear how to measure it.
So there's a lot of ways that if you want to track everything, like there's a lot of them,
and I don't do all of them quite yet.
But I've been sort of adding them one by one and hope to cover the vast majority of cases pretty soon.
Yeah, but you covered some of these at least already, huh?
Yeah.
I cover basic MF usage, malloc, calloc, realloc, which I said, the standard APIs, added aligned alloc, which is C++.
Apparently, at least in some cases, Fortran, I've never done anything with Fortran.
I just know that it's a thing that scientific computing uses.
And so like I said, okay, I'm going to figure out if Fortran is covered by this.
And it turns out that traditionally Fortran never actually had memory allocation.
You would just like write some code and you would say, I'm going to have this array and that's all you ever got.
But modern Fortran from 1990 onwards has explicit allocation.
And Phil can at least capture that if you use at least GCC's Fortran compiler.
And so the idea is you should be able to just take arbitrary data processing or scientific computing code and it will figure out those allocations.
It won't tell you like which line of Fortran and which line of C was responsible because that's like there are tools that do that.
But the performance overhead is immense.
But it will tell you at least which line of Python was responsible and much of the time that's sufficient.
Right. And as a Python developer, really, that's kind of the answer you want.
You don't want to know that like this internal part of NumPy did it.
You just want to know I called, you know, load CSV on Pandas or something.
And that's where the memory is or something.
Yeah, exactly.
You want to see the kind of boundary into that library because that's where you control.
You're not going to go rewrite Pandas or NumPy.
Yeah.
And yeah, much of it.
So yeah, you will like the goal field is to tell you where in your Python code the memory usage was and not only tell you that in a very easy to understand way, which was another one of my goals.
Yeah. So you want to tell people maybe describe the flame graphs that they can see and explore.
Yeah.
And maybe we can link to one of the show notes.
So flame graph, I think Brendan Gregg came up with the idea.
And the idea is it's sort of showing you, you know, your programs that you can think of as a, any point you have like a call stack, like you have function F calls function Z calls function H.
That's sort of a stack.
And so you can put these bars that where the wider they are, the more resource they're using.
Brendan Gregg originally did this for CPU.
I'm using it for memory.
And the idea is so if you have a really wide, like if you have a bar that's like 100% of the screen, that's like it's things using all this or the functions of call.
They're using all your memory.
If it's like narrower, it's using less memory.
And then I've arranged it in a way that it actually includes the source code.
So what you're reading looks like a stack trace.
It looks like like something through an exception and you're just reading it.
But the width of the bar shows you which lines of code were responsible for how much memory cumulatively.
I also added some stuff where there's a building on a Rust library called Inferno, which is great, which didn't much of the heavy lifting.
But I added a feature to Inferno where the wider the bar, the more memory it's using, the redder it is.
And so the idea is you just look at the graph and you can just see like where it's red is where.
Where is it red?
That's the problem.
That's the thing you got to focus on, right?
Yeah.
It really focuses on the expensive parts of the code.
And then what you're reading is a stetress.
Yeah.
And these are cool.
You can embed these into the web pages and then you can hover over them and click and like zoom into the functions and really explore it quick and easy, right?
Yeah.
I originally rolled this sort of Perl script that converted data into these SVGs and then Inferno library ported that to Rust and so I'm using it.
So they didn't much of the work and I'm just building on top of it mostly.
So they did a few small features.
Yeah.
It's like this whole UI for exploring there.
To use it is super simple.
Like if you were going to run Python space your app.py with its arguments, you just would replace Python with fill dash profile space run and that's it, right?
And you get this output.
Yeah.
My goal was also no options.
Like this isn't a people don't run memory profiling like every day.
Like it's not like a tool you want to tweak and customize your own personal needs or that you want to spend a lot of time learning.
So another of my goals is just it should just work.
So I've at the moment it has one command line option, like where it dumped the data, you know, you need to set that or think about it.
And then the output is like a HTML page that has the graphs embedded and has some explanations.
And so the goal is as much as possible to make it as sort of transparent and easy to use.
And I have some further ideas of how to improve the UX, which I haven't gotten to yet.
Nice.
So if I'm like a data scientist or a computing person who is not necessarily a programmer, I could just drop in here, pip install, fill, fill dash profile, run my thing that normally I would just say Python run.
And that's, that's all I really got to know.
And then I just look at a web page.
Yeah.
It'll open the web page automatically if you're, it can.
So you don't even have to.
Yeah.
If you're, the goal is you run it and it, yeah.
Yeah.
If you're, the goal is you run it, it pops up a web page, read the web page and you have the answer.
Yeah.
What's using where memory is going.
You spoke about one of the cool features being the out of memory catch and analysis, and you've got to do a slightly different thing on the command line to make that work.
Right?
Yeah.
The issue is, and this is a thing I can probably fix eventually.
It's just, this is sort of a limit in my implementation.
The code that generates the report right now is in Python.
And if you just run out of memory, you can't go back into Python at that point.
Yeah.
So if you run out of memory, like it's not, the experience isn't quite as nice.
Eventually I might end up like, if it reaches the point where I'm not like iterating it as quickly, I might rewrite that in Rust.
And then at that point, it might be feasible to actually like have the fully nice UI and the crash.
Yeah.
Right.
Okay, cool.
Now also currently it runs on POSIX, Linux and macOS only, right?
Yeah.
I would expect that.
I'm not sure it would run on anything other than like, if you run this in FreeBSD, my guess is it will work.
Yeah.
But I don't think-
Linux and macOS, yeah.
Yeah.
I don't think data scientists or scientists are using much FreeBSD.
Right.
Yeah.
And macOS was added fairly recently.
And someday I would like to add Windows, but it's, there's a lot of like dealing with like linkers and like fairly low level details that I don't know as much about on Windows.
So it should be possible.
I've seen things that make that make me think that it is possible.
I just, it's a chunk of work I haven't done too, because they're hard.
Sure.
Yeah.
You've either got to get it working or-
Yeah.
You're just supporting macOS, because that's a lot of work, so.
Yeah.
I'm sure it was.
So I actually think that maybe you don't have to worry too much about Windows.
And that's not to say that people don't use Windows.
Windows is used by like half the Python developers, and it's probably pretty heavy in the data science world as well.
But, you know, Windows 10 now has Windows subsystem for Linux, and V2 is quite nice.
So it's very possible you can just point people at, you know, you have to use Windows subsystem for Linux.
It would probably work, because it's all, it's all APIs that I would expect are emulated fairly faithfully.
Yeah.
I think it's just a BlinkView virtual machine, so I don't think you have to do anything.
My impression is that it, well, at least the original one was rather more sophisticated.
Like, there was something about like, translating syscalls.
I don't know about version two.
But yeah, there's a decent chance it'll work just fine on WSL.
Yeah, I'll put a link to Chris Moffitt's article on creating a, using Windows SL to build a Python development environment on Windows.
And maybe that'll help people in general.
Maybe this will work.
I don't know.
We can give it a try.
Cool.
And then you also, you know, it's one thing to just say, well, too bad that didn't work.
It's a lot better to say.
And here are some ideas for making it better.
So you have a couple of recommendations for data scientists on how to be more efficient with their code and their memory.
So I talked earlier about batching, indexing, and compression.
And I actually gave a, supposed to give a talk at PyCon about that this year.
It was, I mean, there's a recorded recording of it, but I never gave it live.
And there's a series of articles here that sort of talk about those ideas and then show how to apply them in NumPy, show how to apply them in Pandas.
And I started writing some articles about like how to just Python level issues, like how do you, like we talked about with like function calls and just ways to structure a code to reduce memory usage.
So there's a bunch of articles that are already adding more over time, just with sort of the techniques you need to, once you figure out where the problem is to reduce the memory usage.
Right, right.
Yeah.
I just saw your video.
I didn't realize, I didn't watch it yet.
So I'll put a link to it in the show notes so people can watch your virtual PyCon talk.
Yeah.
I've been going to PyCon for a very long time.
And so it's just really sad not being able to see like friends that leave me once a year.
And I know, PyCon is like my geek holiday, you know, just get out of there and hang out with a lot of my friends that I only see otherwise interact with online.
And it's really special.
It's too bad it didn't happen this year.
Yeah.
Someday.
Yeah.
Someday it'll be back someday, like everything.
All right.
Well, these are really interesting ideas.
I think covering them in general was good.
And Phil is a cool project.
So I think it'll help some people out there who are having challenges.
Maybe their code is using too much memory and swaps out and becomes insanely slow, or they just couldn't process the data they wanted because it didn't work.
So they can hit it with this, use some of your recommendations and maybe unlock some answers.
Yeah.
I should add, this is a very new project.
And so like, I know one person for whom it worked right, but I also know one person for whom it just wildly misreported the memory usage.
Okay.
He's hoping to send me a reproducer later this week.
We can fix it.
So if it doesn't work, I very much encourage you to file a bug report.
Let me know.
I'm happy to do a screen sharing session.
So some people will bug it just because I want this to be a tool that works for people.
And so if it's not working, I want to help.
And it's an early enough stage that I expect that there are still a bunch of major issues, even if it does actually work in some cases.
So please try it.
It might just work.
And if it doesn't, please let me know.
I'll do my best to help.
Yeah.
Very cool.
And speaking of which, you know, people are asking me recently, hey, I'm looking for an open source project to contribute to.
Do you have any recommendations on ones I might look at or consider contributing to?
What's the story there?
Are you looking for people who might participate?
I would be happy to accept contributions.
It's some parts of it are, there's a lot of fun stuff in there.
Like in terms of low level systems programming, there's like, there's a bunch of rust and like a bunch of C code and like poking into the internals of CPython.
If that is a thing that interests you, there's a bunch of work there.
There's also a bunch of UI things that could be done.
Like, if you think about profiling, the real usage pattern should really be profile this program, try to fix it, and then say, profile this again and show me the difference.
Like, and then you can have a visualization of the differences.
That is my eventual goal is like, to have a user experience that's not just what you use now, but actually shows you if things are better or worse and where.
So if people are interested in sort of that sort of UX kind of work, there's a room there.
What about building like tutorials and stuff like that?
Yeah.
I mean, like in general, it'd be exciting to see people pick it on, but it's also the same time.
Yeah.
Some low level stuff, right?
You will hit these places where it's like, I'm poking into the, like I'm causing slight memory leaks internally in CPython for optimization purposes.
Yeah.
Things like that.
Sure.
Because you want to be able to refer to pointers being like, there's a bunch of work in order to not have a lot of overhead when you report a new allocation.
And so you want to be able to like keep like a pointer address in the Python interpreter as a persistent key, which means you have to make sure things don't think garbage.
Yeah.
Makes sense.
Yeah.
I can imagine low level.
This is a beast.
Yeah.
The debugging can be tricky, but it's a lot of fun.
And it's a very, I find it sort of a therapeutic project because like, it's like, it's tricky and difficult, but it's also like a very, it's like a closed universe.
You know, you're doing web development or distributed systems.
It's like, you're talking to remote services and like, you have to spin up five processes and like, you're dependent on the whole external world to make anything work these days.
Otherwise, this is sort of like, it's a program that runs in your computer, read some data, write some data.
Like there's no, there's no outside world.
Yeah, that's cool.
So it's just like, you can stay focused on the problem on hand and not the fact that like GitHub is down or whatever.
Yeah, I've been there.
All right.
Before I let you out here though, let me ask you the final two questions.
If you're going to write some Python code, what editor do you use?
I use space max, which is a configuration of Emacs that makes Emacs a lot more like a modern ID.
Nice.
Okay.
It makes Emacs like experience jump 20 years forward.
That's awesome.
Just by installing and configuring the right packages.
Cool.
And a notable PI PI package.
Pip install.
Is it Phil or Phil dash profile?
I got a pip install.
It's Phil profiler, no dash or hyphen.
So like F I L E R F I L E R.
That's an obvious one.
What's another one that maybe you've come across recently and you're like, oh, this is really cool.
People should know about.
Nothing is, I guess, to mention Austin.
I don't know quite as much about it, but Pyspy is another, another sampling profiler.
And it's another kind of a system programming package where like it's doing these interesting things in Rust where it's like, it looks at like the memory layout.
It doesn't, it looks at the memory layout of your Python program, like parses out the data structures and reads things out.
So it's another sort of very intense system programming, which ideally is all hidden behind the scenes.
It just gives you really useful results.
Cool.
All right.
That's a good one.
Yeah.
I have to check it out.
I haven't tried that one.
All right.
Final call to action.
People want to get started with Phil.
What do they do?
Go to pythonspeed.com/products/philprofiler.
Maybe get a URL wrong.
I should probably get a shorter URL.
Excuse me.
Google F-I-L space profiler.
That should do it.
Or you can put a link in the show notes.
Yeah.
I'll definitely have a link in the show notes.
No doubt.
Yeah.
Google in Phil space profiler works for me.
Works for me.
Or you can go to pythonspeed.com and then I'll do some links to that and other stuff I've
written.
All right.
Very cool.
Also include the link to your virtual PyCon talk as well so people can check that out.
Cool.
All right.
Thanks for having me.
This has been another episode of Talk Python to Me.
Our guest on this episode was Itamar Turing-Trowing.
And it's been brought to you by Linode and us over at Talk Python Training.
Start your next Python project on Linode's state of the art cloud service.
Just visit talkpython.fm/linode.
You'll automatically get a $20 credit when you create a new account.
Want to level up your Python?
If you're just getting started, try my Python jumpstart by building 10 apps course.
Or if you're looking for something more advanced, check out our new async course that digs into
all the different types of async programming you can do in Python.
And of course, if you're interested in more than one of these, be sure to check out our
everything bundle.
It's like a subscription that never expires.
Be sure to subscribe to the show.
Open your favorite podcatcher and search for Python.
We should be right at the top.
You can also find the iTunes feed at /itunes, the Google Play feed at /play, and the direct
RSS feed at /rss on talkpython.fm.
This is your host, Michael Kennedy.
Thanks so much for listening.
I really appreciate it.
Now get out there and write some Python code.
I'll see you next time.
Bye.
you
Thank you.