Skip to content

Instantly share code, notes, and snippets.

@jjb
Last active September 13, 2021 17:46
Show Gist options
  • Save jjb/d68ac74845ae77d5f910005c82d4f8dd to your computer and use it in GitHub Desktop.
Save jjb/d68ac74845ae77d5f910005c82d4f8dd to your computer and use it in GitHub Desktop.
Trying to figure out performance impact of RUBY_GC_HEAP_GROWTH_FACTOR

background

There are many options available for tuning ruby memory management: https://github.com/ruby/ruby/blob/trunk/gc.c#L7420-L7444

The one that gets the most attention is RUBY_GC_HEAP_GROWTH_FACTOR, which is the only one Heroku suggests adjusting: https://devcenter.heroku.com/articles/ruby-memory-use#gc-tuning

I've often wondered what the drawback was for setting RUBY_GC_HEAP_GROWTH_FACTOR quite low for a webapp. Wouldn't this only marginally affect performance while the app was initially booting up, and then therafter provide the most optimal memory size? So I set out to benchmark the behavior of different settings.

Assumption: since I'm benchmarking the time the memory manager takes to stop execution and allocate more memory, the code used for the benchmark can be anything which increases its memory usage as it runs. So diversity or realism is not needed.

benchmark code

  • ruby 2.3.1p112
  • macOS 10.11.6
  • MacBook Pro (Retina, 15-inch, Mid 2014)
  • 2.8 GHz Intel Core i7
puts ENV["RUBY_GC_HEAP_GROWTH_FACTOR"]

s = []
start = Time.now
25_000_000.times do |i|
  print "[#{i/1_000_000}]" if 0 == i % 1_000_000
  s << "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef"  
end
finish = Time.now
puts

puts 'GC.stat[:heap_sorted_length]: ' + GC.stat[:heap_sorted_length].to_s
puts "seconds elapsed: " + (finish-start).to_s

sleep 100000000

results

➔ export RUBY_GC_HEAP_GROWTH_FACTOR=  #default. i don't know what that is
➔ ruby code.rb                      

[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24]
GC.stat[:heap_sorted_length]: 82445
seconds elapsed: 6.431182
# memory, real memory, private memory: 1.78, 0.7, 0.6
➔ export RUBY_GC_HEAP_GROWTH_FACTOR=1.01
➔ ruby code.rb                          
1.01
[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24]
GC.stat[:heap_sorted_length]: 61783
seconds elapsed: 51.569609
# memory, real memory, private memory: 1.80, 1.51, 1.37
➔ export RUBY_GC_HEAP_GROWTH_FACTOR=1.1 
➔ ruby code.rb                         
1.1
[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24]
GC.stat[:heap_sorted_length]: 67507
seconds elapsed: 9.097858
# memory, real memory, private memory: 1.80, 1.80, 1.67
➔ export RUBY_GC_HEAP_GROWTH_FACTOR=1.5
➔ ruby code.rb                         
1.5
[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24]
GC.stat[:heap_sorted_length]: 92055
seconds elapsed: 6.168111
# memory, real memory, private memory: 1.81, 1.81. 1.68
➔ export RUBY_GC_HEAP_GROWTH_FACTOR=2  
➔ ruby code.rb                       
2
[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24]
GC.stat[:heap_sorted_length]: 93538
seconds elapsed: 5.383548
# memory, real memory, private memory: 1.80, 1.80, 1.70

Conclusions and questions

  • number of slots allocated seems to correspond with what one would expect from increasing growth factor
  • other than in the 1.01 case, time taken to stop code and allocate more memory is not significant, especially in the case of a web app booting up
  • i don't know what the difference is between memory, real memory, and private memory (as reported by Activity Monitor). when i was a kid, we just had resident memory and virtual memory, and WE WERE HAPPY
  • the default case is a wild outlier. If this were ruby 2.4 I would suspect it was using the RUBY_GC_HEAP_FREE_SLOTS_GOAL_RATIO algorithm (https://github.com/ruby/ruby/blob/trunk/gc.c#L7433-L7437). So I haven't the slightest guess what might be going on here.

¯\_(ツ)_/¯

@jjb
Copy link
Author

jjb commented Dec 23, 2016

@jjb
Copy link
Author

jjb commented Jun 6, 2018

ruby 2.5.1

default
[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24]
GC.stat[:heap_sorted_length]: 95718
seconds elapsed: 5.760007
real 1.5G
virtual 6.3gb
private 1.36gb

1.01
[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24]
GC.stat[:heap_sorted_length]: 61736
seconds elapsed: 52.814111
1.59, 6.36, 1.44

1.1
[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24]
GC.stat[:heap_sorted_length]: 67977
seconds elapsed: 11.155236
1.54, 6.34, 1.48

1.5
[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24]
GC.stat[:heap_sorted_length]: 85464
seconds elapsed: 8.176758
1.57, 6.40, 1.47

➔ ruby code.rb                       
2
[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24]
GC.stat[:heap_sorted_length]: 95772
seconds elapsed: 5.824807
1.5, 6.38, 1.36

@jjb
Copy link
Author

jjb commented Jun 6, 2018

experimenting with GC.start and MALLOC_ARENA_MAX (not sure if that's actually respected in my dev environment) showed no significant differences.

@jjb
Copy link
Author

jjb commented Sep 13, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment