RubyConf 2019 – Compacting Heaps in Ruby 2.7 by Aaron Patterson


>>Hello. Hello. Okay. I guess let’s do this. Unfortunately, my oh. Okay. I got to shut off the Wi Fi here. Um, hello. Oh geez. Am I up here? Okay. Hello, everybody! Hello! I’m really, really excited to be here today. I’m so excited to give a 40 minute long ignite
style presentation. Which was the slide I was going to put in
here, but my tethering is too slow, so I’m just telling you the joke instead of putting
it in slide form. Imagine that there is an ignite slide here. Okay. So, I’m wearing this hamburger hat today because
I heard I needed to dress up, wear my formal attire. But I’m gonna take this off now, because I
feel it’s probably bad for the recording. I don’t want my face to get all dark. Hello everybody. Hamburger hat! So we’re going be talking about compacting
GC for MRI. Hello. Hello! I apologize. I know this is the third day. We’re nearing the end of the third day and
you have come to an extremely technical presentation. So, my apologies. My name is Aaron Paterson. My given name is @tenderlove. I changed my name to Aaron. You can call me either one of those. This is what I look like on the internet. I have two cats. This one’s name is Gorbachev Puff Puff Thunder
Horse. He is the most famous of my cats. I have stickers of him. If you would like a sticker. This is Sea Tac Facebook
>>CAPTIONER: Don’t wait for me… I got Gorbachev!>>I am extremely nervous in front of you
all today. But actually, I stayed at a Holiday Inn Express. Here is me. I’m on the Rails core team. We’re the team responsible for developing
Rails, and I looked up my commits, and my first commit was here in 2009, so I have been
a commiter my first commit was about ten years ago, but I have been on the core team since
2011. I’m also on the Ruby core team which is responsible
for developing Ruby. And my very first commit to Ruby core was
ten years ago in October. This is my ten year anniversary on the Ruby
core team. [ Applause ]
Thank you! Now, this is not to say that I know what I
am talking about. That is absolutely not true. I need you to take your expectations for this
presentation and lower them a bit for me. And when they’re down here, bring them down
just a bit more. I’d really appreciate that. The way that I got on to the Ruby core team
is I was able to crash Ruby a whole lot, and I got a lot of seg faults and those produced
core files, and they were Ruby core files. Ahhhh! All of you may actually already be on the
Ruby core team. What you should do is go check and do ls /cores
and you may also have many Ruby core files as well. So, today I’m going to be talking about compacting
GC for MRI, but first I want to talk about controversial features in Ruby 2.7. Matz talked a little about these in his presentation
and I want to talk about them too. The first one is the pipeline operator. It got deleted, but I want to talk about it
anyway. It looks like this. And I know that there was a lot of controversy
around this. People did not like this operator implemented
in this way. But I thought to myself that Ruby is designed
for developer happiness, as Matz says. I would change it to a smile operator. You can see the pull request. It’s up there. Unfortunately, it got rejected and I think
it’s because I guess it’s because the pipeline operator was canceled. So, that’s unfortunate. We could have had emojis in there, but we
do not. The next thing I want to talk about is Typed
Ruby. How many of you are excited about getting
types into Ruby? Yeah! All right. That’s great. That’s great. But the thing I don’t understand about it
is I thought we already typed Ruby… I know some of you may copy and paste from
stack overflow, but we typically type things so… Let’s talk about let’s actually get down to
what we’re supposed to be talking about today, which is manual heap compaction for MRI. This is a patch that I wrote for Ruby, and
it’s in Ruby now. It took me about three years to complete this
work. And I have to admit, this is the most difficult
project I have ever worked on in my life. This is the most difficult thing I’ve ever
written in my life. And I usually don’t say that I have any new
ideas. I’ll give presentations and I’ll say I don’t
think of anything new, but I think I finally had a new idea, and that’s this one. I will show you how I know this. Actually, Chris Seaton tweeted this. He is on the truffle team. He said that I had a novel solution for not
moving objects referenced from C. Now Chris is he comes from an academic background, so
I want to translate this slide a little bit or this tweet a little bit for you. So, when Chris says novel solution, what that
actually means is that I am a genius. [ Laughter ]
So thank you so much. So, I want to, before we get into the meat
of this, I do want to say a quick couple of thank yous. I want to call out a couple of people specifically
to thank. That is Koichi Sasada and Allison McMillan. I’ll tell you why. When I first came up with this project, for
many years I kept thinking oh, it’s impossible to get compaction implemented in MRI and I
just thought that was a thing that couldn’t be done. And I explained this to Koichi and he said
to me, why not? Why is it impossible? And I said to him I don’t know, just many
smart people tell me that and he said maybe you should try it and see if it actually is. And I was like okay. So, then I tried it, and it is possible. But unfortunately, it took me a very long
time to implement this. And it was, as I said, this was the most difficult
project I’ve ever worked on. So, I would get stalled and depressed about
it. And Allison reached out to me and said hey,
I would like to work with you on this project. So, every week I would pair with her on this. And that meant that I don’t particularly like
pairing with people when it’s just me staring at some really not great C code. Trying to figure out what’s going on. So, each week I would try to figure out what
we were gonna do, plan it, and actually do that pairing session. So, she helped me move this project along,
and it wouldn’t be here today without these two people. So today’s topics. I am going to talk about Ruby’s heap, the
compaction algorithm, implementation details, results, and if I have time at the end, I
will talk about debugging techniques. I think a lot of people talk about GC algorithms
and implementations, but I don’t think they talk about debugging garbage collectors and
I want to talk about that if we have time. So what is compaction? It is taking allocated memory and free memory
and rearranging them. Imagine we have memory that looks like this
with allocated interspersed. And we can combine the blocks to look something
like this. This may maybe some of you remember this. This is defragging a hard drive. But instead of defragging persistent data,
instead what we’re doing is defragging data that’s in memory. One question is why would you want to do this? What is the benefit of compaction? One benefit is efficient memory usage. For example we have a heap layout that looks
something like this and we want to allocate a new chunk of memory. That chunk that we would like to allocate
is too wide so it won’t fit in this free area. So, maybe we look at this free area. And it won’t fit there either. It won’t fit in either of the two places so
we will get an out of memory error. We can’t allocate this chunk. If we compacted it, it is wide enough and
we can allocate into that chunk so we’re able to use this memory more efficiently. Another reason to compact is more efficient
use of CPU caches. So, when a program reads memory, it has to
go through the CPU to get the memory. It has to read some memory and in order to
do that, the CPU has to ask your RAM for it. The CPU will read chunks of memory at a time
and actually reading from the RAM is kind of a slow operation, so there are a few caches
in between the CPU and your RAM. And what will happen is the CPU will say I
want to read a chunk into the cache. Hopefully when it is processing, all of the
data we will be dealing with is out of the CPU caches. So, in this case we read this memory here. We have allocated memory and we happened to
get some free memory, too. These are read into the CPU cache. As our program is going along processing the
data in the memory, maybe it gets along to here and it’s like oh, well, I don’t need
the stuff in the free memory. I need a different address and I need to go
somewhere else and get it. What that means is maybe we have to hop over
here and read over there and continue with our program. Doing this hop and reading memory from RAM
is a slow operation, so if we can reduce that, then we’ll have a faster program. If we are rearranging memory like this, hopefully
we’ll rearrange memory such that when we read out of memory, we’ll read everything we need
into a CPU cache and we’ll be able to process all the results. That is a feature or what do you call it? The attribute of this is called good locality. If we have all of the data in the same kind
of area, we call that good locality. Another reason is copy on write friendliness. We use Unicorn and it saves memory by using
a copy on write technique and compaction will increase the efficiency of this particular
technique. What this technique is, is we create a parent
process. We boot up our Rails app and fork off a bunch
of child processes. Those child processes get a copy of the parent
process’s memory. Let’s say we do this and say okay. We have a child process. Now when the child process is created, it
doesn’t actually copy all of the parent’s memory into the child process. Instead it points at the parent process. So, it creates virtual memory that points
at the parent process. Now, let’s say we need to write some data,
the child wants to write some data into its pages. What will happen is the child process will
say okay, I want to write into a free memory chunk. It’ll write into this free memory chunk and
that connection will get removed. So, it’s no longer connected to the parent
process. So, the child process will copy that chunk
down into itself and then write into that location. Now, unfortunately, when this happens, the
operating system takes care of this for us, and the operating system doesn’t copy just
what we need. It copies them in multiples of page sizes. So, that means that instead of just copying
that free chunk down into the child process, we may actually get some stuff on the left
or right of it that we don’t actually need, so instead maybe we copy this allocated memory
down into the child process as well. That means now our child process is consuming
more memory than we originally could have. If we were to eliminate that fragmentation,
it’s less likely that we would encounter situations like this. The solution to all of these problems is essentially
eliminating fragmentation, and fragmentation compaction is the solution to fragmentation. That’s it. Just to recap, fragmented memory looks something
like this. When we compact it to have no fragmentation,
it looks something like this. Now another thing that I need to cover as
well is in Ruby we can think of our programs as having two kind of heaps. So let’s imagine we have our computer system. Our computer system has some amount of memory. Now in that memory, when we allocate memory
from the system, we typically use Malloc to do that. We’ll create what I call a Malloc heap. So, we ask malloc for memory. And inside of that malloc heap is Ruby’s object
heap. We have two different ones, but one is inside
of the other. So, Ruby’s object heap is allocated by malloc
as well. But we can think of them as two separate areas. When we allocate objects, they get allocated
out of Ruby’s object heap. We do object.new, that comes out of Ruby’s
object heap like this. Now ideally the Ruby heap and the malloc heap
would be exactly the same size, but unfortunately they’re not. And I’ll give you a simple example of why
they’re not. Let’s say they have a string. We allocate a new string. This string actually points at a byte array,
and that byte array is allocated out of malloc. So, it’s allocated out of Ruby’s object heap
but the actual string itself is allocated via malloc. This is one reason why the malloc heap will
be larger than Ruby’s object heap. Unfortunately, fragmentation can occur in
both of these heaps. When you talk about eliminating fragmentation,
you need to be specific about which one you’re talking about. So, for the malloc heap, at work we typically
use jemalloc, and I’m not going to get into this, but if you have a Rails application,
you should use anything but glibc. So Ruby’s heap, let’s take a look at Ruby’s
heap. And I don’t mean the system memory or the
malloc heap. I’m talking about what’s stored inside of
there. This is ruby’s heap. Ruby’s heap layout looks something like this. Ruby’s objects are represented by a specific
amount of memory. One of these boxes represents a byte. So, each object is 40 bytes. Each chunk is a slot. The slots can be empty or filled. So, we’ll use white to represent empty and
blue to represent filled. And later I will represent a new color, orange,
and that’s going to represent moved. Now one page, these slots are stored on what’s
called a page and one page is about 16 kilobytes so here we have contiguous slots and they
create a page. One page is 16 kilobytes, and a Ruby heap
is made up of multiple pages. So, this is what a Ruby heap looks like. We have multiple pages here, and each of those
pages is allocated using the malloc system call. Now there’s one more bit of information that
we need to know before we can actually implement compaction and that’s that each slot in Ruby’s
heap has a unique address. So, if we look at all of these slots, each
of them has a unique address. Given this information, we can actually implement
a compaction algorithm. So, let’s look at the algorithm I choose. The one I’m going to use is called a two finger
compaction algorithm. It was originally done in LISP. It is not a very good algorithm. If we have time after, I will tell you why. If you want to ask me a question, say Aaron,
why is this not a good algorithm and I will be happy to explain this to you. When I first started this project, I didn’t
think it was possible, so I wanted to choose something easy to start with. Essentially the algorithm has two different
parts. The first is moving objects and the second
is updating references. First we will look at moving objects. So, imagine we have a heap that looks something
like this. We’ve got a bunch of objects in there. The algorithm works by having two different
fingers or pointers, one to either side of the heap. One pointer is called the free pointer, and
we’ll put that at the left side and the other pointer is called the scan pointer and we’ll
put that at the right side. The free pointer moves to the right until
it can find a free slot. It will go along like this, finds a free spot
and stops. The scan will move to the left until it finds
a filled slot and then it will stop. Once they’re in place, it will swap the two
locations. It swaps like this. And we leave a forwarding address in place
of the old object. So, here this one was at 9, but it is now
at 4. So, we’ll leave the number four here. And we’ll repeat this process. We’ll move the free pointer over. We’ll move the scan pointer back. Swap the two. Leave a forwarding address, and then we repeat
this process until the two pointers have met. So once the two have met, we are done moving
objects around. The next part is updating references. So, let’s say we have that same heap. It looks something like this. After objects are moved it will look like
this. What we do to update the references is we
walk through each object looking at the references and updating them. So, for example we start by looking at one. One points at six, but it’s fine. It’s not a forwarding address. So, we move on to two and two points at nine
but nine is now at four so we update it to point at four. So, we say okay. Point over here at four. And we do that for three as well. Three points at eight, but it should go to
five, so we update that. Thank you. And then we continue this process for each
of the objects in the heap, and then we’re done updating references, and all we have
left to do is we convert those moved addresses into free slots, and we’ve successfully compacted
the heap. This is it. So, if we were to rewrite this algorithm in
Ruby, this is a Ruby conference, not a C conference… right? Though I will give you pointers in C if you
would like. [ Laughter ]
Sorry! Okay. So, this is what the algorithm would look
like. The left and right (((TEXT MISSING DUE TO SOFTWARE CRASH DURING
TALK))) So for example, we need to know how do hashes
hold references, how do arrays hold references, how do objects hold references, expressions,
structs, etc. So, this is actually the most complicated
part of this patch. So, I’m gonna show you the actual code here. Sorry. It’s C code. This is it. One sec. Here is the entire patch. And that is only the reference updating part
of this. If I was to estimate, I would say maybe 80%
of the patch is just updating references. So, since updating references is the most
complicated part of this, I want to talk about this part of the project. This is probably the hardest part for me to
implement, and the main thing is we need to be able to update references while supporting
C extensions. And I came up with a scheme for doing this
and, I think this scheme is the novel idea. Which isn’t trust me, it’s not that novel. So, we have to answer a question like where
are references stored? The most difficult thing about updating references
is figuring out where all of them are stored. Now today, regardless of whether or not you
have any Ruby core files on your computer, we are all part of the Ruby core team today,
okay? All of us. Now a nice thing for us, while we are all
hacking on this garbage collector is that we can say, okay, how do an array store its
references? So for example, we have an array here, and
we can go look at the source code for array. And if we go up array.c, we can see that the
array points at a buffer. This buffer just contains a buffer of value
stars, and it points at a bunch of different objects. Since all of us know how this is implemented,
we can actually go into the garbage collector and say okay, when it’s time to update references,
we know how the references are stored so we can actually go in and write the code to fix
them up. We can do the same thing for hashes as well. We know that a hash will point to a list of
values and a list of keys. And we can look at the implementation of a
hash and since we all know the implementation of the hash, we can write the reference updating
code. And we just repeat this process for every
single class that’s in Ruby. So, we do it for strings, classes, modules,
symbols, reg xs, all of the fun stuff. What I like to call the types is I like to
call these types known types. So, the garbage collector can update all known
types. Known types are types that are implemented
by Ruby, by Ruby the language. But that leaves a question for us. What about unknown types and I refer to unknown
types as types that are implemented in C. And I will give you an example of one. We use a JSON parser called yajl. Yajl is written in C and this is the struct
that is used for that parser. You don’t need to understand C to get this. I will explain it. But essentially these two values here, the
struct points at two values. These are Ruby objects. It has references to two Ruby objects, okay? When we create a new yajl parser, we will
Malloc the struct. Then we will also allocate a Ruby object allocated
out of the Ruby heap. This Ruby object will point at that Malloc’s
data and this is what we deal with in our Ruby programs. Now this struct will point it has two references
as we noticed here in the struct definition. It points at a builder stack and a parse_complete_callback. I don’t know what they are, but what’s important
is they are two Ruby objects. Now unfortunately, the garbage collector doesn’t
know anything about this struct. Since it doesn’t know, it can’t update the
references. So, how do we prevent those objects from moving? Now the garbage collector will look and say
I don’t know what this is. I can’t fix it. Unfortunately, if any of these objects move,
say that one moves, now it’s moved somewhere else and the program will just crash. Hopefully. [ Laughter ]
Yes. Let me I am going to convey to you how scary
this is. Imagine okay. I’m going off script here. Imagine that Ruby object moves, okay? So it moves. It goes away. GC says I don’t know. This thing moves away. Imagine the program continues to run and it
doesn’t crash. Like we don’t use the agile for a little bit. Another object gets allocated, another Ruby
object gets allocated but allocated in its place. Now the reference is good but it’s pointing
at the wrong thing. So just imagine that in your program. So, yes. This is scary. So, we really, really want to prevent this
from happening. All right. So how do we do that? The way that we do that is all of these c
extensions need to implement a mark function. They have to keep these references alive. If they don’t keep the references alive, the
garbage collector will of course collect them, and then the program will crash anyway. So, C extension authors need to write a mark
function that marks the references. And this is from the yajl C extension and
it’s marking its references here. We can say every time we mark something, the
C extension author has to call rb_gc_mark and they pass it in. So, these objects go through rb_gc_mark, and
anything that passes through rb_gc will not allow it to move. So, anything marked with rb_gc_mark cannot
move. I had to add something called pinning bits. It’s a bit table that sits alongside of the
objects. So, let’s say is we have an object layout
that looks like this. We have an array at the top. I want to demonstrate the difference between
a known and unknown type. During the mark phase, we’ll go through and
mark this. The GC will mark, yajl will mark those in
the pin bit table. When the array goes to mark, it will mark
its references, but it doesn’t use that function, it uses a different function. One we have implemented in Ruby and is private. They get marked but not pinned in the pin
bit table. Now when the garbage collector goes to compact,
we go and do the same algorithm, so we’ll scan along, but what will happen is when the
scan pointer finds something that’s pinned, it just skips it and goes to the next one. So, all the objects that were pinned, they
don’t move, and we just continue with this algorithm as normal, swapping, leaving forwarding
addresses, etc. Okay. Great. Good job, keynote. And it’s that fast, too, GC. So when we go through and update the references,
the GC will look at yajl and say I don’t know this type. I can’t update it. But it doesn’t matter because we’ve guaranteed
that all of its references stay put. They don’t move. So, it will update the array. The array gets updated as normal. All the objects get updated as normal. Okay. Bleh. Thank you. That fast. So known types will use gc_mark_no_pin. I have changed it to movable. I think that’s better than no_pin. So, we know not to move those. This is how we can keep our c extensions safe,
and this is the novel idea, I guess. So I want to talk a little bit about allowing
movement in C extensions. We would like to write extensions that are
compaction friendly. So, what I did to do that is I implemented
three things. Compaction callback, movable marking, and
a new location function. The GC can’t update C extensions, but C extensions,
they can update themselves. So, what I did is I said okay. We’re going to update yajl. Here we specify a mark function and a free
function. We’ll update this to have a third callback
which is the compact function. This is updated code. We specified compact callback. This gets called after every compaction. Now we change the mark function, rather than
calling RBGC mark. That says I want you to mark this but it’s
okay if the object moves. Then inside of our compaction callback, we
call this function, rb_gc_location and this asks the garbage collector hey, if this thing
moved, give me the updated address. And that’s how we implement that. There is one known issue, and this is kind
of a doozy. Let’s say we have an object graph that looks
like this. We have an object implemented in Ruby and
one implemented in C. Imagine I am a C extension author, and I know that my Ruby object, it
points at this third object and I know that the C object also points at this third object,
and I know that the Ruby object will keep this third object alive. So, I think to myself, ah, I am clever. I know that the Ruby object will keep the
third object alive, so I’m not going to mark it from the C object. I will rely on that Ruby object to keep it
alive. I know that this one is automatically marked
via gc_mark_no_pin, so I’m not going to mark this one because it will stay alive. It is a thing. So, what will happen in this case is the compactor
will run, will swap locations, everything will look just fine. The Ruby object, its references will get updated
correctly, but the C object, its references will not get updated. It will point to a free location or in a very,
very worse case scenario, the wrong object. And our program will actually, hopefully crash. We want it to crash in this situation. So maybe this isn’t too common, although I
have found it in a few places so far, one of them is the compilation process. When we convert text to an abstract syntax
tree, there is a location. I fixed it in instruction sequences and the
intermediate representation, and we will talk about that now. Essentially inside of our instruction sequences
we point at real Ruby objects. If you look at an instruction sequence, it’s
an array of bytes and it has a pointer to a literal. A string. It used to be we would keep this alive via
a mark array. They would be relied on to keep these things
alive and this pattern is almost the same as we saw in our problem object graph. So, I fixed this by essentially walking all
of the instruction sequences at mark time. What that means is that we’re able to eliminate
this mark array and directly mark these things. And as you can see, like, this is to demonstrate
this has been a multiyear yak shave, this was released in Ruby 2.6 and done in preparation
of implementing the compacter. During compilation it would break and I was
able to fix it in basically the same way. The intermediate representation had almost
the exact same pattern used there. Those are all in Ruby 2.7. So, that is great. All right. One more example. I found this in the JSON library as well. An interesting pattern about this problem
is that if these two objects are written in Ruby, we have pure Ruby implementations here,
if we are clever and by clever, I mean evil Ruby developers, we can actually cut that
reference, because it’s just Ruby code, right? And if we cut that reference, the garbage
collector will go ahead and collect that object and now we have a C extension that’s pointing
at nothing and the program will crash. So, let me give you an example. Here is a problem code here in the JSON library. We’re pointing at constants and the constants
are globals. I could make this program crash by doing this. Essentially what I would say is okay. Guess what? I am going to remove that constant here. The garbage collector would then go ahead
and collect that constant when we did a gc.start and then the program would crash. So that was cool. The fix is very simple. We just say we need to mark these things. I found the same thing in message pack. Since this reference was created in Ruby,
I was able to cut it, make the program crash, and then let’s see did I do the animation? Yes! Ahhhh! I love that. I’m trying to use all of the Keynote animations. All right. So my hypothesis with this, and I think that
most of you will agree with me is that pure Ruby programs shouldn’t crash. Right? [ Laughter ]
Yes! Yes! Clap for that. Yes! [ Applause ]
I know we all want to be on the Ruby core team, but our programs really shouldn’t crash. We should not get core files out of them. Since I was able to get the rest of the core
team to agree with this particular premise and because of that if I can make your program
crash using pure Ruby, it is a bug and we should fix it. What shakes out of this is a very, very simple
rule for C extension authors, and that is essentially if you hold a reference, you must
mark the reference. That’s it. If you hold a reference, you need to mark
a reference. Now let me give you another solution, another
solution to this problem is to just don’t write any C code. I think that that is the better solution. So, let’s go over one more challenge and check
out the results. I want to talk about object_id. This is an interesting problem. Generally direct memory access prevents movement
of objects, so let’s take a look at how object_id is implemented in Ruby 2.6 and older. Object_id in Ruby 2.6 and older is based on
location. Now, the question is if one of these objects moves,
what is the object_id now? Right? And I’m gonna talk about my initial solution
for this, and we’ll see a much better solution here in a couple minutes. Essentially, what is the object_id here? We would say we don’t know. We haven’t looked. We compact the heap. It moves. Check out the object_id. It is 1. No problem. What is it after we move? We will look here. Okay. Create an object. Look at its object_id, it’s 4. We compact the heap. Then we check the object_id again, question
is what should the object_id be here? Well, you would probably expect 4. I would think. So to fix this, my initial solution was to
keep track of that object_id and make sure it didn’t change. So, essentially I keep a global map. If you call object_id, we keep track of the
object_id that you saw. Okay? So essentially the solution is here, walking
through the code again, check the object_id, create a map, 4 maps to 4. We compact the heap. Now it’s at address 1. So, the address 1 maps to object_id 4 and
when you call again, you get the right number. Unfortunately, this introduces a problem,
and this is a thing that sucks about this algorithm. What happens here, let’s say we take a look
at this and move this and allocate a new object. This object y happens to be put into slot
4. And now what? What should the object_id be? In my initial implementation what I did is
said okay. We’re just going to count up. Just add one. If that one’s being used, go up again. Just keep going and see what happens. [ Laughter ]
And then store that. So, in this particular case our object_id
is gonna be 5. Let’s walk through these animations. Yay! I can’t wait to get to the better solution,
because this one is not good. So, here we have object_id 5. Unfortunately that means we have to clean
up the table when an object gets freed or the table would grow forever. So, in GC clean up, if the location was seen,
we have to remove it from the map. I call this mostly location based object IDs,
because it’s typically the address, but sometimes it won’t be. Fortunately, this is canceled. Well, it’s like 20% canceled. Actually, we refactored it to be better. That’s what we did. Now in master, I don’t know if this is actually
going to make it into 2.7, but in master we have a monotonic object ID and I worked on
this with my co worker, j Hawthorne. So, when you call object ID, we will give
you a number and when you call we will give you a bigger number and we will do it forever
and ever and ever. This is what it looks like if you build Ruby
today, you call object_id new. I gave an example here in the middle where
we allocate an object, But you will see each time it increments by 20. But that final one, the one in the middle
there, it didn’t get an object_id, and the reason I did that is to demonstrate that this
number is only calculated at the time that you call object_id. So, a cool feature about this is object_ids
are truly unique. Before since they were memory addresses, it
meant that we could reuse them. Today they are unique. We can also do weird things like count the
number of object ID calls. So, for example here, the other thing we can
do is I didn’t put a slide of counting object IDs, but I want to call something out that’s
weird. In Ruby 2.6 and older, if you call in spec,
it is actually the address. Those two numbers are related. Now in Ruby 2.7 and newer, there is absolutely
no relationship between those two numbers. And let’s cut this part out of the video,
but a question I have for you in the audience here is if that inspect is based on the address
and the object moves, what will the inspect look like? But please don’t file a bug about that. Thank you. Okay. So, the tl;dr is please don’t use object ID
unless you need to. Oh my God, I’m way out of time. This is a basic Rails application. I mapped the memory out. This is the first implementation, after compaction
and before. This is our Rails application at work. The top is before bottom is after. It’s way too big to fit on a slide so I slide
it all the way like this. This is the very first implementation when
I first got it working. And this first implementation, 3% of the objects
were pinned in the heap. I have since been able to improve it. I didn’t tell you this. So, red dots represent objects that are pinned. Black represents objects, white represents
free space. So the red dots, you can see from the red
how much of the heap is pinned. You can see we have a lot of it here. What’s weird is that a lot is only 3%. So, here is after some work on it before and
after. And you can hardly see any red now. So, you can see it’s shrunk down a bunch. That’s with 1% pinned. Right now, this is what it is today. Actually it may be even better than this. So, you can see there we have a much bigger
improvement. [ Applause ]
Yes, thank you! 3% pinned results in a 10% smaller heap. Future plans. I’m way over time. I need to do performance improvements. It’s not fast. It’s very inefficient. We do a full collection and then move the
objects and then update the references and then a full GC. I have eliminated the full GC. That is gone. We still do the three steps but we can reduce
down to two and it is theoretically possible to combine those two together into one. So, I want to do that as well. In Ruby 3.0, my plan is to add automatic compaction. Right now you need to call GC.compact. In the future, you do nothing. The difference between what we have today
in sliding, instead of using two fingers, we will slide them together like that. The reason is for better locality and it supports
variable width objects. And the final thing I want to do is implement
variable width allocation. And what that means, oh, thank you, yes. What that means is today we have fixed width
allocation that looks like this. Our heap looks like this and instead I would
like it to look like this where we can allocate things of any size that we want to. The way I want to end this talk today is we’re
constantly improving Ruby. The folks on the core team are doing their
best to improve the Ruby implementation that we have now. I personally believe that Ruby’s future is
very exciting. And I want to say thank you to all of you
for coming today. Thank you. [ Applause ]

Leave a Reply

Your email address will not be published. Required fields are marked *