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 `["<internal:numeric>", nil]` per iseq created in those files. Around half
of those survive GC and become long-lived Arrays.
This commit is contained in:
Luke Gruber authored and Luke Gruber committed 2026-10-13 15:01:01 -04:00
1 parent 1b19b75b9c
commit 817e940d8c
5 files changed
+32 -17
Notes: git 2026-10-13 19:01:32 +00:00

No files matched your search

+6 -2
View File
@@ -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)) {
+10 -9
View File
@@ -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
+4 -1
View File
@@ -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;
}
+2 -1
View File
@@ -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();
+10 -4
View File
@@ -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);