Playin' with the Ruby GC
So I, like many of you geeks out there heard about the changes to Ruby in 1.9.3-rc1. Upon hearing the news, I hurriedly scanned the intra-webz to see if there were any cool features that would affect me or my production apps. Some of the changes in the release candidate were pleasantly surprising, especially file loading (which saw a notable ~38% speed boost). When I saw the garbage collector was being changed to something called "Lazy Sweeping" I had to figure out how much this was going to impact performance. I mean c'mon "lazy sweeping", sounds so interesting right? So I started writing a script to tell me just that.
Let's start this in reverse order.
The results
I found that the the new "lazy sweeping" (still super cool name) was about 8% faster on average. Here's the pretty graph I created from Excel.

The vertical axis is the time in milliseconds, and the horizontal axis represents the GC cycle during the creation of ~10,000 objects. Alright alright alright. I'll explain the methodology so you can actually understand the graph.
The Methodology
While doing the research I came across a nifty little script by the Narihiro Nakamura here. I modified it slightly, but kept the same idea of creating a number of objects to fill up the heap.
# './gc_test.rb'
@m = 1000000
@b = 2 * 5 * (4**3) + 1
@a = 100001
def make_fragmentation(h, seed)
i = seed
10000.times {|m| h << Object.new}
10000.times do |m|
i = ((@a * i) + @b) % @m
h[i % h.length] = nil
end
end
def run_test
GC::Profiler.enable
heaps = []
100.times{|i| make_fragmentation(heaps, i) }
GC::Profiler.result
end
puts run_test
This returns a result set from the GC profiler. Which I call about a hundred times like so:
require 'bigdecimal'
require 'pp'
output = Hash.new([])
@totals = {}
path = './gc_test.rb'
100.times do |test_run|
result_set = `ruby #{path}` # this is where we get the result from the previous script
result_set.split("\n").each_with_index do |result, index|
next if [0,1].include?(index)
result = result.split(" ")
output[result[0]] += [result[5]]
end
end
# This is where we take the cumulative values of the previous 100 results sets by GC cycle
# and grab the average. Big decimal because we want to be precise and the numbers are already
# strings.
output.each do |k,v|
length = BigDecimal.new(v.length.to_s)
nv = v.map! {|i| BigDecimal.new(i)}
@totals[k] = (nv.inject(:+) / length).to_s("F")
end
puts @totals.map{|k,v| v + "\n"}
I attempted to run this ten thousand times but it just took too long. I wanted to get a large sample because the GC can run at off times and isn't at all guaranteed to be consistent. I ended up just going with a 100 run set for sake of speed. Once I had the script all set I just used RVM to set my ruby and ran it. As of writing this article RVM didn't offer 1.9.3-rc1 so I used 1.9.3-preview1 instead.
The interpretation
Well as I said before "lazy sweeping" is about 8% faster on average. The largest increases in speed was when there were a small/medium number of objects in memory, the larger the number of objects the more 1.9.2 won out over 1.9.3. InfoQ talked to Narihiro Nakamura [2] where he explains why these results are likely accurate.
If the program creates many long-lived objects, lazy sweep may not be able to find a free object. In that case, lazy sweep spends a long time on a single object allocation. I think that in most cases, the performance of this should still be better than M&S GC.
This InfoQ article was especially enlightening so I recommend giving it a glance through (it's in the footnote).
Try it yourself
The GC module offers many ways to examine the garbage collector. If you want something more, then you should look into ruby-prof which has options that show you GCTIME, GCRUNS and a whole host of other useful profiling tools. @wycats recently pushed a branch to ruby-prof that enables those GC operations, you can find it here. You'll note that it requires ruby to be patched. So try ruby-prof out!
Anyways, I know this wasn't exactly scientific but it was certainly fun and helped me understand some things about ruby a little better. Hope it helps you too.
Have an improvement to the scripts above? Think I'm doing something completely wrong? Maybe you have an entirely different approach. Let me know in the comments below, and don't forget to subscribe to 'Run With It'.
