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:
Aaron Patterson 2023-09-18 14:44:51 -07:00 committed by Aaron Patterson
parent 655bcee95a
commit d3574c117a
5 changed files with 150 additions and 15 deletions

View File

@ -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
View File

@ -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
View File

@ -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

View File

@ -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
View File

@ -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 *