Skip to content
100 changes: 99 additions & 1 deletion gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ rb_gc_guarded_ptr_val(volatile VALUE *ptr, VALUE val)
#endif

static const char *obj_type_name(VALUE obj);
static st_table *id2ref_tbl;
#include "gc/default/default.c"

#if USE_MODULAR_GC && !defined(HAVE_DLOPEN)
Expand Down Expand Up @@ -1242,6 +1243,104 @@ rb_gc_handle_weak_references(VALUE obj)
}
}

/*
* Returns true if the object requires a full rb_gc_obj_free() call during sweep,
* false if it can be freed quickly without calling destructors or cleanup.
*
* Objects that return false are:
* - Simple embedded objects without external allocations
* - Objects without finalizers
* - Objects without object IDs registered in id2ref
* - Objects without generic instance variables
*
* This is used by the GC sweep fast path to avoid function call overhead
* for the majority of simple objects.
*/
bool
rb_gc_obj_needs_cleanup_p(VALUE obj)
{
VALUE flags = RBASIC(obj)->flags;

if (flags & FL_FINALIZE) return true;

switch (flags & RUBY_T_MASK) {
case T_IMEMO:
switch (imemo_type(obj)) {
case imemo_constcache:
case imemo_cref:
case imemo_ifunc:
case imemo_memo:
case imemo_svar:
case imemo_throw_data:
return false;
default:
return true;
}

case T_DATA:
if (flags & RUBY_TYPED_FL_IS_TYPED_DATA) {
uintptr_t type = (uintptr_t)RTYPEDDATA(obj)->type;
if (type & TYPED_DATA_EMBEDDED) {
RUBY_DATA_FUNC dfree = ((const rb_data_type_t *)(type & TYPED_DATA_PTR_MASK))->function.dfree;
return (dfree == RUBY_NEVER_FREE || dfree == RUBY_TYPED_DEFAULT_FREE);
}
}
return true;

case T_OBJECT:
case T_STRING:
case T_ARRAY:
case T_HASH:
case T_BIGNUM:
case T_STRUCT:
case T_FLOAT:
case T_RATIONAL:
case T_COMPLEX:
break;

default:
return true;
}

shape_id_t shape_id = RBASIC_SHAPE_ID(obj);
if (id2ref_tbl && rb_shape_has_object_id(shape_id)) return true;

switch (flags & RUBY_T_MASK) {
case T_OBJECT:
if (flags & ROBJECT_HEAP) return true;
return false;

case T_STRING:
if (flags & (RSTRING_NOEMBED | RSTRING_FSTR)) return true;
return rb_shape_has_fields(shape_id);

case T_ARRAY:
if (!(flags & RARRAY_EMBED_FLAG)) return true;
return rb_shape_has_fields(shape_id);

case T_HASH:
if (flags & RHASH_ST_TABLE_FLAG) return true;
return rb_shape_has_fields(shape_id);

case T_BIGNUM:
if (!(flags & BIGNUM_EMBED_FLAG)) return true;
return false;

case T_STRUCT:
if (!(flags & RSTRUCT_EMBED_LEN_MASK)) return true;
if (flags & RSTRUCT_GEN_FIELDS) return rb_shape_has_fields(shape_id);
return false;

case T_FLOAT:
case T_RATIONAL:
case T_COMPLEX:
return rb_shape_has_fields(shape_id);

default:
UNREACHABLE_RETURN(true);
}
}

