 Hi, as you can probably rapidly tell from my accent, I'm not actually from the UK. What he means is I was in the UK before I came to Fosden. I went to the Manchester United game on Wednesday to watch our awful terrible draw against Hull, but it was still fun. I still enjoyed it. Okay, so but let's talk about Ruby garbage collection. So I have a performance consultancy called Speed Shop where I work with companies on their Ruby and Rails applications to make them faster. To make them more performant. And one of I would say the number one issues that I tend to see is companies that don't understand memory usage in their Ruby applications. And one of the big reasons for that is because we have a garbage collected language. You're not supposed to think about memory. You're supposed to just write the code you want to write. And that is, you know, the garbage is collected afterward and everything's fine. And you can't intentionally write leaks, right? But it's a little more complicated than that. And so I thought we could all benefit if we understood a little bit more about how garbage collection actually works and how it's evolved and got a lot more complicated since those long ago days of Ruby 1.8. This talk is beginner friendly. That's why it's a people's history. It's by the people for the people. If you're a Ruby core team member, you probably will not find anything interesting or novel in this talk. And I've tried to structure all of this so that if you don't know anything about garbage collection or if you don't know anything about garbage collection algorithms, this should not be too challenging for you to understand. Okay, so I would say that the whole history of Ruby's garbage collector has mostly been about how do we reduce GC pause times? How do we reduce the amount of time that we spend stopped to garbage collect? Because in Ruby, we have to, in MRI, see Ruby, which is what I'm talking about here today. I'm not talking about JRuby or Truffle Ruby. Those guys have their own garbage collectors that work in completely different, much more complicated ways. In CRuby, we're mostly at this evolution over time. We're trying to reduce the amount of time we have to spend stopped to garbage collect. We have to stop during garbage collection because if we allow things to move along and execute Ruby during garbage collection, we're going to lose track of which objects we've already looked at to see if they're free or not, which ones we haven't, and we're going to end up with memory bugs. We have to stop for periods of time and say, stop execution, hold on, which of these objects are being used and which of them are not. But if we pause for a very long time, that would cause pretty bad behavior, especially in a web application. So if we need to spend a half a second garbage collecting, which was not uncommon in Ruby 1.8 days, then what happens if we spend, we have 50 milliseconds in our web response. We're doing our normal work and then, oh crap, we need a GC. So the person getting this response has to wait a half a second for that to complete, and then you do the rest of the work at the request, 50 milliseconds, whatever. So a 100 millisecond request now takes 600 milliseconds. So that's not really acceptable. So really, this history is about how do we make these GC pauses shorter and more interleaved throughout the application actually running. So let's start with 1.8. This 1.8 employ, and as far as I know from Ruby's inception, employed a Mark and Sweep garbage collection algorithm. This is really the simplest possible garbage collection algorithm that you can have. So to talk about how Mark and Sweep works, I need to explain to you how Ruby organizes its memory. Ruby objects are organized in something called the object space. Sometimes this is called the heap, but I find that slightly confusing. Object space is the name of the module that you can use to inspect the object space. So I'm just gonna call this part of the Ruby's heap the object space. It's basically just a list of Ruby objects. So every Ruby object has one R value, which is the name of the C structure that we hold these Ruby objects, right? R values are 40 bytes, and they contain some information about the Ruby object. It's just a list of, R values is just a list of some flags, like have we marked this object yet for garbage collection, some other information, and also some very basic information about the object. This all depends on what type of object it is. If it's a string, an array, et cetera, et cetera, et cetera. If it's a string, if it's a very small string, we can contain that string right in the R value. It's actually like 23 bytes or smaller, it's like a whole great blog post by Pat Shaughnessy about why Ruby can only store 23 byte strings inside the R value. If the data is too large to store in the R value, so strings longer than 24 characters or arrays of five elements or more. So if it's too big to fit in that R value, we just store the pointer to space somewhere else on Ruby's heap. The object space is a list of 16 kilobyte heap pages and of 400 R values each. So it's just a long list of these heap pages which contain all of our R values. So what triggers GC? I think one common misconception is that GC just sort of, well, people don't really understand when it runs at all. That kind of runs on a loop or like it runs every five seconds or whatever. Really, there are two main things that trigger GC, although there's more conditions than this that I'm not going to get into. Either we run out of open slots for new R values or we allocate enough memory on the heap that passes a limit. So if we just run out of object slots, we have to get new slots, right? So Ruby will garbage collect to try to reuse the slots that it has already allocated for itself. The other malloc limit is when, like I said, if a string, for example, is 1,000 characters long, it's going to have to allocate space on the heap for that 1,000 character string because it doesn't fit in the R value, right? So that's 1,000 characters, which would be 8,000 bytes or whatever, or bits. So once the amount of space that we allocate in that way crosses a certain threshold, we garbage collect. That's called the malloc limit. There's also some other limits which I'm not going to talk about, but those are kind of the main things that end up triggering GC in Ruby. So when we trigger GC, we're going to start with the root objects. And root objects are things like classes, constants, things that don't get garbage collected ever. So we start with the Rails.Application object. And then we're going to look for all the references to that object and traverse down the object graph. So each object that references Rails.Application, for example, which I don't know if is really a root object or not. Maybe Rails.is, but anyway, we start at our root object, traverse down the object graph until we've marked everything that references each other. And then any object at the end of that, which hasn't been marked, is free, because it's nothing's referencing it, right? Pretty simple. And then all the free rvalue slots get added to the free list. Simple as that. And then in the future, so that's the marking phase, and then I should say marking phase, and then sweep puts objects into the free list. So setting the mark bits, marking objects is one phase, sweeping them into the free list as the next. And then the next time we need an rvalue slot for an object, we just grab one off the free list. If we run out of, even if after garbage collection, if we still don't have any free slots, then we allocate more slots using malloc to put more slot, to create more heat pages and get more slots. In 1.8, this is done with the FL mark flag. This is one of the flags in rvalue at the time. And you just set that to one or zero after you mark an object. If it was zero when you were all done marking, that was a free object. Of course, sorry, I should say an object that could be freed during sweeping. So garbage collection looked like this. You had a whole long mark phase and a whole long sweep phase. Then you were done with GC. You could go do whatever you wanted to do. And then you have the GC again eventually and a long mark phase and a long sweep phase. Sweeping always immediately came after marking. There's no pause in between. And so in a large Rails application that had 400,000 objects, even a million active objects, this could take a very long time. Could take a million objects, could take a second to mark all the way through, right? Because we have to mark through the entire object graph. So that takes a long time. No Ruby can be executed during that time. And like I said, that's not really convenient for a web application where if normally your responses only take 10 milliseconds to process, if someone gets 10 milliseconds plus one second of GC pause, that's kind of annoying and not really performant. So next came 1.8 Ruby Enterprise Edition. This is a project of Fusion, the guys that write the passenger web server. This was kind of like the big, I don't know if anyone here used Ruby Enterprise Edition. That's kind of like old school now, yeah. So it had a couple things going for it. One of it was it's support for copy and write memory. So let's talk about copy and write. We have these things called forking web servers in Ruby because we don't have real parallelism I should say. You can't execute two bits of Ruby code at the same time. So what we do instead is we have two different processes, two different web server processes that are the same and can just execute different responses at the same time. So Puma, which I am on the committer team for is an example of a forking web server, also passenger and unicorn do that. And when you fork a process, the child process shares its memory with the parent to the extent that it can. And it does that through copy and write memory. So we have to talk about shared versus private memory. So the child process when it gets forked from the parent, gets a, not a copy, but it gets some addresses that looks like a copy of the parent's memory. And that's shared memory. And it's just the exact copy of the parent's processes memory. But whenever it tries to modify one, the child process tries to modify one of those memory pages, the operating system in the background will copy it. So it's kind of like a copy on demand thing, right? So it's very important. When are you writing to memory locations in a child process? Because that'll create a copy which will increase your memory usage. So this copy and write is the whole reason that when you have a Puma process with three workers, you don't see exactly three times the memory usage that you do when you have one process. Cause we're sharing some small portion, 10 to 30% of that memory between the child and the process. This is the whole reason why you pre-boot or pre-load depending on what your web server is. So the idea is that you load the entire application first before forking. So you can share the maximum amount of memory possible, right? The problem with this, right? If you remember my description of how 1.8 worked, we touch the FL mark flag every time we mark. So you set the mark from zero to one and one to zero and that touches that memory location which ends up copying the memory page. So we couldn't garbage collect without breaking copy on write, right? Every time you garbage collected, it would create copies of all the memory in the child process and now you're back to where you started with memory usage. So that was kind of inconvenient and it's kind of a wasteful use of memory. Ruby Enterprise Edition fixed that with a method I'm gonna talk about later cause we got it for in core and 2.0. And Ruby Enterprise Edition ended up using the exact same method that 2.0 used. So we'll talk about that in a minute. In the meantime, 1.9, which I think actually came in .3, Narhira Nakamura gave us lazy sweeping. So the whole idea is you don't sweep until you need it. It's, we're trying to break the sweep phase into multiple smaller phases. We're still gonna have one big long stop the world mark but we're gonna sweep only in smaller increments when we actually need it, which is when we need new slots. So we just sweep until we have enough free objects to have to deal with whatever were Alec were, whatever objects were creating and then we go back to program execution until we need more slots and then sweep again. So we're doing the mark phase just like we did in 1.8 but then we'll sweep a little bit immediately for whatever we needed but then we'll go back to program execution and then sweep objects into the free list only as necessary. So this decreases the worst case times, right? Because rather than having one long mark followed by one very long sweep, you have one mark followed by several smaller sweeps. So the worst case that you could have for a GC pause time was greatly reduced. So mark sweep sweep sweep and then mark again. In 2.0, we got official copy on write support. I mean, technically you're always supporting copy on write. It just either works well or it doesn't work at all. Like it's all, copy on write only happens at the operating system level. So technically, like you already supported it. You just didn't take advantage of it really. And we got that through something called bitmap marking which is another Naruto Nakamura commit. The whole idea with bitmap marking is okay, well if touching the FL mark bit on the R value triggers that copy, let's just keep the mark bit somewhere else where in a different memory location so that when we touch it, we're only copying mark bits and we're not copying this 40 byte R value object. And that's done through bit mapping. So instead of keeping the FL mark bit in R value, we keep it in a separate array of bits. And we can get that, it's not like a point or anything, it's just an address. So if our R value is at memory address 0x whatever, we just do some arithmetic to calculate where the mark bit is. And that's done through something called memory address aligning. So you can just do some arithmetic to figure that out to figure out where the bit for marking is. So that makes Ruby copy on write friendly, yay. And this saw pretty big memory reduction usage for people using forking web servers. And it's still marking with lazy sweeping. So we're still marked with sweep, sweep, sweep, we just kind of changed where the mark bit actually resides, right? It now resides in a separate area, not in the R value. There are a couple more bits now that are bitmap marked in this way that aren't stored in R value. We'll probably get to those in a bit. In 2.1, we got generational garbage collection. This is where we start, 2.1 is where we start seeing some actually like big changes in how GC is actually done. This was written by Koichi Sasada who's a Rails Ruby core team member, works for Heroku now, actually doesn't work for Heroku anymore, works for Cookpad, did work for Heroku for a while, but one of the few people paid to work on MRI. And the whole idea of generational garbage collection is that certain objects are gonna be marked as the old generation and certain objects are gonna be marked and the other objects are just gonna be new objects. New objects become old when they survive a certain amount of garbage collections. I don't think this is actually the correct number for what it was in 2.1. Now, if you survive three collections, you are promoted, if you survive three marking phases, I should say, you're now promoted to an old object. Back then, I think it might have been less. Now we have minor and major GCs. So the whole reason you have generational garbage collection is based on this thesis that most objects die young. Most objects are just around for the duration of a web request or whatever, and then they can be garbage collected after that. And traversing old objects, looking for stuff to garbage collect there, like if you're going through the Ruby code, if you're going through all the objects that get created when Rails starts up, it's kind of inefficient to traverse those objects every time we mark, right? Because they're not going anywhere. The only objects that are probably gonna go somewhere are the ones that we just created, like request headers or whatever. So during a minor GC, we're only gonna look at new objects to mark and during a major GC, we're gonna look at all objects. We're gonna look at new and old objects in the marking phase. So all we've done is just created two different kinds of marking phases. The problem is now you have to keep track of these different kinds of objects. So the way that this gets explained, and I'm pretty sure this is how it works in GC.cs, we have three different colors. We have black, gray, and white. All objects start as white. Then all root objects, which I explained in 1.8, root objects get colored gray. And then from each gray object, we're gonna traverse the object graph and look at all the references and we're gonna turn those objects gray. Then when we're done, when we marked all the references to this object gray, we turn the original object black. Then we keep going through the tree until all objects are black or white. Okay, now the problem is when we have a black object that refers directly to a white object. So that shouldn't happen, right? Only gray objects should refer to white objects. To solve this problem, we have something called the right barrier. The right barrier just is a barrier for realizing whenever an object creates a reference to another object. So we've put a barrier between you being able to reference an object directly. The right barrier is used to implement what was originally called sunny and shady objects. Now it's called right barrier protected and unprotected. Sunny objects are protected by right barriers. Shady objects are not. And the reason we do this is because with a sunny object, we can track when new references get added to it. Shady objects, we can't. In the generational garbage collector, we have to check shady objects every minor, every garbage collection during minor and major because we can't know whether or not you added a new reference to that object or not. And sunny objects, we know whether or not you did so we can actually promote them to old generation and then not look at them for new references. All right, explain that. So sunny or white right barrier unprotected objects can never become old. They can never be promoted to the old generation. We have to check them during every minor garbage collection. Now, one of the major sources of right barrier unprotected objects are C extensions. If a C extension has direct access to the Ruby object, if it knows where it is in memory, we can't be sure that C extension hasn't added some references or that isn't using that object. So we cannot promote those objects. Right barrier unprotected objects. The right barrier unprotected objects are added to something called the remembered set. The remembered set just contains right barrier unprotected objects and any old generation objects which have a new object added to them which we detect via the right barrier. So if you have like an old, let's say like a big array that you're adding and removing objects from and it's like a stack or something that you use throughout the whole application. So it becomes an old object, okay? When you add a new element to that array, we wanna make sure it doesn't get garbage collected, right? Because if a minor GC marking phase happens, we've added a new object to this old array. That new object, if we didn't have the right barrier would get garbage collected because an old object wouldn't, we don't look at the old objects during this minor GC marking phase. And so that means that our new object is white and so it'll get garbage collected. We don't think that it has any references to it but the right barrier means that when we create that reference, when we add the white new object to the old black array, we will catch that and Ruby says, ah, okay, I will add this old object to the remembered set. So I will check it during the next minor GC. Okay, so now garbage collection looks like this. This is what we had in 2.0 and also 1.9. This, I should say this diagram and the previous diagram are from Koichi's talk about Ruby 2.2 GC. So these are all Koichi's diagrams. This is what we had in 1.9.3 and 2.0. You had a long mark phase and a short lazy sweep when you just swept objects into the free list as you needed them. Then in generational GC, we introduced the two different types of GCs, minor and major. Minor GCs are quicker because there are fewer new objects than there are total objects, right? So you have a short mark phase followed by short sweeps. Once you've done, and then there's major GC, which is the same as it was before. Major GC in Ruby 2.1 and up took exactly the same amount of time that a regular GC did in 2.0 and below just because it's not any different. In fact, it probably took a little bit longer because now you have to keep track of the object age slightly longer, not like anything crazy different. So the whole advantage here is most of the time we're gonna be doing minor GCs. And you can check this yourself. If you look at GC.stat and then you're running Ruby Process, you can see how many minor and major GCs it's done. And usually you'll see minor GCs happen twice as often as major GCs. And sometimes more depending on the age of the process and like what you're doing with it. So that's great. It means we have shorter pause times on average, which means that we're less likely to have some big GC pause during their quest. We still have them though with major GCs because well, this is just the same as it was in 2.0. Is there anything else that I'm missing? I don't think so. Okay, so we still have this problem, right? Of long major GC mark phases. So we'll fix that with incremental GC in 2.2. Again, this is a Koichi project. So the whole idea with incremental GC is well, let's just break up the mark phase. Let's just break the mark phase into shorter bits. And so we can execute Ruby during the pauses in marking. Now there's a lot of problems here because like I said the whole reason it's easier to just stop and not execute Ruby during marking is because then you don't have to keep track of all the stuff that happens in between. When you start adding stuff to arrays that you thought that you marked already but then you gotta keep track of all the stuff that you added to stuff you already marked. That's not good. So total GC time will remain the same. The idea wasn't to make GC time quicker, it was just to reduce the amount of total time we spend in these long pauses. And this is the point where out of band GC becomes not a thing anymore in Ruby. People still somewhat do this, although it's kind of dangerous. Out of band GC was basically the idea that before a request in a web application you could disable the GC with GC.disable or whatever, do your request and response and then enable the GC again. Now that has a lot of problems and it's kind of dicey because what happens if GC doesn't get re-enabled somehow? What happens if during a request you allocate a bajillion objects and now GC is disabled? So well you need a bajillion megs of RAM, right? So it was always kind of a dicey proposition and at this point if we can make mark phases shorter, which means that our maximum pause time for GC ever is gonna be 10, 15 milliseconds, well then what's the point of out of band GC, right? It's just kind of like adding this possible failure point when the maximum GC pause is only 15 seconds. It's not like a huge benefit of moving it out of the request depending on your application and what your goals are or whatever. So, but this is the problem I was talking about is that we can do a mark phase and then execute some more Ruby, but now objects which you've already marked can have new white objects directly referring to them, which isn't good because we don't go back and look at black objects again usually, right? So this just goes back to the right barrier. Now the right barrier in addition to tracking old generation objects which get new references to new objects written, which, sorry, old objects which track references to new objects. Now we just have to do that basically all the time during marking. After a mark phase is over, we check the right barrier for objects which got new references to them. So it just adds other things to the remembered set. You can check the size of the remembered set in GC.stat. Again, I recommend just taking a look at this. It's kind of opaque. Like the keys are a little bit hard to figure out what they mean and GC.stat changes a lot. It changes in every Ruby version pretty much. So, but you can get an idea the remembered objects is sort of like just right in there. And it'll also tell you like how many objects are protected by the right barrier and how many are not. From a practical perspective, the right barrier now is so important after 2.2 for incremental and generational GC. What you might have caught is that this means that C extensions which touch or make shady or basically remove the right barrier from large numbers of objects that could be really bad, right? Because the more objects which are not protected by the right barrier, the more objects we have to check every minor GC which isn't the idea. We're trying to make minor GCs as short as possible. So I've never seen this happen in production because I don't know of any C extension that does it but it would be interesting because that bug is kind of possible where a C extension touches tons of Ruby objects, makes them shady slash right barrier unprotected and suddenly your minor GC pauses are just as long as your majors and you've lost the benefit of generational and incremental GC. Kind of interesting. Symbol GC, we also got that in 2.2. It's not really that complicated. Now symbols are garbage collected too. 2.3, not much changed. That was 2.2 was kind of like the last major change to the garbage collector that we've gotten. Kouichi introduced a new experimental right barrier in 2.3 which his assumption was that we can assume new objects referred to from old objects are probably also long-lived objects. So if we add a new object to an old generation object, we're gonna assume that this new object will probably become an old object. It's probably gonna stick around for a while. So for example, adding a item to an array in a config file, right? So if you, I can only give like really specific century examples, but changing the setting in a config, right? You're creating a new object, you add it to your config object. That object's gonna stick around for a while and it would be better if we just made it old right away. Cause if we made it old right away, it doesn't have to be marked for three minor GCs and we could just ignore it until the next major. The problem with this is, well what happens if that object isn't, you know, it is not going to become an old object. So if you have like my stack example from earlier, if you have some stack object in your application that you're constantly taking items in and out of with this new algorithm that any object added to your long living stack would automatically get marked in the old generation. So that wouldn't be great cause you're taking stuff in and out of it all the time. So that would greatly increase the memory usage of your stack. I guess this experiment didn't work or he didn't like the results because it's still commented out in the rubygc.c code. It's still in there. I don't know what he ended up thinking about it but it's not been enabled yet. You can mess with it if you know how to compile ruby and want to comment it out or whatever but kind of interesting. 2.4, we got a couple more little changes. You now can control somewhat the amount of three slots ruby tries to keep open. So the whole idea with GC and when it triggers and how many slots and how often we want it to trigger is that GC is expensive, takes time. We don't want to do it that often. So when we do do it, we're going to try to make sure we don't have to do it very often again. So what ruby does is let's say you have a million free slots and you have to garbage collect. Okay, nothing got garbage selected. Everything is being used up. So we still have a million slots but we need more room. Ruby will grow the amount of slots by a factor. And that factor I think by default now is 1.4. So with a million free slots, if we need more slots we're going to grow to a 1.4 million slot heap. That factor is configurable by an environment variable. But let's say we get to 1.4 million, whatever and then five minutes later we GC again and now we're only using 500,000 of our 1.4 million slots. Well, that's kind of inefficient, right? We should return some of that space back to the operating system if we can. And that is what that control over that ratio of what ratio between used and empty slots is now configurable I should say in Ruby 2.4 by an environment variable. These environment variables, I talk about them sometimes in talks but I tell people not to touch them usually. If you can find them in GC.C you're probably also smart enough to figure out what exactly they do. It's very easy to cripple a Ruby process if you just copy and paste someone else's settings for these things. So I don't even tell people where to find them or how to look at them. But if you can find them in GC.C you can probably figure out how to use them. But this is an interesting one because one thing that I see a lot in production with Ruby processes is they get in their steady state. Where am I at? Okay. In their long-term steady state after six hours or whatever they have too many free slots. And they're not returning those free slots back if they can. Which means a long-term higher memory usage, right? So I've been interested in this. I haven't played with it yet to see if it reduces long-term memory usage for people's real applications. Part of the problem with returning free slots back to the operating system, and I've talked about this in other talks is memory fragmentation. Ruby can only release a slot back to the operating system if the entire heat page, so 400 slots in the page have to all be free. That's called a tomb page if you see that referred to anywhere in Ruby. A heat page with an object in it is an Eden page and a page with no objects in it is a tomb page. So page has to be a tomb page for us to release it back to the operating system because we can't move objects around the heap. Because we don't see see right-barrier unprotected objects, right? These are referred to by C extensions. They have just the direct memory address. There's no indirection between that C API access and where that object resides in memory so we can't move the object. Otherwise we'd break the C extension. So that means we can't move objects around and which in turn means we can't free pages back unless there's no slots. Being used in the page, which happens about never. It's very easy for the Ruby object space and heap to look like Swiss cheese with an old object here and then a bunch of new objects and old objects and when that gets garbage collected it just looks like this big Swiss cheese heap. There's a couple of things which are being done to help with this. So let's talk about those. Now we can talk about the future about where Ruby GC seems to be going. And the main thing I think that most people are gonna be interested in is garbage compaction which is moving objects around the object space and moving them around the heap to make them less Swiss cheesy and more griere. I don't know, it looks like not cheesy about holes in it, I don't know. J Ruby and Truffle Ruby already do this because they're on the JVM or Grawl or whatever and those garbage collectors are compacting or at least some of the JVM garbage collectors compact. So they get to take advantage of all that for free and it just compacts for them and that's great. Aaron Patterson has been working on this after a hallway conversation in Ruby Comp he realized that basically we might be able to move some objects around the heap so long as we don't have any, we can make sure that they're not being referred to by C-Extensions and he's been working on that if you follow his Twitter, he's been posting about it every once in a while. It's a very interesting project. It will never be able to move all objects around the heap but we might be able to move some of them and improve fragmentation and being able to return some memory back to the operating system somewhat. So what this means in the long term is that your big long term Ruby processes will probably use somewhat less RSS or memory in the long term. Another approach here besides being able to move objects around is to just put them in separate areas to begin with. So if we have an object and we need to put that object somewhere, right? If we think that this object is probably going to be old and it's probably gonna stick around we're gonna put that in one pile and then any object like an instance variable that we think is not going to be around for a long time we'll put that in another pile. And in theory what that means is our old object pile will be less fragmented than our new object pile, right? Because old objects probably won't get garbage collected and all the new objects that'll get frequently freed up and whatever. So that should reduce memory fragmentation and therefore less total RSS usage. Guilds, so everyone talks about guilds probably not coming for a while but I was told Ruby 2.6 or 2.7. So guilds will be sharing the same object space in the plan right now. I mean this is all super early, whatever but guilds are Ruby's future plan concurrency model if you're not familiar with this. But the idea is that these guilds are gonna be sort of like lightweight processes almost and they will share the same heap. So everything that I've talked about and all this garbage collection is not gonna change when we get guilds. I did sort of an interesting side note. For more reading, I suggest these three authors, Pat Shaughnessy wrote a great book called Ruby Under the Microscope which is the starting point if you wanna learn more about how Ruby works internally. Koichi has written a couple things online in English about how Ruby Garbage Collection works and Sam Saffron of Discourse has as well. If this was interesting to you, I have a course called the Complete Guide to Rails Performance which is a bunch of screencasts and a very long book about optimizing rails applications. It's all at railspeed.com. You may have seen my blog, it kinda looks like this. I write about Ruby performance from time to time and that blog is at speedshop.co. Thank you very much. We have more than five minutes for questions. Yeah. Container space, containers, containers, containers. Everyone's talking about containers. Where do you see Ruby, not just in general, Rails, Ruby, Rails fitting in within the container space because I just tend to say, just put it into container. Don't worry about it, put it into container. Well, that's pretty much what everyone does already because, yeah, sure, sure. So the question was like, how do I see Ruby fitting into containers? I think the fact is that most people run Ruby in a container already because most people run Ruby on Heroku which uses LXC, where do Terrence go? Yeah, well, Terrence is, who's giving me a talk next could maybe even say a little bit more about it, but Heroku uses LXC, I think, which is containers and so it's really not or hypervisor or whatever. So I don't see a lot of people running Ruby on Docker yet, but most people are running Ruby already in a containerized environment whether they realize it or not, or depending on your definition of container. I think what's interesting and what kind of relates back to this talk is that most people running in these environments tend to run them in very resource constrained environments and memory is usually constrained in these environments. You know, like the default Heroku Dino size, right? It's like 512 megabytes, which is enough for like half a Rails process. So I think people's memory knowledge is important for being able to shove big old legacy Rails applications into small containers, but increasingly I'm starting to think that CPU is also an extremely important thing in these environments. Like Amazon's VCPUs are just hyper threads. So like, you can have like eight gigabytes of RAM and then get like two hyper threads. So that's like nothing, you know, you can run like 1.5 Puma processes on that. So I gotta do some benchmarking and look at this more, but like memory is a very important constraint and understanding how it works and when GC occurs is gonna help you with shoving stuff into these small containers, but CPU is also an increasingly scarce resource in the cloud, I guess. Yes? So there's two things that Ruby's done over the years to improve GC. One is all the stuff you talked about and the other has been trying to not allocate objects to begin with like, like flow nums so that there are no other objects that are tracked by the GC. But then there was also set throws and string literal. So I was curious if you have any experience with how much library is changing to use that as reduced memory. Yeah, so the question is like, we've also, the other side, the flip side, which I didn't talk about of trying to reduce GC and memory pressure is just don't allocate new objects in the first place. So the example was pushing frozen string literals. So I think, was it 2.2? We got the magic comment at the top of the file that was frozen string literal or you could just make the whole file. Sorry? 2.3. 2.3, okay. We got the dot freeze and 2.3. Oh, okay. So that frozen string literal and the dot freeze or whatever, right, was intended so that we freeze an object so it's just not garbage collected, right? So you kind of like, it's almost like hinting for the garbage collector because you're just saying, hey, don't ever collect, this object is not ever gonna get collected, right? So I was also interested in that. I actually asked Kouishi about frozen string literal, the magic comment at RubyConf. And because I think part of the original promise of the magic comment was that you would, that they were thinking about making that core language feature that just all strings would all be frozen. And so it was sort of an experiment to put in the runtime to see like, okay, does that actually make people's applications faster? Kouishi feels, and I also sort of feel that that hasn't really been a huge performance impact. Just either not enough stuff can be frozen or when you do freeze stuff, you end up creating new strings anyway to create the copy or whatever. And you sort of lose whatever gains you thought you had. So I haven't seen any real world benefit, major real world benefit from that. And it doesn't seem like it's gonna change or become like that path where we're at right now with freezing objects. Doesn't seem like it's gonna change to me so much in the future. I asked him that in the context of guilds, right? Because the whole thing with guilds is we can share frozen objects between guilds. We know they're not gonna be modified, right? So we can share these frozen objects between the different processes without fear of modification. So I said, well, you know, isn't that when we wanna create more shared objects then? And I guess Kouishi feels that that's the way that it is currently is adequate, I guess. Yeah. About locality, I mean that objects that are allocated nearly at the same time often also will be used nearly at the same time. Sweep is, mark and sweep is known for destroying locality. For the compaction, are you considering algorithm which preserve locality or like the Morris algorithm which also destroys locality? Yeah, so I am not a Ruby core developer. I've never committed to Ruby, actually, I don't think, yeah. So the person you have to ask is Aaron Patterson because he's the one doing all the work in compaction in C Ruby right now. So if you just Google Aaron Patterson Ruby and you'll find him, he's hard to miss. Yeah. I think that that's kind of like what has been talked about with putting objects in an old heap and a new heap, that the new heap, you're preserving locality that way, I guess. But that's kind of outside my area of expertise to be honest. Yeah. Two minutes from question. Is it really interesting to make the GC learn to adapt to the application? Because we have one like one to fit them all and for instance the 1.4 ratio you were talking about. Yeah. Stuff like this could benefit from the GC learning from how it works with a specific application. Yeah. The question was how can we make the GC learn how to perform based on how to make decisions based on the actual application performance. What do you think? I don't really think there's anything in GC.C right now I think that could be described as doing that. I think all the important knobs really are set by environment variables beforehand. One thing that Ruby GC does do is these thresholds like for increasing the heap size do change because it's like based on a ratio, right? Like 1.4 is a ratio of the current heap slots used. There's also ways to cap that. Like you can say 1.4 or 300,000 heap slots whichever is less. So there's some changes in these thresholds as the application executes but I wouldn't describe it as the run time learning about itself. I think that's interesting because one of the problems that MRI core maintainers have is, Rails long running Rails processes have like one very specific set of behavior and then there's people that still use Ruby for 10 line sysadmin scripts and then someone comes into that, someone comes into the discussion and says, hey, wait a second, you can't do that because you're going to break my 64 megabytes of RAM, Raspberry Pi, whatever, like some crazy environment that they run Ruby on. Ruby has to cater to all these use cases. Yeah, and so there's just so many different ways that you can run Ruby that it's difficult for a core language maintainer to make aggressive decisions for one workload or another workload. So that's an interesting path forward. As far as I know, nothing has really been done on that front yet. Thank you Nate.