Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions src/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -2238,7 +2238,18 @@ vm_op_div(mrb_state *mrb, uint32_t a, mrb_sym *midp)

#ifndef MRB_NO_FLOAT
f = mrb_div_float(x, y);
SET_FLOAT_VALUE(mrb, regs[a], f);
{
/* This is the one boxing site of OP_DIV that sits outside mrb_vm_exec(),
so VM_SET_FLOAT_VALUE() and the arena index it restores to are not in
scope. Saving here instead restores to the height on entry to this
branch rather than to the height on entry to the frame, which still
bounds the arena across a loop and is what the Integer branch above
already does. The result needs no protection past the restore: it is
stored into regs[], and the VM stack is a GC root. */
int ai = mrb_gc_arena_save(mrb);
SET_FLOAT_VALUE(mrb, regs[a], f);
mrb_gc_arena_restore(mrb, ai);
}
#endif
return VM_NEXT;
}
Expand Down Expand Up @@ -2319,6 +2330,23 @@ vm_call_proc(mrb_state *mrb, const struct RProc *p, mrb_int nargs,
#define VM_SET_INT_VALUE(r,n) SET_INT_VALUE(mrb,r,n)
#endif

/* The same for an mrb_float. Under word boxing SET_FLOAT_VALUE() heap-
allocates an RFloat for a value the mrb_value word cannot hold inline: every
float under MRB_WORDBOX_NO_INLINE_FLOAT, and otherwise a subnormal, an
exponent outside the inlinable range, or a rotation that would collide with
a sentinel. The other boxing modes store the float in the mrb_value itself
and never allocate, so there this expands to the bare store. As above the
restore is skipped when the store produced an immediate, which is the inline
float fast path, and `ai` is mrb_vm_exec()'s saved arena index. */
#ifdef MRB_WORD_BOXING
#define VM_SET_FLOAT_VALUE(r,f) do { \
SET_FLOAT_VALUE(mrb, r, f); \
if (!mrb_immediate_p(r)) mrb_gc_arena_restore(mrb, ai); \
} while (0)
#else
#define VM_SET_FLOAT_VALUE(r,f) SET_FLOAT_VALUE(mrb,r,f)
#endif