static void
io_fptr_finalize(void *fptr)
{
Expand Down Expand Up @@ -1831,7 +1930,6 @@ rb_gc_pointer_to_heap_p(VALUE obj)
#define OBJ_ID_INCREMENT (RUBY_IMMEDIATE_MASK + 1)
#define LAST_OBJECT_ID() (object_id_counter * OBJ_ID_INCREMENT)
static VALUE id2ref_value = 0;
static st_table *id2ref_tbl = NULL;

#if SIZEOF_SIZE_T == SIZEOF_LONG_LONG
static size_t object_id_counter = 1;
Expand Down
72 changes: 42 additions & 30 deletions gc/default/default.c
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,7 @@ heap_page_in_global_empty_pages_pool(rb_objspace_t *objspace, struct heap_page *
#define GET_HEAP_WB_UNPROTECTED_BITS(x) (&GET_HEAP_PAGE(x)->wb_unprotected_bits[0])
#define GET_HEAP_MARKING_BITS(x) (&GET_HEAP_PAGE(x)->marking_bits[0])


#define RVALUE_AGE_BITMAP_INDEX(n) (NUM_IN_PAGE(n) / (BITS_BITLENGTH / RVALUE_AGE_BIT_COUNT))
#define RVALUE_AGE_BITMAP_OFFSET(n) ((NUM_IN_PAGE(n) % (BITS_BITLENGTH / RVALUE_AGE_BIT_COUNT)) * RVALUE_AGE_BIT_COUNT)

Expand Down Expand Up @@ -3481,15 +3482,34 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit
rb_asan_unpoison_object(vp, false);
if (bitset & 1) {
switch (BUILTIN_TYPE(vp)) {
default: /* majority case */
gc_report(2, objspace, "page_sweep: free %p\n", (void *)p);
case T_MOVED:
if (objspace->flags.during_compacting) {
/* The sweep cursor shouldn't have made it to any
* T_MOVED slots while the compact flag is enabled.
* The sweep cursor and compact cursor move in
* opposite directions, and when they meet references will
* get updated and "during_compacting" should get disabled */
rb_bug("T_MOVED shouldn't be seen until compaction is finished");
}
gc_report(3, objspace, "page_sweep: %s is added to freelist\n", rb_obj_info(vp));
ctx->empty_slots++;
RVALUE_AGE_SET_BITMAP(vp, 0);
heap_page_add_freeobj(objspace, sweep_page, vp);
break;
case T_ZOMBIE:
/* already counted */
break;
case T_NONE:
ctx->empty_slots++; /* already freed */
break;

default:
#if RGENGC_CHECK_MODE
if (!is_full_marking(objspace)) {
if (RVALUE_OLD_P(objspace, vp)) rb_bug("page_sweep: %p - old while minor GC.", (void *)p);
if (RVALUE_REMEMBERED(objspace, vp)) rb_bug("page_sweep: %p - remembered.", (void *)p);
}
#endif

if (RVALUE_WB_UNPROTECTED(objspace, vp)) CLEAR_IN_BITMAP(GET_HEAP_WB_UNPROTECTED_BITS(vp), vp);

#if RGENGC_CHECK_MODE
Expand All @@ -3501,42 +3521,34 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit
#undef CHECK
#endif

rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ);
if (!rb_gc_obj_needs_cleanup_p(vp)) {
if (RB_UNLIKELY(objspace->hook_events & RUBY_INTERNAL_EVENT_FREEOBJ)) {
rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ);
}

rb_gc_obj_free_vm_weak_references(vp);
if (rb_gc_obj_free(objspace, vp)) {
// always add free slots back to the swept pages freelist,
// so that if we're compacting, we can re-use the slots
(void)VALGRIND_MAKE_MEM_UNDEFINED((void*)p, BASE_SLOT_SIZE);
RVALUE_AGE_SET_BITMAP(vp, 0);
heap_page_add_freeobj(objspace, sweep_page, vp);
gc_report(3, objspace, "page_sweep: %s is added to freelist\n", rb_obj_info(vp));
gc_report(3, objspace, "page_sweep: %s (fast path) added to freelist\n", rb_obj_info(vp));
ctx->freed_slots++;
}
else {
ctx->final_slots++;
}
break;
gc_report(2, objspace, "page_sweep: free %p\n", (void *)p);

case T_MOVED:
if (objspace->flags.during_compacting) {
/* The sweep cursor shouldn't have made it to any
* T_MOVED slots while the compact flag is enabled.
* The sweep cursor and compact cursor move in
* opposite directions, and when they meet references will
* get updated and "during_compacting" should get disabled */
rb_bug("T_MOVED shouldn't be seen until compaction is finished");
rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ);

rb_gc_obj_free_vm_weak_references(vp);
if (rb_gc_obj_free(objspace, vp)) {
(void)VALGRIND_MAKE_MEM_UNDEFINED((void*)p, BASE_SLOT_SIZE);
RVALUE_AGE_SET_BITMAP(vp, 0);
heap_page_add_freeobj(objspace, sweep_page, vp);
gc_report(3, objspace, "page_sweep: %s is added to freelist\n", rb_obj_info(vp));
ctx->freed_slots++;
}
else {
ctx->final_slots++;
}
}
gc_report(3, objspace, "page_sweep: %s is added to freelist\n", rb_obj_info(vp));
ctx->empty_slots++;
RVALUE_AGE_SET_BITMAP(vp, 0);
heap_page_add_freeobj(objspace, sweep_page, vp);
break;
case T_ZOMBIE:
/* already counted */
break;
case T_NONE:
ctx->empty_slots++; /* already freed */
break;
}
}
Expand Down
1 change: 1 addition & 0 deletions gc/gc.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ MODULAR_GC_FN void rb_gc_after_updating_jit_code(void);
MODULAR_GC_FN bool rb_gc_obj_shareable_p(VALUE);
MODULAR_GC_FN void rb_gc_rp(VALUE);
MODULAR_GC_FN void rb_gc_handle_weak_references(VALUE obj);
MODULAR_GC_FN bool rb_gc_obj_needs_cleanup_p(VALUE obj);

