From a23ae56c4d24ade9438393fc47d6f2b730e3f7a8 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Mon, 3 Apr 2023 13:43:31 -0400 Subject: [PATCH] Add REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO [Feature #19571] This commit adds the environment variable `RUBY_GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO` which is used to calculate the `remembered_wb_unprotected_objects_limit` using a ratio of `old_objects`. This should improve performance by reducing major GC because, in a major GC, we mark all of the old objects, so we should have more uncollectible WB unprotected objects before starting a major GC. The default has been set to 0.01 (1% of old objects). On one of [Shopify's highest traffic Ruby apps, Storefront Renderer](https://shopify.engineering/how-shopify-reduced-storefront-response-times-rewrite), we saw significant improvements after deploying this patch in production. In the graphs below, we have the `tuned` group which uses `RUBY_GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO=0.01` (the default value), and an `untuned` group, which turns this feature off with `RUBY_GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO=0`. We see that the tuned group spends significantly less time in GC, on average 0.67x of the time compared to the untuned group and 0.49x for p99. We see this improvement in GC time translate to improvements in response times. The average response time is now 0.96x of the time compared to the untuned group and 0.86x for p99. https://user-images.githubusercontent.com/15860699/229559078-e23e8ce4-5f1f-4a2f-b5ef-5769f92b8c70.png --- gc.c | 11 ++++++++++- test/ruby/test_gc.rb | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/gc.c b/gc.c index bd34b9feb8..38cc1d463f 100644 --- a/gc.c +++ b/gc.c @@ -291,6 +291,9 @@ rb_gc_guarded_ptr_val(volatile VALUE *ptr, VALUE val) #ifndef GC_HEAP_GROWTH_MAX_SLOTS #define GC_HEAP_GROWTH_MAX_SLOTS 0 /* 0 is disable */ #endif +#ifndef GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO +# define GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO 0.01 +#endif #ifndef GC_HEAP_OLDOBJECT_LIMIT_FACTOR #define GC_HEAP_OLDOBJECT_LIMIT_FACTOR 2.0 #endif @@ -347,6 +350,7 @@ typedef struct { double heap_free_slots_min_ratio; double heap_free_slots_goal_ratio; double heap_free_slots_max_ratio; + double uncollectible_wb_unprotected_objects_limit_ratio; double oldobject_limit_factor; size_t malloc_limit_min; @@ -369,6 +373,7 @@ static ruby_gc_params_t gc_params = { GC_HEAP_FREE_SLOTS_MIN_RATIO, GC_HEAP_FREE_SLOTS_GOAL_RATIO, GC_HEAP_FREE_SLOTS_MAX_RATIO, + GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO, GC_HEAP_OLDOBJECT_LIMIT_FACTOR, GC_MALLOC_LIMIT_MIN, @@ -8346,7 +8351,10 @@ gc_marks_finish(rb_objspace_t *objspace) if (full_marking) { /* See the comment about RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR */ const double r = gc_params.oldobject_limit_factor; - objspace->rgengc.uncollectible_wb_unprotected_objects_limit = (size_t)(objspace->rgengc.uncollectible_wb_unprotected_objects * r); + objspace->rgengc.uncollectible_wb_unprotected_objects_limit = MAX( + (size_t)(objspace->rgengc.uncollectible_wb_unprotected_objects * r), + (size_t)(objspace->rgengc.old_objects * gc_params.uncollectible_wb_unprotected_objects_limit_ratio) + ); objspace->rgengc.old_objects_limit = (size_t)(objspace->rgengc.old_objects * r); } @@ -11749,6 +11757,7 @@ ruby_gc_set_params(void) get_envparam_double("RUBY_GC_HEAP_FREE_SLOTS_GOAL_RATIO", &gc_params.heap_free_slots_goal_ratio, gc_params.heap_free_slots_min_ratio, gc_params.heap_free_slots_max_ratio, TRUE); get_envparam_double("RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR", &gc_params.oldobject_limit_factor, 0.0, 0.0, TRUE); + get_envparam_double("RUBY_GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO", &gc_params.uncollectible_wb_unprotected_objects_limit_ratio, 0.0, 0.0, TRUE); if (get_envparam_size("RUBY_GC_MALLOC_LIMIT", &gc_params.malloc_limit_min, 0)) { malloc_limit = gc_params.malloc_limit_min; diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb index 4f1d32580a..3ff2d8bff2 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -383,6 +383,15 @@ class TestGc < Test::Unit::TestCase assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_OLDMALLOC_LIMIT_MAX=16000000/, "") assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR=2.0/, "") end + + ["0.01", "0.1", "1.0"].each do |i| + env = {"RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR" => "0", "RUBY_GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO" => i} + assert_separately([env, "-W0"], __FILE__, __LINE__, <<~RUBY) + GC.disable + GC.start + assert_equal((GC.stat[:old_objects] * #{i}).to_i, GC.stat[:remembered_wb_unprotected_objects_limit]) + RUBY + end end def test_profiler_enabled