From 817e940d8c55bf11ef661bb9732a39b78b7fc11e Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Mon, 5 Oct 2026 13:37:09 -0400 Subject: [PATCH] Change ISEQ_BODY(iseq)->location.pathobj to be a String instead of Array Any iseq created inside an eval, class_eval or module_eval had its `location.pathobj` set to an Array of 2 elements where the 2nd element was `nil`. This is wasteful in terms of memory and also causes longer major GC times. This was true for the eval forms even when `__FILE__` was given directly. Example script: ``` class Foo; end Foo.class_eval("def hi = 'hi'") ObjectSpace.each_object(Array) { |obj| p obj if obj.frozen? && obj.size == 2 && obj[0].to_s.start_with?("(eval at") } ``` output before: ``` ["(eval at ../ruby/test.rb:47)", nil] ["(eval at ../ruby/test.rb:47)", nil] ``` output after: empty The changes to the 2 structs do not alter the struct sizes (they make use of padding that was already there). This also saves ~1000 Arrays at startup, some from the builtin_prelude: arrays like `["", nil]` per iseq created in those files. Around half of those survive GC and become long-lived Arrays. --- compile.c | 8 ++++++-- iseq.c | 19 ++++++++++--------- proc.c | 5 ++++- vm.c | 3 ++- vm_core.h | 14 ++++++++++---- 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/compile.c b/compile.c index 1404cf68523..b9fe435097a 100644 --- a/compile.c +++ b/compile.c @@ -12770,7 +12770,7 @@ typedef uint32_t ibf_offset_t; #define IBF_MAJOR_VERSION ISEQ_MAJOR_VERSION #ifdef RUBY_DEVEL -#define IBF_DEVEL_VERSION 8 +#define IBF_DEVEL_VERSION 9 #define IBF_MINOR_VERSION (ISEQ_MINOR_VERSION * 10000 + IBF_DEVEL_VERSION) #else #define IBF_MINOR_VERSION ISEQ_MINOR_VERSION @@ -13955,6 +13955,7 @@ ibf_dump_iseq_each(struct ibf_dump *dump, const rb_iseq_t *iseq) ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(param_opt_table_offset)); ibf_dump_write_small_value(dump, param_keyword_offset); ibf_dump_write_small_value(dump, location_pathobj_index); + ibf_dump_write_small_value(dump, body->no_realpath); ibf_dump_write_small_value(dump, location_label_index); ibf_dump_write_small_value(dump, body->location.first_lineno); ibf_dump_write_small_value(dump, body->location.node_id); @@ -14070,6 +14071,7 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset) const ibf_offset_t param_opt_table_offset = (ibf_offset_t)IBF_BODY_OFFSET(ibf_load_small_value(load, &reading_pos)); const ibf_offset_t param_keyword_offset = (ibf_offset_t)ibf_load_small_value(load, &reading_pos); const VALUE location_pathobj_index = ibf_load_small_value(load, &reading_pos); + const bool no_realpath = (bool)ibf_load_small_value(load, &reading_pos); const VALUE location_label_index = ibf_load_small_value(load, &reading_pos); const int location_first_lineno = (int)ibf_load_small_value(load, &reading_pos); const int location_node_id = (int)ibf_load_small_value(load, &reading_pos); @@ -14111,7 +14113,8 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset) VALUE realpath = Qnil; if (RB_TYPE_P(path, T_STRING)) { - realpath = path = rb_fstring(path); + path = rb_fstring(path); + realpath = no_realpath ? Qnil : path; } else if (RB_TYPE_P(path, T_ARRAY)) { VALUE pathobj = path; @@ -15398,6 +15401,7 @@ rb_iseq_dup_with_independent_caches(const rb_iseq_t *src_root) struct rb_iseq_constant_body *cb = ISEQ_BODY(copy); if (!cb->local_iseq) RB_OBJ_WRITE(copy, &cb->local_iseq, sb->local_iseq); RB_OBJ_WRITE(copy, &cb->location.pathobj, sb->location.pathobj); + cb->no_realpath = sb->no_realpath; VALUE sl = ISEQ_SCRIPT_LINES(src_root); VALUE cov = ISEQ_COVERAGE(src_root); if (!NIL_P(sl) || !NIL_P(cov)) { diff --git a/iseq.c b/iseq.c index 7efa247de17..69ca09c7c57 100644 --- a/iseq.c +++ b/iseq.c @@ -659,20 +659,19 @@ iseq_alloc(void) } VALUE -rb_iseq_pathobj_new(VALUE path, VALUE realpath) +rb_iseq_pathobj_new(VALUE path, VALUE realpath, bool *no_realpath) { VALUE pathobj; VM_ASSERT(RB_TYPE_P(path, T_STRING)); VM_ASSERT(NIL_P(realpath) || RB_TYPE_P(realpath, T_STRING)); - if (path == realpath || - (!NIL_P(realpath) && rb_str_cmp(path, realpath) == 0)) { + *no_realpath = NIL_P(realpath); + + if (*no_realpath || path == realpath || rb_str_cmp(path, realpath) == 0) { pathobj = rb_fstring(path); } else { - if (!NIL_P(realpath)) { - realpath = rb_fstring(realpath); - } + realpath = rb_fstring(realpath); VALUE fpath = rb_fstring(path); pathobj = rb_ary_new_from_args(2, fpath, realpath); @@ -685,8 +684,10 @@ rb_iseq_pathobj_new(VALUE path, VALUE realpath) void rb_iseq_pathobj_set(const rb_iseq_t *iseq, VALUE path, VALUE realpath) { - RB_OBJ_WRITE(iseq, &ISEQ_BODY(iseq)->location.pathobj, - rb_iseq_pathobj_new(path, realpath)); + bool no_realpath; + VALUE pathobj = rb_iseq_pathobj_new(path, realpath, &no_realpath); + RB_OBJ_WRITE(iseq, &ISEQ_BODY(iseq)->location.pathobj, pathobj); + ISEQ_BODY(iseq)->no_realpath = no_realpath; } // Make a dummy iseq for a dummy frame that exposes a path for profilers to inspect @@ -1594,7 +1595,7 @@ rb_iseq_path(const rb_iseq_t *iseq) VALUE rb_iseq_realpath(const rb_iseq_t *iseq) { - return pathobj_realpath(ISEQ_BODY(iseq)->location.pathobj); + return pathobj_realpath(ISEQ_BODY(iseq)->location.pathobj, ISEQ_BODY(iseq)->no_realpath); } VALUE diff --git a/proc.c b/proc.c index dc9a55de103..1c34b4c336b 100644 --- a/proc.c +++ b/proc.c @@ -4565,11 +4565,14 @@ proc_binding(VALUE self) if (iseq) { rb_iseq_check(iseq); RB_OBJ_WRITE(bindval, &bind->pathobj, ISEQ_BODY(iseq)->location.pathobj); + bind->no_realpath = ISEQ_BODY(iseq)->no_realpath; bind->first_lineno = ISEQ_BODY(iseq)->location.first_lineno; } else { + bool no_realpath; RB_OBJ_WRITE(bindval, &bind->pathobj, - rb_iseq_pathobj_new(rb_fstring_lit("(binding)"), Qnil)); + rb_iseq_pathobj_new(rb_fstring_lit("(binding)"), Qnil, &no_realpath)); + bind->no_realpath = no_realpath; bind->first_lineno = 1; } diff --git a/vm.c b/vm.c index 839810b739b..7267109233c 100644 --- a/vm.c +++ b/vm.c @@ -1793,6 +1793,7 @@ rb_vm_make_binding(const rb_execution_context_t *ec, const rb_control_frame_t *s RB_OBJ_WRITE(bindval, &bind->block.as.captured.self, cfp->self); RB_OBJ_WRITE(bindval, &bind->block.as.captured.code.iseq, CFP_ISEQ(cfp)); RB_OBJ_WRITE(bindval, &bind->pathobj, ISEQ_BODY(CFP_ISEQ(ruby_level_cfp))->location.pathobj); + bind->no_realpath = ISEQ_BODY(CFP_ISEQ(ruby_level_cfp))->no_realpath; bind->first_lineno = rb_vm_get_sourceline(ruby_level_cfp); return bindval; @@ -1803,7 +1804,7 @@ rb_binding_add_dynavars(VALUE bindval, rb_binding_t *bind, int dyncount, const I { VALUE envval, pathobj = bind->pathobj; VALUE path = pathobj_path(pathobj); - VALUE realpath = pathobj_realpath(pathobj); + VALUE realpath = pathobj_realpath(pathobj, bind->no_realpath); const struct rb_block *base_block; const rb_env_t *env; rb_execution_context_t *ec = GET_EC(); diff --git a/vm_core.h b/vm_core.h index df995d2c2e3..0fe4f94e0f6 100644 --- a/vm_core.h +++ b/vm_core.h @@ -337,7 +337,7 @@ struct rb_execution_context_struct; #endif typedef struct rb_iseq_location_struct { - VALUE pathobj; /* String (path) or Array [path, realpath]. Frozen. */ + VALUE pathobj; /* String (path) or Array [path, realpath]. Frozen. See pathobj_realpath(). */ VALUE label; /* String */ int first_lineno; int node_id; @@ -360,9 +360,13 @@ pathobj_path(VALUE pathobj) } static inline VALUE -pathobj_realpath(VALUE pathobj) +pathobj_realpath(VALUE pathobj, bool no_realpath) { - if (RB_TYPE_P(pathobj, T_STRING)) { + if (no_realpath) { + VM_ASSERT(RB_TYPE_P(pathobj, T_STRING)); + return Qnil; + } + else if (RB_TYPE_P(pathobj, T_STRING)) { return pathobj; } else { @@ -554,6 +558,7 @@ struct rb_iseq_constant_body { unsigned int builtin_attrs; // Union of rb_builtin_attr bool prism; // ISEQ was generated from prism compiler + bool no_realpath; // location.pathobj names no real file (ex: during eval) // Set once an EP escape of this iseq has been reported to the enabled JIT. rb_atomic_t jit_ep_escape_recorded; @@ -1414,6 +1419,7 @@ typedef struct { const struct rb_block block; const VALUE pathobj; int first_lineno; + bool no_realpath; } rb_binding_t; /* used by compile time and send insn */ @@ -2055,7 +2061,7 @@ VALUE rb_iseq_path(const rb_iseq_t *iseq); VALUE rb_iseq_realpath(const rb_iseq_t *iseq); RUBY_SYMBOL_EXPORT_END -VALUE rb_iseq_pathobj_new(VALUE path, VALUE realpath); +VALUE rb_iseq_pathobj_new(VALUE path, VALUE realpath, bool *no_realpath); void rb_iseq_pathobj_set(const rb_iseq_t *iseq, VALUE path, VALUE realpath); int rb_ec_frame_method_id_and_class(const rb_execution_context_t *ec, ID *idp, ID *called_idp, VALUE *klassp);