YJIT: Implement new struct accessors (#5161)

* YJIT: Implement optimized_method_struct_aref

* YJIT: Implement struct_aref without method call

Struct member reads can be compiled directly into a memory read (with
either one or two levels of indirection).

* YJIT: Implement optimized struct aset

* YJIT: Update tests for struct access

* YJIT: Add counters for remaining optimized methods

* Check for INT32_MAX overflow

It only takes a struct with 0x7fffffff/8+1 members. Also add some
cheap compile time checks.

* Add tests for non-embedded struct aref/aset

Co-authored-by: Alan Wu <XrXr@users.noreply.github.com>
This commit is contained in:
John Hawthorn 2021-11-25 11:56:58 -08:00 коммит произвёл GitHub
Родитель e469ebd7d3
Коммит de9a1e4a96
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 192 добавлений и 5 удалений

Просмотреть файл

@ -2455,3 +2455,80 @@ assert_equal 'new', %q{
test
} if false # disabled for now since OOM crashes in the test harness
# struct aref embedded
assert_equal '2', %q{
def foo(s)
s.foo
end
S = Struct.new(:foo)
foo(S.new(1))
foo(S.new(2))
}
# struct aref non-embedded
assert_equal '4', %q{
def foo(s)
s.d
end
S = Struct.new(:a, :b, :c, :d, :e)
foo(S.new(1,2,3,4,5))
foo(S.new(1,2,3,4,5))
}
# struct aset embedded
assert_equal '123', %q{
def foo(s)
s.foo = 123
end
s = Struct.new(:foo).new
foo(s)
s = Struct.new(:foo).new
foo(s)
s.foo
}
# struct aset non-embedded
assert_equal '[1, 2, 3, 4, 5]', %q{
def foo(s)
s.a = 1
s.b = 2
s.c = 3
s.d = 4
s.e = 5
end
S = Struct.new(:a, :b, :c, :d, :e)
s = S.new
foo(s)
s = S.new
foo(s)
[s.a, s.b, s.c, s.d, s.e]
}
# struct aref too many args
assert_equal 'ok', %q{
def foo(s)
s.foo(:bad)
end
s = Struct.new(:foo).new
foo(s) rescue :ok
foo(s) rescue :ok
}
# struct aset too many args
assert_equal 'ok', %q{
def foo(s)
s.set_foo(123, :bad)
end
s = Struct.new(:foo) do
alias :set_foo :foo=
end
foo(s) rescue :ok
foo(s) rescue :ok
}

Просмотреть файл

@ -17483,6 +17483,7 @@ yjit.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h
yjit.$(OBJEXT): $(top_srcdir)/internal/serial.h
yjit.$(OBJEXT): $(top_srcdir)/internal/static_assert.h
yjit.$(OBJEXT): $(top_srcdir)/internal/string.h
yjit.$(OBJEXT): $(top_srcdir)/internal/struct.h
yjit.$(OBJEXT): $(top_srcdir)/internal/variable.h
yjit.$(OBJEXT): $(top_srcdir)/internal/vm.h
yjit.$(OBJEXT): $(top_srcdir)/internal/warnings.h

Просмотреть файл

@ -412,9 +412,20 @@ class TestYJIT < Test::Unit::TestCase
RUBY
end
def test_invokebuiltin
skip "Struct's getter/setter doesn't use invokebuiltin and YJIT doesn't support new logic"
def test_struct_aref
assert_compiles(<<~RUBY)
def foo(obj)
obj.foo
obj.bar
end
Foo = Struct.new(:foo, :bar)
foo(Foo.new(123))
foo(Foo.new(123))
RUBY
end
def test_struct_aset
assert_compiles(<<~RUBY)
def foo(obj)
obj.foo = 123

3
yjit.c
Просмотреть файл

@ -69,6 +69,9 @@ YJIT_DECLARE_COUNTERS(
send_zsuper_method,
send_undef_method,
send_optimized_method,
send_optimized_method_send,
send_optimized_method_call,
send_optimized_method_block_call,
send_missing_method,
send_bmethod,
send_refined_method,

Просмотреть файл

@ -7,6 +7,7 @@
#include "internal/object.h"
#include "internal/sanitizers.h"
#include "internal/string.h"
#include "internal/struct.h"
#include "internal/variable.h"
#include "internal/re.h"
#include "probes.h"
@ -3901,6 +3902,83 @@ gen_send_iseq(jitstate_t *jit, ctx_t *ctx, const struct rb_callinfo *ci, const r
return YJIT_END_BLOCK;
}
static codegen_status_t
gen_struct_aref(jitstate_t *jit, ctx_t *ctx, const struct rb_callinfo *ci, const rb_callable_method_entry_t *cme, VALUE comptime_recv, VALUE comptime_recv_klass) {
if (vm_ci_argc(ci) != 0) {
return YJIT_CANT_COMPILE;
}
const unsigned int off = cme->def->body.optimized.index;
// Confidence checks
RUBY_ASSERT_ALWAYS(RB_TYPE_P(comptime_recv, T_STRUCT));
RUBY_ASSERT_ALWAYS((long)off < RSTRUCT_LEN(comptime_recv));
// We are going to use an encoding that takes a 4-byte immediate which
// limits the offset to INT32_MAX.
{
uint64_t native_off = (uint64_t)off * (uint64_t)SIZEOF_VALUE;
if (native_off > (uint64_t)INT32_MAX) {
return YJIT_CANT_COMPILE;
}
}
// All structs from the same Struct class should have the same
// length. So if our comptime_recv is embedded all runtime
// structs of the same class should be as well, and the same is
// true of the converse.
bool embedded = FL_TEST_RAW(comptime_recv, RSTRUCT_EMBED_LEN_MASK);
ADD_COMMENT(cb, "struct aref");
x86opnd_t recv = ctx_stack_pop(ctx, 1);
mov(cb, REG0, recv);
if (embedded) {
mov(cb, REG0, member_opnd_idx(REG0, struct RStruct, as.ary, off));
}
else {
mov(cb, REG0, member_opnd(REG0, struct RStruct, as.heap.ptr));
mov(cb, REG0, mem_opnd(64, REG0, SIZEOF_VALUE * off));
}
x86opnd_t ret = ctx_stack_push(ctx, TYPE_UNKNOWN);
mov(cb, ret, REG0);
jit_jump_to_next_insn(jit, ctx);
return YJIT_END_BLOCK;
}
static codegen_status_t
gen_struct_aset(jitstate_t *jit, ctx_t *ctx, const struct rb_callinfo *ci, const rb_callable_method_entry_t *cme, VALUE comptime_recv, VALUE comptime_recv_klass) {
if (vm_ci_argc(ci) != 1) {
return YJIT_CANT_COMPILE;
}
const unsigned int off = cme->def->body.optimized.index;
// Confidence checks
RUBY_ASSERT_ALWAYS(RB_TYPE_P(comptime_recv, T_STRUCT));
RUBY_ASSERT_ALWAYS((long)off < RSTRUCT_LEN(comptime_recv));
ADD_COMMENT(cb, "struct aset");
x86opnd_t val = ctx_stack_pop(ctx, 1);
x86opnd_t recv = ctx_stack_pop(ctx, 1);
mov(cb, C_ARG_REGS[0], recv);
mov(cb, C_ARG_REGS[1], imm_opnd(off));
mov(cb, C_ARG_REGS[2], val);
call_ptr(cb, REG0, (void *)RSTRUCT_SET);
x86opnd_t ret = ctx_stack_push(ctx, TYPE_UNKNOWN);
mov(cb, ret, RAX);
jit_jump_to_next_insn(jit, ctx);
return YJIT_END_BLOCK;
}
const rb_callable_method_entry_t *
rb_aliased_callable_method_entry(const rb_callable_method_entry_t *me);
@ -4064,8 +4142,24 @@ gen_send_general(jitstate_t *jit, ctx_t *ctx, struct rb_call_data *cd, rb_iseq_t
return YJIT_CANT_COMPILE;
// Send family of methods, e.g. call/apply
case VM_METHOD_TYPE_OPTIMIZED:
GEN_COUNTER_INC(cb, send_optimized_method);
return YJIT_CANT_COMPILE;
switch (cme->def->body.optimized.type) {
case OPTIMIZED_METHOD_TYPE_SEND:
GEN_COUNTER_INC(cb, send_optimized_method_send);
return YJIT_CANT_COMPILE;
case OPTIMIZED_METHOD_TYPE_CALL:
GEN_COUNTER_INC(cb, send_optimized_method_call);
return YJIT_CANT_COMPILE;
case OPTIMIZED_METHOD_TYPE_BLOCK_CALL:
GEN_COUNTER_INC(cb, send_optimized_method_block_call);
return YJIT_CANT_COMPILE;
case OPTIMIZED_METHOD_TYPE_STRUCT_AREF:
return gen_struct_aref(jit, ctx, ci, cme, comptime_recv, comptime_recv_klass);
case OPTIMIZED_METHOD_TYPE_STRUCT_ASET:
return gen_struct_aset(jit, ctx, ci, cme, comptime_recv, comptime_recv_klass);
default:
rb_bug("unknown optimized method type (%d)", cme->def->body.optimized.type);
UNREACHABLE_RETURN(YJIT_CANT_COMPILE);
}
case VM_METHOD_TYPE_MISSING:
GEN_COUNTER_INC(cb, send_missing_method);
return YJIT_CANT_COMPILE;
@ -4347,7 +4441,8 @@ gen_objtostring(jitstate_t *jit, ctx_t *ctx, codeblock_t *cb)
jit_guard_known_klass(jit, ctx, CLASS_OF(comptime_recv), OPND_STACK(0), comptime_recv, SEND_MAX_DEPTH, side_exit);
// No work needed. The string value is already on the top of the stack.
return YJIT_KEEP_COMPILING;
} else {
}
else {
struct rb_call_data *cd = (struct rb_call_data *)jit_get_arg(jit, 0);
return gen_send_general(jit, ctx, cd, NULL);
}