Know What Triggers GC

As you now know, GC must control both the object allocation in the heap space and the object memory allocation outside the Ruby heap. Consequently, two events will trigger GC:

  • There are not enough free slots in the heap space.

  • The current memory allocation (malloc) limit has been exceeded.

So any object creation or memory allocation can invoke GC. Let’s see when that happens, and then talk about how we can reduce the number of GC runs.

GC Triggered by Heap Usage

When Ruby runs out of slots, it executes GC to free up some memory. If GC can’t free enough slots, Ruby increases the heap space as described earlier.

Ruby defines enough slots as either 20% of all allocated slots, or GC_HEAP_FREE_SLOTS (FREE_MIN in Ruby 2.0 and earlier), whichever is greater.

GC_HEAP_FREE_SLOTS/FREE_MIN is by default 4,096 slots. In practice, this value is too low, and gets used only once when Ruby increases the heap space for the first time. Because the initial number of slots as defined by GC_HEAP_INIT_SLOTS is 10,000, the enough slots rule is actually free 40% of slots the first time and 20% afterward.

Let’s see how that works in irb. For simplicity, we’ll use Ruby 1.9. Later versions behave the same, but they won’t let us observe the effect easily because of the more complicated memory management.

 
$ ​rbenv shell 1.9.3-p551
 
$ ​irb
 
irb(main):001:0> GC.start
 
=> nil
 
irb(main):002:0> GC.stat
 
=> {:count=>7, :heap_used=>77, :heap_length=>77, :heap_increment=>0,
 
:heap_live_num=>11939, :heap_free_num=>19365, :heap_final_num=>0}

During startup irb called GC six times and allocated 77 heaps. We called it one more time to ensure that heap_free_num will correctly estimate the number of free slots on the heap. This number is about 19,000.

If we allocate fewer objects than free slots we won’t see GC:

 
irb(main):003:0> x = Array.new(15000) { Object.new }; nil
 
=> nil
 
irb(main):004:0> GC.stat.select { |k,v| [:count, :heap_used].include?(k) }
 
=> {:count=>7, :heap_used=>77}

As we’ve predicted, there was enough free space on the heap. Let’s now unset the variable x to make all 15,000 objects in the array garbage, and allocate 15,000 objects again:

 
irb(main):005:0> x = nil
 
=> nil
 
irb(main):006:0> GC.stat.select { |k,v| [:count, :heap_used].include?(k) }
 
=> {:count=>7, :heap_used=>77}
 
irb(main):007:0> y = Array.new(15000) { Object.new }; nil
 
=> nil
 
irb(main):008:0> GC.stat.select { |k,v| [:count, :heap_used].include?(k) }
 
=> {:count=>8, :heap_used=>77}

This time when allocating our second array y, we didn’t have enough free slots on the heap. So GC ran. However, it managed to reclaim enough free space. It freed all 15,000 objects from the array x. The heap space size at that time was 77 pages, approximately 31,000 slots on a 64-bit computer. So GC reclaimed about 50% of free space, more than the 20% threshold. This is why the heap space didn’t grow and heap_used stayed the same.

Now if we allocate another 15,000 objects, GC will not be able to free enough space and the heap will grow:

 
irb(main):009:0> z = Array.new(15000) { Object.new }; nil
 
=> nil
 
irb(main):010:0> GC.stat.select { |k,v| [:count, :heap_used].include?(k) }
 
=> {:count=>9, :heap_used=>138}

This is exactly what happens. We see one more GC run, and the heap becomes 1.8 times bigger.

GC Triggered by Malloc Limit

Ruby objects can allocate extra memory outside of the Ruby heap space, and Ruby GC has little control over that memory. The only thing it can do to ensure Ruby objects don’t use too much memory is to free as many unused objects as possible and hope that finalizing them reduces extra memory consumption.

That’s why Ruby triggers GC by a memory limit. When we allocate more than the current limit, GC is forced regardless of how many free heap slots we have.

