 Remember memory, now it's easy to forget about memory kind of in our day to day Ruby lives and problems like bloating, leaking and faulting, problems that we don't have to deal with anymore because Ruby automatically collects all of the memory that it allocates, but it doesn't automatically collect all the objects that you refuse to let it automatically collect for you. So today I'm gonna talk about how easy it is to forget about memory. What I'm not gonna talk about is the story of Ruby's garbage collector, but let me give a brief overview. So this is kind of how the garbage collector has gone over time from mark and sweep over to lazy sweep to bitmap marking and finally generational garbage collection. And if you look at the dates on these, we actually end up with the story that means pretty much that we should have a new garbage collector already by now. So today I am going to talk about how slow this thing is, the state of memory profiling in Ruby, some helpful tools to deal with memory profiling in Ruby, and some tips and techniques that you can use in your everyday Ruby code to decrease the amount of objects that you allocate or to let them be freed quicker. First a quick introduction on me. My name is John Carpezi. I'm Cijon Run, pretty much everywhere that matters. I've done a few large-ish Ruby projects. Ice Cube is one of them. Easy Translate's another. I'm also responsible for hastebin.com, which is currently banned in India. No idea why. But I got banned somewhere. It feels pretty good. So I also work at rapgenus.com. Who's familiar with rapgenus.com? Just raise your hand. Yeah, thank you, thank you. So rapgenus.com, now renamed to genus.com. You go check it out for those of you that don't know it's a site for annotating lyrics. You can click on different pieces of lyrics and you can see what the community has said that they mean, and you can contribute your own explanations. And you can also test out our beta product, which is dealing with off-site annotations. So if you go to genus.com slash any URL, you'll actually get an annotatable copy of that page that you can drag, select, text, and write your own annotations that you can then share with other people. So first, let's start in on the state of memory profiling in Ruby. Who's familiar with Object Space? So Object Space is a, can I go back? Ah, Object Space is a Ruby API, a built-in Ruby API built into MRI that essentially gives you introspection into all of the different objects and the counts of objects that currently exist inside the Ruby VM that are currently live. So it gives you APIs like Object Space count objects. What this will do is it'll return a hash and that hash will contain a count of all the different types of objects that currently exist. So if we have Object Space count objects and then we do nothing in between, we would expect that the post count minus the precount would equal zero, but in this case we actually end up with one. And this is an illustration of a thing called Probe Effect. So what Probe Effect is is that in the actual construction of the monitoring that you wanted to do, the way that you wanted to do your experiment to find out what the difference is between the different memory, you end up creating objects and by ending up creating those objects, you end up affecting the ultimate result. In this case, what happens is that the post, when it goes to count the objects, the first thing that it does is it creates a hash, which is why t-hash ends up with a difference of one. These APIs though, nicely, actually allocate for you to pass in your own hashes. And if you pass in your own hashes, you can eliminate this thing called Probe Effect by essentially creating the hashes beforehand and now you get the result that you want. It also allows you to do things like Object Space each object. So this will actually go through every single object that's currently live in the VM and yield to this block for every single object. So here we see the first 11 objects that come out. These are the first classes that Ruby initializes. These are the error classes. And the string equivalence for access signal. It also allows you to do each object by passing in a class and going through each instance of that object that currently is live in a VM. So if we were to do this for a big num, we get these two numbers. Can anyone guess what these two numbers are? Any guesses? Can't quite see up there. So max in is a good guess. The second one here, no. The second one here is two to the 64th minus one. The first one here is actually random S-rent. So these are the two big nums that exist. You can do this with any class at any time and you can go through all of the different objects that currently exist in the VM. There's also a lesser known extension to Object Space called objectspace.so. What this will do is allow you write code to do things like this, which is crazy if you think about it. So actually for any given object, you can tell what the source line of code, the source file, the source method that that object was actually created or allocated from. You can also count objects the same way you would before, but this one will actually return you a hash of the byte sizes. You can get the memory size of an individual object. You can get the memory size of all of the objects of a particular class to see how they've grown over time. You can also do this, which is reachable objects from, which will tell you all of the different objects that could potentially hit this object at any given time, the reachable objects from this object. So to look at an example of that real quick, we define a class called name printer, which takes in a name. And then we make a new instance of that name printer. We pass it John. And then we go through the reachable objects from that name printer. And we see that what we get back is name printer, the class that that thing contains, and the string John, which is what we'd expect. So if you look at, for that name printer, you look at this array. If this array is empty, that means that this thing is ready to be garbage collected. So if I go through this again, and I change the initializer just a little bit to instantly overwrite the name attribute, we actually end up with a different reachable objects from set that currently is not John, which means that this object no longer holds a reference to John. There's also GC. Who's used GC before? This is a power corner over here. I'm noticing a lot of questions from this area. Pick it up up there. So you can do things like GC.count, which will give you the total number of garbage collection cycles, or you can disable garbage collection, or you can turn it back on. You can also do things like get the latest information about the last GC cycle, or you can force GC to start it at any given time in your application. You can also do GC.stat, which used to contain a little bit of information, but as of Ruby 2.1 contains a lot of information. It contains information about the number of heat pages, the number of total allocated objects, the number of allocated objects that didn't get cleaned up in the last garbage collection cycle. And this data is more meaningful because of the generational garbage collection that now exists. So now let's talk about some helpful tools. And unfortunately, this section is a little bit short. And at the end, I'm going to ask for a little bit of help in this section. So one thing that exists is, wow, exists, is a thing called GC Profiler. This is a built-in garbage collection API that will allow you to create and enable a profiler, do some work, then start a garbage collection cycle, and it will tell you when the garbage collection ran, how long it took to run, how many objects it cleaned up, what the total size of the objects that it cleaned up were. You also have Memory Profiler. Memory Profiler is a gem by Sam Saffron. It has a very simple API, so you take a memory profiler report block. You can also do this by starting and stopping memory profiler. You get out a report object and you can pretty print it. So it'll tell you things like the allocated objects by location, the allocated objects by file, or by method name. It'll even try to pinpoint the method names down to what gem they were imported from. So you can go figure out what gems are actually creating the most used objects. And it actually gives you, at the end, a little bit of an allocated string report for strings that ended up being allocated a whole bunch of times that it never released. And then we want to talk about these two, Mem Prof and Ruby Prof. So these two are a little bit less useful these days. So Mem Prof is only useful if you're developing in Ruby 1.8 at this point, unfortunately. And Ruby Prof, although it does have a memory profiler, it requires you to include separate patch sets to get the memory profiling hooks into Ruby that a Ruby Prof will hook into. And the point here is that we need more tools like memory profiler, and the special thing about memory profiler is that it's built on top of object space. The tools that didn't exist when Ruby Prof was created now exist to monitor and profile the memory inside of MRI, and we need more tools built on top of these libraries. So if you're looking for a new project that you want to get started with, definitely take on something to do with memory profiling. It's super interesting, and there's a lot of kind of things you can build on top of now. And now let's talk about some tips and techniques. First, I'm going to talk about lazy numerables. Who's familiar with lazy numerables? Kind of good. This is a great one. I love this one. I love just introducing this to people. For those of you that don't know, this is just a great thing to know in general. So this is a typical operation, and if you ignore the .lazy piece, looks like code that you would normally write, it ends up just chaining a bunch of array operations or new mobile operations, and then eventually limiting the result at the end. So the non-lazy form of this starts with an array like one, two, three, four, five, ends up multiplying them all by three to obtain the next result, then ends up selecting only the ones that are odd, getting three, nine, and 15, and then taking only the first two. What you'll notice here is that four and five didn't make it into the result set, and because we were only wanting the first two results, we wouldn't actually have needed to compute four and five through the entire chain anyway, and that's where lazy comes in. So the lazy form of this operation only does three things. First, it takes one and runs it the whole way through, adding it to the eventual result set. Then it takes two, doesn't make it, then it takes three, puts it in the result set, and then it stops. So four and five never have to happen. So let's do some interesting look into what happens with innumerable lazy. Okay, so a thousand times, we're gonna map something, taking it to the second power, then we're gonna turn them to strings, then we're gonna take the length of those strings, then we're gonna turn it into an array. The reason we have to turn it into an array here is because innumerable lazy doesn't do something until you have a terminating piece, so without the two a, innumerable lazy will actually just do nothing, which would be an unfair comparison. So if we look at this, takes 27,000 bytes, well, 28,000 bytes, three arrays, and a thousand strings. This isn't too surprising. Three arrays come from the three operations that have to happen. A thousand strings comes from the fact that I'm doing this 1,000 times, and then 27,000 bytes, like, I don't know, can put it in your head, like something, something, something, 27,000 bytes. Now let's look at the lazy form of that. So the lazy form actually takes 15,000 bytes, but it creates 3,004 arrays. It's pretty wild, right? It still creates a thousand strings. So that is a 45% decrease in bytes. It's good. However, it's 100,000% increase in arrays. That's not very good. So the reason that this actually happens, just for those of you that are interested, the reason that this happens, the reason that there's so many arrays, is that the way that lazy actually works is that every time that it runs one of the original elements through this entire set, it creates a new array of just that one element, and runs it through the set. So you end up taking 1,000 separate times, one element, and running them each through three operations, three times 1,000 is 3,000, and that's how you end up with a 3,004. Now let's look at this one. This is 1,000 times, this is adding a new addition. This is adding a select toward the end of the operation. So presumably this select will change something. So if we look at it now, 35,000 bytes, four arrays, 1,000 strings, the lazy form, similar story, 3,762 arrays. But still less bytes. So that's a 65% decrease, but it's a 90,000, I believe? 93,000% increase. This is on a 10 element set. This is where the story starts to get really interesting. So on a 10 element set instead of a 1,000 element set, with the exact same set of operations happening, it's an increase in memory, and still an increase in arrays. So in all ways, this smaller set actually for lazy performs worse. So that makes you think that there must be a point where the memory that is consumed by the non-lazy version and the lazy version cross over each other, and this right here for that operation is a chart of when they cross over each other. So every operation ends up with a chart that looks like this one. All the charts will look different, but they'll all be very similar in the regard that the lower the amount of iterations, you will do better with the non-lazy form, and over time the lazy form will overtake it. This is in terms of memory consumption. So then that raises the question for everyone. What about the allocations and how we're allocating more objects over time? Surely eventually the allocations will hurt us more than we'll save in memory, or the other way around. So this is actually a graph of that. So as we move to the right of this graph, we end up with a smaller number of larger objects. So we can see that only in the middle here does it actually start to pay off to you. So if you have a large amount of very small objects, it's worse, and if you have a small amount of very large objects, it's worse. Really the story here is that kind of every memory situation is different, and the takeaway, the big takeaway is this, is that if you wanna use lazy or an operation like lazy, the ultimate benefit you'll get from it is not from just adding it to your code and using it as a magic bullet, but looking at operations like this, which lazy will really, really shine in. So this is when you're taking some set, doing something to it, selecting some set out of it, and then at the end, actually limiting the result set. So the reason that lazy shines in this case is that it ends up performing less operations, which will get you a win in every category. Now I wanna talk about memoization. So memoization's a pretty classic Ruby thing to do. It looks like this. So these are three methods. We have a big thing that memoizes its big thingness so that we don't end up calling it twice when we ask for the small thing and the medium thing. I'm sure everyone in this room has written code pretty much exactly like this. The problem with this code is that if this object ends up living for a long period of time, the first time that you call small thing or medium thing, big thing has to stay in memory forever until this object can be garbage collected. So if you end up with a long-lived object that has to memoize its contents, it's better to write something a little bit more complicated, but also a little bit nicer, and in this case, large objects will actually let itself be freed, because what we're doing instead here is that the first time that this object's created, we're loading up big thing or small thing and big thing, it's not as nice to look at with your kind of Ruby mind looking at things, but it is nicer in terms of how much memory has to sit inside the VM even though you're never using it. And now I wanna talk about string freezing. So 100 times we take the string hello, then we take a space, then we take the string world. So how many strings get created here? Okay, we got 400, anyone else? So the answer here, 500 strings. So 100 times we do this operation, so I'll leave it to you to do the 100 times multiplication. The five is one, two, three for each of these strings, then hello plus space makes hello space, then hello space world is the last one. So that's five total strings every time that this happens. And you would think that 100 times would mean it only had to happen five times, but actually that's not the case, it ends up happening 500 times. So you create 500 separate strings. How about this one? So this one introduces freezes. What freezes do, they essentially make it so that when a particular piece of this code happens that the frozen strings don't have to get recreated. So how many does this create? 200. And three. 200 and three is right. Yeah, so the three actually moves into a different space inside Rubyland, so it's not tracked here unfortunately, but 203 is the right answer. But everyone understands what the 200, it's because we take hello and space and then we take hello space world. How about this one? This one's super interesting. So this is an interpolation. But it's an interpolation of two strings inside of another string. So how many, how would you imagine this would create? 300, right? 100. So this is actually a very, very interesting fact. And one that I think a lot of people don't know which is why it's kind of in this talk. The reason that this is 100 is that Ruby actually can take this interpolation and it knows that those strings can't change, so it actually takes them and kind of deinterpolates them for you. What that means though is that there's this difference that exists. So if you have a string, a variable called world, that has the value world, and you interpolate it, you'll create one string, but if you actually try to add them together, you're gonna create two strings. It's pretty amazing. It also leads into this fact with string freezing, which is that this is a really useful thing. You see this a lot of libraries that end up doing things like maybe pulling headers out of hashes or starting to have large patches put against them where they freeze all the strings. This is definitely useful. This will actually save you an allocation. And in the future version of Ruby, there will be a magic comment that you can put at the top of the file that will make kind of all strings that don't have any kind of interpolated components automatically frozen. So you won't have to do this soon, but this is definitely a useful thing to do. This is actually not a useful thing to do though. But it's Ruby already optimizes this. So if you have a key, which is a string, and a value one, and then you have a key.freeze, these are actually the same. There's no extra string created. Now I'm gonna talk about lastly method definitions. So inside this class body, when it's created, we create a string. This string is 3137, or 3137. A is long. I don't know if everyone, I'm sure you do. Okay. So you create this whole thing, and at the end of kind of this running, we end up garbage collecting this string string. Because at the end of this class being created, there's nothing left that can access this string string. It's it's fallen out of scope. So if we do the same thing with class eval, actually string falls out of scope because there's nothing that can access it. We do the same thing with defined method though. Actually the C string can never fall out of scope. The reason for that is that defined method at this point holds onto closure scope to the things inside of the class creation. So string C times 3137 will never ever be garbage collected. So definitely be careful if you're using defined method like this for this kind of problem. Because this also means that kind of anything that you're creating when you're setting up your initializers and rails, or inside of your classes will never go away if you use defined method like this. Or any block. Or any block. Any block inside of class definition that works like this will take closure scope on the class. The same is true for eval here. We'll take closure scope. So it's easy, it's really easy to forget about memory. It's really easy to forget about how many objects you create or how much space those objects take up or whether or not you're letting them be freed by the garbage collector because the garbage collector does so much work for you. So I hope this talk kind of reminded people of how they can be more logical and thoughtful about the objects they create and how they're released. And thank you.