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);
|
||||
PUREFUNC(st_table *rb_vm_fstring_table(void));
|
||||
|
||||
void rb_lastline_set_up(VALUE val, unsigned int up);
|
||||
|
||||
/* vm_eval.c */
|
||||
VALUE rb_current_realfilepath(void);
|
||||
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;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* :nodoc: */
|
||||
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)) {
|
||||
rb_eof_error();
|
||||
@ -15420,7 +15426,6 @@ Init_IO(void)
|
||||
rb_define_method(rb_cIO, "read", io_read, -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, "readline", rb_io_readline, -1);
|
||||
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, "readchar", rb_io_readchar, 0);
|
||||
|
13
io.rb
13
io.rb
@ -120,4 +120,17 @@ class IO
|
||||
def write_nonblock(buf, exception: true)
|
||||
Primitive.io_write_nonblock(buf, exception)
|
||||
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
|
||||
|
@ -1898,6 +1898,110 @@ class TestIO < Test::Unit::TestCase
|
||||
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
|
||||
pipe(proc do |w|
|
||||
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);
|
||||
}
|
||||
|
||||
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 */
|
||||
|
||||
const char *
|
||||
|
Loading…
x
Reference in New Issue
Block a user