In Ruby 2.0 and earlier that limit is defined by a GC_MALLOC_LIMIT constant. Its default value is 8 million bytes, about 7.63 MB.

This means that every time we allocate additional 7.63 MB, GC will run. And this limit is too small.

What if our app is receiving 100 MB of data from the network in 10 MB batches? Get ready for at least 10 extra GC runs. Let’s take a look:

 
$ ​rbenv shell 1.9.3-p551
 
$ ​irb
 
irb(main):001:0> data = ​"x"​*1024*1024*10; nil
 
=> nil
 
irb(main):002:0> ​# store in array to keep data from garbage collection
 
irb(main):003:0* buffers = []
 
=> []
 
irb(main):004:0> GC.start
 
=> nil
 
irb(main):005:0> GC.stat[:count]
 
=> 9
 
irb(main):006:0> 10.times ​do​ |i|
 
irb(main):007:1* buffers[i] = data.dup
 
irb(main):008:1> ​# actually force Ruby to copy data in the memory
 
irb(main):009:1* buffers[i][0] = ​'a'
 
irb(main):010:1> ​end​; nil
 
=> nil
 
irb(main):011:0> GC.stat[:count]
 
=> 21

For me Ruby ran GC 12 times, because every time I allocated more than the 7.63 MB limit, and some allocations actually exceeded the limit twice. You might see a slightly different number. But in any case it won’t be far off from 10.

These days 7.63 MB is nothing. Your program will easily exceed this limit by doing trivial operations with data. So if you’re using Ruby 2.0 or earlier, the malloc limit is the first parameter you’d want to tweak.

Admittedly, Ruby tries to adjust this limit at runtime. It takes the excess over the limit, adjusts it by a percentage of free space in the Ruby heap, and adds it to the current malloc limit.

For example, if our app uses 40% of the Ruby heap and allocates 10 MB, 2.37 MB over the limit, the malloc limit will be increased by 2.37 * 0.5 = 0.948 MB. And if it plans to continue allocating the 10 MB like our example did, the subsequent limit increases will be smaller and smaller. In any case, the limit will never exceed 10 MB with this algorithm.

So this malloc limit adaptation is not good enough for our example. In practice, I’ve rarely found it adequate, and tweaking it is a must.

Ruby 2.1 and later work slightly better. Just repeat the same irb session with the latest version. You’ll see something like this:

 
$ ​rbenv shell 2.2.0
 
$ ​irb
 
irb(main):001:0> data = ​"x"​*1024*1024*10; nil
 
=> nil
 
irb(main):002:0> ​# store in array to keep data from garbage collection
 
irb(main):003:0* buffers = []
 
=> []
 
irb(main):004:0> GC.start
 
=> nil
 
irb(main):005:0> GC.stat[:count]
 
=> 8
 
irb(main):006:0> 10.times ​do​ |i|
 
irb(main):007:1* buffers[i] = data.dup
 
irb(main):008:1> ​# actually force Ruby to copy data in the memory
 
irb(main):009:1* buffers[i][0] = ​'a'
 
irb(main):010:1> ​end​; nil
 
=> nil
 
irb(main):011:0> GC.stat[:count]
 
=> 11

Having only 3 extra garbage collections is better than 11, isn’t it? So let’s see what the newest Ruby does better.

Ruby 2.1 introduced RGenGC—restricted generational GC. Ruby 2.2 adds RIncGC—incremental GC built on top of generational GC.

Here we’ll talk about RGenGC and RIncGC only enough to understand their impact on performance. To learn more, read Aman Gupta’s blog post[39] and watch Koichi Sasada’s presentations.[40]

So how do RGenGC and RIncGC improve performance?

