Move IO#readline to Ruby
This commit moves IO#readline to Ruby. In order to call C functions, keyword arguments must be converted to hashes. Prior to this commit, code like `io.readline(chomp: true)` would allocate a hash. This commits moves the keyword "denaturing" to Ruby, allowing us to send positional arguments to the C API and avoiding the hash allocation. Here is an allocation benchmark for the method: ``` x = GC.stat(:total_allocated_objects) File.open("/usr/share/dict/words") do |f| f.readline(chomp: true) until f.eof? end p ALLOCATIONS: GC.stat(:total_allocated_objects) - x ``` Before this commit, the output was this: ``` $ make run ./miniruby -I./lib -I. -I.ext/common -r./arm64-darwin22-fake ./test.rb {:ALLOCATIONS=>707939} ``` Now it is this: ``` $ make run ./miniruby -I./lib -I. -I.ext/common -r./arm64-darwin22-fake ./test.rb {:ALLOCATIONS=>471962} ``` [Bug #19890] [ruby-core:114803]
This commit is contained in:
parent
655bcee95a
commit
d3574c117a
@ -58,6 +58,8 @@ VALUE rb_yield_refine_block(VALUE refinement, VALUE refinements);
|
|||||||
VALUE ruby_vm_special_exception_copy(VALUE);
|
VALUE ruby_vm_special_exception_copy(VALUE);
|
||||||
PUREFUNC(st_table *rb_vm_fstring_table(void));
|
PUREFUNC(st_table *rb_vm_fstring_table(void));
|
||||||
|
|
||||||
|
void rb_lastline_set_up(VALUE val, unsigned int up);
|
||||||
|
|
||||||
/* vm_eval.c */
|
/* vm_eval.c */
|
||||||
VALUE rb_current_realfilepath(void);
|
VALUE rb_current_realfilepath(void);
|
||||||
VALUE rb_check_block_call(VALUE, ID, int, const VALUE *, rb_block_call_func_t, VALUE);
|
VALUE rb_check_block_call(VALUE, ID, int, const VALUE *, rb_block_call_func_t, VALUE);
|
||||||
|
35
io.c
35
io.c
@ -4357,22 +4357,28 @@ rb_io_set_lineno(VALUE io, VALUE lineno)
|
|||||||
return lineno;
|
return lineno;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/* :nodoc: */
|
||||||
* call-seq:
|
|
||||||
* readline(sep = $/, chomp: false) -> string
|
|
||||||
* readline(limit, chomp: false) -> string
|
|
||||||
* readline(sep, limit, chomp: false) -> string
|
|
||||||
*
|
|
||||||
* Reads a line as with IO#gets, but raises EOFError if already at end-of-stream.
|
|
||||||
*
|
|
||||||
* Optional keyword argument +chomp+ specifies whether line separators
|
|
||||||
* are to be omitted.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static VALUE
|
static VALUE
|
||||||
rb_io_readline(int argc, VALUE *argv, VALUE io)
|
io_readline(rb_execution_context_t *ec, VALUE io, VALUE sep, VALUE lim, VALUE chomp)
|
||||||
{
|
{
|
||||||
VALUE line = rb_io_gets_m(argc, argv, io);
|
if (NIL_P(lim)) {
|
||||||
|
// If sep is specified, but it's not a string and not nil, then assume
|
||||||
|
// it's the limit (it should be an integer)
|
||||||
|
if (!NIL_P(sep) && NIL_P(rb_check_string_type(sep))) {
|
||||||
|
// If the user has specified a non-nil / non-string value
|
||||||
|
// for the separator, we assume it's the limit and set the
|
||||||
|
// separator to default: rb_rs.
|
||||||
|
lim = sep;
|
||||||
|
sep = rb_rs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NIL_P(sep)) {
|
||||||
|
StringValue(sep);
|
||||||
|
}
|
||||||
|
|
||||||
|
VALUE line = rb_io_getline_1(sep, NIL_P(lim) ? -1L : NUM2LONG(lim), RTEST(chomp), io);
|
||||||
|
rb_lastline_set_up(line, 1);
|
||||||
|
|
||||||
if (NIL_P(line)) {
|
if (NIL_P(line)) {
|
||||||
rb_eof_error();
|
rb_eof_error();
|
||||||
@ -15420,7 +15426,6 @@ Init_IO(void)
|
|||||||
rb_define_method(rb_cIO, "read", io_read, -1);
|
rb_define_method(rb_cIO, "read", io_read, -1);
|
||||||
rb_define_method(rb_cIO, "write", io_write_m, -1);
|
rb_define_method(rb_cIO, "write", io_write_m, -1);
|
||||||
rb_define_method(rb_cIO, "gets", rb_io_gets_m, -1);
|
rb_define_method(rb_cIO, "gets", rb_io_gets_m, -1);
|
||||||
rb_define_method(rb_cIO, "readline", rb_io_readline, -1);
|
|
||||||
rb_define_method(rb_cIO, "getc", rb_io_getc, 0);
|
rb_define_method(rb_cIO, "getc", rb_io_getc, 0);
|
||||||
rb_define_method(rb_cIO, "getbyte", rb_io_getbyte, 0);
|
rb_define_method(rb_cIO, "getbyte", rb_io_getbyte, 0);
|
||||||
rb_define_method(rb_cIO, "readchar", rb_io_readchar, 0);
|
rb_define_method(rb_cIO, "readchar", rb_io_readchar, 0);
|
||||||
|
13
io.rb
13
io.rb
@ -120,4 +120,17 @@ class IO
|
|||||||
def write_nonblock(buf, exception: true)
|
def write_nonblock(buf, exception: true)
|
||||||
Primitive.io_write_nonblock(buf, exception)
|
Primitive.io_write_nonblock(buf, exception)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# call-seq:
|
||||||
|
# readline(sep = $/, chomp: false) -> string
|
||||||
|
# readline(limit, chomp: false) -> string
|
||||||
|
# readline(sep, limit, chomp: false) -> string
|
||||||
|
#
|
||||||
|
# Reads a line as with IO#gets, but raises EOFError if already at end-of-stream.
|
||||||
|
#
|
||||||
|
# Optional keyword argument +chomp+ specifies whether line separators
|
||||||
|
# are to be omitted.
|
||||||
|
def readline(sep = $/, limit = nil, chomp: false)
|
||||||
|
Primitive.io_readline(sep, limit, chomp)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1898,6 +1898,110 @@ class TestIO < Test::Unit::TestCase
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_readline_bad_param_raises
|
||||||
|
File.open(__FILE__) do |f|
|
||||||
|
assert_raise(TypeError) do
|
||||||
|
f.readline Object.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
File.open(__FILE__) do |f|
|
||||||
|
assert_raise(TypeError) do
|
||||||
|
f.readline 1, 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_readline_raises
|
||||||
|
File.open(__FILE__) do |f|
|
||||||
|
assert_equal File.read(__FILE__), f.readline(nil)
|
||||||
|
assert_raise(EOFError) do
|
||||||
|
f.readline
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_readline_separators
|
||||||
|
File.open(__FILE__) do |f|
|
||||||
|
line = f.readline("def")
|
||||||
|
assert_equal File.read(__FILE__)[/\A.*?def/m], line
|
||||||
|
end
|
||||||
|
|
||||||
|
File.open(__FILE__) do |f|
|
||||||
|
line = f.readline("def", chomp: true)
|
||||||
|
assert_equal File.read(__FILE__)[/\A.*?(?=def)/m], line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_readline_separators_limits
|
||||||
|
t = Tempfile.open("readline_limit")
|
||||||
|
str = "#" * 50
|
||||||
|
sep = "def"
|
||||||
|
|
||||||
|
t.write str
|
||||||
|
t.write sep
|
||||||
|
t.write str
|
||||||
|
t.flush
|
||||||
|
|
||||||
|
# over limit
|
||||||
|
File.open(t.path) do |f|
|
||||||
|
line = f.readline sep, str.bytesize
|
||||||
|
assert_equal(str, line)
|
||||||
|
end
|
||||||
|
|
||||||
|
# under limit
|
||||||
|
File.open(t.path) do |f|
|
||||||
|
line = f.readline(sep, str.bytesize + 5)
|
||||||
|
assert_equal(str + sep, line)
|
||||||
|
end
|
||||||
|
|
||||||
|
# under limit + chomp
|
||||||
|
File.open(t.path) do |f|
|
||||||
|
line = f.readline(sep, str.bytesize + 5, chomp: true)
|
||||||
|
assert_equal(str, line)
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
t&.close!
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_readline_limit_without_separator
|
||||||
|
t = Tempfile.open("readline_limit")
|
||||||
|
str = "#" * 50
|
||||||
|
sep = "\n"
|
||||||
|
|
||||||
|
t.write str
|
||||||
|
t.write sep
|
||||||
|
t.write str
|
||||||
|
t.flush
|
||||||
|
|
||||||
|
# over limit
|
||||||
|
File.open(t.path) do |f|
|
||||||
|
line = f.readline str.bytesize
|
||||||
|
assert_equal(str, line)
|
||||||
|
end
|
||||||
|
|
||||||
|
# under limit
|
||||||
|
File.open(t.path) do |f|
|
||||||
|
line = f.readline(str.bytesize + 5)
|
||||||
|
assert_equal(str + sep, line)
|
||||||
|
end
|
||||||
|
|
||||||
|
# under limit + chomp
|
||||||
|
File.open(t.path) do |f|
|
||||||
|
line = f.readline(str.bytesize + 5, chomp: true)
|
||||||
|
assert_equal(str, line)
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
t&.close!
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_readline_chomp_true
|
||||||
|
File.open(__FILE__) do |f|
|
||||||
|
line = f.readline(chomp: true)
|
||||||
|
assert_equal File.readlines(__FILE__).first.chomp, line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_set_lineno_readline
|
def test_set_lineno_readline
|
||||||
pipe(proc do |w|
|
pipe(proc do |w|
|
||||||
w.puts "foo"
|
w.puts "foo"
|
||||||
|
11
vm.c
11
vm.c
@ -1750,6 +1750,17 @@ rb_lastline_set(VALUE val)
|
|||||||
vm_svar_set(GET_EC(), VM_SVAR_LASTLINE, val);
|
vm_svar_set(GET_EC(), VM_SVAR_LASTLINE, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
rb_lastline_set_up(VALUE val, unsigned int up)
|
||||||
|
{
|
||||||
|
rb_control_frame_t * cfp = GET_EC()->cfp;
|
||||||
|
|
||||||
|
for(unsigned int i = 0; i < up; i++) {
|
||||||
|
cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
|
||||||
|
}
|
||||||
|
vm_cfp_svar_set(GET_EC(), cfp, VM_SVAR_LASTLINE, val);
|
||||||
|
}
|
||||||
|
|
||||||
/* misc */
|
/* misc */
|
||||||
|
|
||||||
const char *
|
const char *
|
||||||
|
Loading…
x
Reference in New Issue
Block a user