/**
* @brief Executes a sequence of mruby bytecode instructions.
*
Expand Down Expand Up @@ -2443,7 +2471,7 @@ mrb_vm_exec(mrb_state *mrb, const struct RProc *begin_proc, const mrb_code *iseq
#endif
#ifndef MRB_NO_FLOAT
case IREP_TT_FLOAT:
regs[a] = mrb_float_value(mrb, irep->pool[b].u.f);
VM_SET_FLOAT_VALUE(regs[a], irep->pool[b].u.f);
break;
#endif
default:
Expand Down Expand Up @@ -3328,7 +3356,7 @@ mrb_vm_exec(mrb_state *mrb, const struct RProc *begin_proc, const mrb_code *iseq
case TYPES2(OP_MATH_TT_##t1, OP_MATH_TT_##t2): \
{ \
mrb_float z = mrb_##t1(regs[a]) OP_MATH_OP_##op_name mrb_##t2(regs[a+1]); \
SET_FLOAT_VALUE(mrb, regs[a], z); \
VM_SET_FLOAT_VALUE(regs[a], z); \
} \
break
#endif
Expand Down Expand Up @@ -3409,7 +3437,7 @@ mrb_vm_exec(mrb_state *mrb, const struct RProc *begin_proc, const mrb_code *iseq
case MRB_TT_FLOAT: \
{ \
mrb_float z = mrb_float(regs[a]) OP_MATH_OP_##op_name b; \
SET_FLOAT_VALUE(mrb, regs[a], z); \
VM_SET_FLOAT_VALUE(regs[a], z); \
} \
break
#endif
Expand All @@ -3429,7 +3457,7 @@ mrb_vm_exec(mrb_state *mrb, const struct RProc *begin_proc, const mrb_code *iseq
case MRB_TT_FLOAT: \
{ \
mrb_float z = mrb_float(regs[a]) OP_MATH_OP_##op_name c; \
SET_FLOAT_VALUE(mrb, regs[a], z); \
VM_SET_FLOAT_VALUE(regs[a], z); \
} \
break
#endif
Expand Down
130 changes: 117 additions & 13 deletions test/t/gc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,21 @@
# The inline `[]`, `[]=` and arithmetic opcodes answer from C without a method
# call, so the arena restore that every cfunc return performs never runs for
# them. What they allocate then stays arena-protected for the rest of the
# enclosing method. `GC.stat` is itself a cfunc, so it reads the count before
# its own restore and still sees what the loop pinned. The loop bodies below
# are built only from opcodes that do not restore, since a single send in the
# body would empty the arena and hide the retention.
# enclosing method. The loop bodies below are built only from opcodes that do
# not restore, since a single send in the body would empty the arena and hide
# the retention.
#
# Each assertion runs a full GC while the arena still holds what the loop left
# there. The arena is a GC root, so exactly the pinned objects survive that
# collection and the rise in `GC.stat[:live]` counts them and nothing else.
# The `GC.start` has to be the first send after the loop: any cfunc return
# drains the arena, so reading `GC.stat` first would discard the very thing
# being measured.
#
# A retaining branch pins one object per iteration and reports the full 20000.
# The margin below covers the few objects `GC.stat` allocates for its own
# result; it is not slack for a partial leak, and a branch that retains on even
# a small fraction of the iterations is over it.

assert('OP_GETIDX does not retain its result in the GC arena') do
s = "hello"
Expand All @@ -133,7 +144,8 @@
s[1]
i += 1
end
assert_operator GC.stat[:live] - base, :<, 5000
GC.start
assert_operator GC.stat[:live] - base, :<, 100
end

assert('OP_GETIDX does not retain a Hash default in the GC arena') do
Expand All @@ -145,7 +157,8 @@
h[1]
i += 1
end
assert_operator GC.stat[:live] - base, :<, 5000
GC.start
assert_operator GC.stat[:live] - base, :<, 100
end

assert('OP_GETIDX0 does not retain a String result in the GC arena') do
Expand All @@ -157,7 +170,8 @@
s[0]
i += 1
end
assert_operator GC.stat[:live] - base, :<, 5000
GC.start
assert_operator GC.stat[:live] - base, :<, 100
end

assert('OP_GETIDX0 does not retain a Hash default in the GC arena') do
Expand All @@ -169,7 +183,8 @@
h[0]
i += 1
end
assert_operator GC.stat[:live] - base, :<, 5000
GC.start
assert_operator GC.stat[:live] - base, :<, 100
end

assert('OP_SETIDX does not retain a duplicated Hash key in the GC arena') do
Expand All @@ -182,7 +197,8 @@
h[k] = 1
i += 1
end
assert_operator GC.stat[:live] - base, :<, 5000
GC.start
assert_operator GC.stat[:live] - base, :<, 100
end

assert('OP_ADD does not retain an overflowed Integer in the GC arena') do
Expand All @@ -208,7 +224,8 @@
x + x
i += 1
end
assert_operator GC.stat[:live] - base, :<, 5000
GC.start
assert_operator GC.stat[:live] - base, :<, 100
end
end

Expand Down Expand Up @@ -239,7 +256,8 @@
x + 1 # OP_ADDI
i += 1
end
assert_operator GC.stat[:live] - base, :<, 5000
GC.start
assert_operator GC.stat[:live] - base, :<, 100
end
end

Expand All @@ -258,7 +276,8 @@
x / one
i += 1
end
assert_operator GC.stat[:live] - base, :<, 5000
GC.start
assert_operator GC.stat[:live] - base, :<, 100
end
end

Expand All @@ -275,6 +294,91 @@
z = 1073741824
i += 1
end
assert_operator GC.stat[:live] - base, :<, 5000
GC.start
assert_operator GC.stat[:live] - base, :<, 100
assert_equal 1073741824, z
end

# The Float branches of the same opcodes box through `SET_FLOAT_VALUE()`, which
# under word boxing heap-allocates an RFloat whenever the mrb_value word cannot
# hold the value inline. With `MRB_WORDBOX_NO_INLINE_FLOAT` that is every
# Float; without it, on a 64-bit host, it is a subnormal, an exponent outside
# the inlinable range, or a rotation that would collide with a sentinel.
# `5.0e-324` and `1.0e100` are on the wrong side of both bounds, so the loops
# allocate under either setting. The immediate-operand opcodes are run only on
# `1.0e100`, since a subnormal plus a non-zero integer is an ordinary Float that
# does inline. On a boxing mode that keeps the Float in the word the loops
# retain nothing and the assertions hold trivially.

assert('OP_MATH does not retain a boxed Float in the GC arena') do
[1.0e100, 5.0e-324].each do |x|
zero = 0.0
one = 1.0
y = nil
GC.start
base = GC.stat[:live]
i = 0
while i < 20000
y = x + zero # OP_ADD
y = x - zero # OP_SUB
y = x * one # OP_MUL
i += 1
end
GC.start
assert_operator GC.stat[:live] - base, :<, 100
assert_equal x, y
end
end

assert('OP_DIV does not retain a boxed Float in the GC arena') do
# OP_DIV boxes from a helper outside the interpreter loop, so it restores to
# its own saved arena index rather than to the frame's.
[1.0e100, 5.0e-324].each do |x|
one = 1.0
y = nil
GC.start
base = GC.stat[:live]
i = 0
while i < 20000
y = x / one
i += 1
end
GC.start
assert_operator GC.stat[:live] - base, :<, 100
assert_equal x, y
end
end

assert('OP_ADDI does not retain a boxed Float in the GC arena') do
x = 1.0e100
y = nil
GC.start
base = GC.stat[:live]
i = 0
while i < 20000
y = x + 1 # OP_ADDI
y = x - 1 # OP_SUBI
x += 1 # OP_ADDILV, the fused local form
x -= 1 # OP_SUBILV
i += 1
end
GC.start
assert_operator GC.stat[:live] - base, :<, 100
assert_equal 1.0e100, x
assert_equal 1.0e100, y
end

assert('OP_LOADL does not retain a boxed Float in the GC arena') do
y = nil
GC.start
base = GC.stat[:live]
i = 0
while i < 20000
y = 1.0e100
y = 5.0e-324
i += 1
end
GC.start
assert_operator GC.stat[:live] - base, :<, 100
assert_equal 5.0e-324, y
end
Loading