Generational GC divides all Ruby objects into two groups: a new generation and the old generation. An object becomes old when it survives at least one GC. Malloc limits for these generations are different: GC_MALLOC_LIMIT_MIN for the new generation and GC_OLDMALLOC_LIMIT_MIN for the old generation. Initial default values are the same, though: 16 MB.

The minimum 16 MB malloc limit is already a nice improvement over older Ruby. But even better is that it is allowed to grow up until GC_MALLOC_LIMIT_MAX (32 MB by default) for the new generation, and GC_OLDMALLOC_LIMIT_MAX (128 MB by default) for the old generation.

It is a good thing that the old generation’s limit is larger because long-lived objects tend to be the ones to use more memory.

The malloc limit’s growth factor no longer depends on the Ruby heap usage. Instead, it’s fixed at GC_MALLOC_LIMIT_GROWTH_FACTOR (1.4 by default) for the new generation, and at GC_OLDMALLOC_LIMIT_GROWTH_FACTOR (1.2 by default) for the old generation. This way, Ruby is able to better adjust the GC settings when your program keeps allocating memory.

The growth factor for the new generation is larger. That allows it to quickly allocate memory without hitting GC. The growth factor for the old generation is smaller, but its malloc limit maximum is much larger. That allows the old generation to consume larger amounts of memory without the need for GC.

In practice, the new generations’ malloc limit grows even faster because it applies not to the previous limit, but to the amount of memory your application has allocated since the last GC. That number is always bigger. So, for example, if our current limit is 16 MB and we’re trying to allocate another 20 MB, then our next limit is 20 * 1.4 = 28 MB.

If our program doesn’t allocate memory over the current limit anymore, Ruby gradually reduces it, decreasing by 0.98 times every time GC runs until the limit reaches GC_MALLOC_LIMIT_MIN or GC_OLDMALLOC_LIMIT_MIN.

Now we should be able to explain why our example triggers GC only three times.

We allocate 10 MB at a time, so only the second allocation will definitely exceed the 16 MB new generation malloc limit and trigger GC. At that time we’ll be allocating 20 MB of memory since the last GC. So our next malloc limit will be 20 * 1.4 = 28 MB.

The fifth allocation will exceed the new limit, maxing it out at 32 MB because of the GC_MALLOC_LIMIT_MAX cap. Finally, the ninth allocation will be the last to exceed the limit and trigger GC. In total, this gives us the three GC runs that we saw when we ran our example.

GC#stat in Ruby 2.1 and later give us enough information to see for ourselves how well this theory corresponds to the practice.

Here are the malloc limit--related parameters:

malloc_limit (Ruby 2.1) or malloc_increase_bytes_limit (Ruby 2.2)

Current malloc limit for the new generation.

malloc_increase (Ruby 2.1) or malloc_increase_bytes (Ruby 2.2)

The amount of memory allocated by the new generation since the last GC.

oldmalloc_limit (Ruby 2.1) or oldmalloc_increase_bytes_limit (Ruby 2.2)

Current malloc limit for the old generation.

oldmalloc_increase (Ruby 2.1) or oldmalloc_increase_bytes (Ruby 2.2)

The amount of memory allocated by the new generation since the last GC.

So let’s see at GC#stat the output in between allocations.

 
$ ​rbenv shell 2.2.0
 
$ ​irb

As before, we’ll allocate the buffer and force GC to reset all malloc parameters for predictability:

 
irb(main):001:0> data = ​"x"​*1024*1024*10; nil
 
=> nil
 
irb(main):002:0> buffers = []
 
=> []
 
irb(main):003:0> GC.start
 
=> nil
 
irb(main):004:0> [GC.stat[:count], GC.stat[:malloc_increase_bytes],
 
GC.stat[:malloc_increase_bytes_limit]]
 
=> [8, 22816, 16777216]

Our theory predicted that we’ll see the GC after the second allocation:

 
irb(main):005:0> buffers[0] = data.dup; buffers[0][0] = ​'a'​; nil
 
=> nil
 