#if USE_MODULAR_GC
MODULAR_GC_FN bool rb_gc_event_hook_required_p(rb_event_flag_t event);
Expand Down
13 changes: 7 additions & 6 deletions lib/prism/lex_compat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -659,13 +659,14 @@ def result
IgnoreStateToken.new([[lineno, column], event, value, lex_state])
when :on_words_sep
# Ripper emits one token each per line.
lines = value.lines
lines[0...-1].each do |whitespace|
tokens << Token.new([[lineno, column], event, whitespace, lex_state])
lineno += 1
column = 0
value.each_line.with_index do |line, index|
if index > 0
lineno += 1
column = 0
end
tokens << Token.new([[lineno, column], event, line, lex_state])
end
Token.new([[lineno, column], event, lines.last, lex_state])
tokens.pop
when :on_regexp_end
# On regex end, Ripper scans and then sets end state, so the ripper
# lexed output is begin, when it should be end. prism sets lex state
Expand Down
12 changes: 12 additions & 0 deletions test/ruby/test_thread.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1652,4 +1652,16 @@ def test_mutexes_locked_in_fiber_dont_have_aba_issue_with_new_fibers
end
end;
end

# [Bug #21836]
def test_mn_threads_sub_millisecond_sleep
assert_separately([{'RUBY_MN_THREADS' => '1'}], "#{<<~"begin;"}\n#{<<~'end;'}", timeout: 30)
begin;
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1000.times { sleep 0.0001 }
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
elapsed = t1 - t0
assert_operator elapsed, :>=, 0.1, "sub-millisecond sleeps should not return immediately"
end;
end
end
10 changes: 1 addition & 9 deletions thread_pthread.c
Original file line number Diff line number Diff line change
Expand Up @@ -2947,15 +2947,7 @@ timer_thread_check_signal(rb_vm_t *vm)
static bool
timer_thread_check_exceed(rb_hrtime_t abs, rb_hrtime_t now)
{
if (abs < now) {
return true;
}
else if (abs - now < RB_HRTIME_PER_MSEC) {
return true; // too short time
}
else {
return false;
}
return abs <= now;
}

static rb_thread_t *
Expand Down