irb(main):006:0> [GC.stat[:count], GC.stat[:malloc_increase_bytes],
 
GC.stat[:malloc_increase_bytes_limit]]
 
=> [8, 10543000, 16777216]
 
irb(main):007:0> buffers[1] = data.dup; buffers[1][0] = ​'a'​; nil
 
=> nil
 
irb(main):008:0> [GC.stat[:count], GC.stat[:malloc_increase_bytes],
 
GC.stat[:malloc_increase_bytes_limit]]
 
=> [9, 17032, 29464791]

Yes, that’s exactly what we see. And the new malloc limit is 28 MB as expected. Let’s continue with allocations:

 
irb(main):009:0> buffers[2] = data.dup; buffers[2][0] = ​'a'​; nil
 
=> nil
 
irb(main):010:0> [GC.stat[:count], GC.stat[:malloc_increase_bytes],
 
GC.stat[:malloc_increase_bytes_limit]]
 
=> [9, 10537568, 29464791]
 
irb(main):011:0> buffers[3] = data.dup; buffers[3][0] = ​'a'​; nil
 
=> nil
 
irb(main):012:0> [GC.stat[:count], GC.stat[:malloc_increase_bytes],
 
GC.stat[:malloc_increase_bytes_limit]]
 
=> [9, 21057392, 29464791]
 
irb(main):013:0> buffers[4] = data.dup; buffers[4][0] = ​'a'​; nil
 
=> nil
 
irb(main):014:0> [GC.stat[:count], GC.stat[:malloc_increase_bytes],
 
GC.stat[:malloc_increase_bytes_limit]]
 
=> [10, 7616, 33554432]

Everything goes as expected so far: another GC after the fifth allocation, and the malloc limit is at its maximum.

 
irb(main):015:0> buffers[5] = data.dup; buffers[5][0] = ​'a'​; nil
 
=> nil
 
irb(main):016:0> [GC.stat[:count], GC.stat[:malloc_increase_bytes],
 
GC.stat[:malloc_increase_bytes_limit]]
 
=> [10, 10488960, 33554432]
 
irb(main):017:0> buffers[6] = data.dup; buffers[6][0] = ​'a'​; nil
 
=> nil
 
irb(main):018:0> [GC.stat[:count], GC.stat[:malloc_increase_bytes],
 
GC.stat[:malloc_increase_bytes_limit]]
 
=> [10, 21008104, 33554432]
 
irb(main):019:0> buffers[7] = data.dup; buffers[7][0] = ​'a'​; nil
 
=> nil
 
irb(main):020:0> [GC.stat[:count], GC.stat[:malloc_increase_bytes],
 
GC.stat[:malloc_increase_bytes_limit]]
 
=> [10, 31528976, 33554432]
 
irb(main):021:0> buffers[8] = data.dup; buffers[8][0] = ​'a'​; nil
 
=> nil
 
irb(main):022:0> [GC.stat[:count], GC.stat[:malloc_increase_bytes],
 
GC.stat[:malloc_increase_bytes_limit]]
 
=> [11, 16984, 33554432]
 
irb(main):023:0> buffers[9] = data.dup; buffers[9][0] = ​'a'​; nil
 
=> nil
 
irb(main):024:0> [GC.stat[:count], GC.stat[:malloc_increase_bytes],
 
GC.stat[:malloc_increase_bytes_limit]]
 
=> [11, 10536160, 33554432]

As you see, our theory matches perfectly with the practice. The ninth allocation did the third GC run.

Now you know everything about what triggers GC. Armed with this knowledge, you can not only predict how often your own program will hit GC, but optimize it to reduce the number of collections.

Unsurprisingly, the best thing you can do to minimize GC is to upgrade to the most recent Ruby. It needs less GC, and that by itself is a significant optimization. But I’ll tell you more. Each individual GC run itself is much faster in Ruby 2.1 and later. Let’s see why.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.234.150