Feature #5677

IO C API

Added by Martin Bosslet over 2 years ago. Updated about 1 year ago.

[ruby-core:41338]
Status:Feedback
Priority:Normal
Assignee:Akira Tanaka
Category:core
Target version:next minor

Description

This is related to the proposal in [1].

I'd like to take advantage of streaming IO in an extension I am
working on. The problem I'm having is that I don't want to call
IO#read on the rb_funcall level because that would kill the
performance due to wrapping the bytes into Ruby objects back and
forth again.

I saw two solutions to my problem:

  1. Duplicating the file descriptor to obtain a pure FILE*
    like it is done in ext/openssl/ossl_bio.c[2] and continue
    working on the raw FILE*.

  2. Since I really only need to read and write on the stream,
    I was looking for public Ruby C API that would support me
    in the process, and I found

  • ssizet rbiobufwrite(VALUE io, const void *buf, sizet size)
  • ssizet rbiobufread(VALUE io, void *buf, sizet size)

I think both cases are valid use cases, 1. is likely necessary
if there is the need to pass a FILE* on to an external C library,
2. is for cases like mine where there is the need to operate
on raw C data types for performance reasons.

The problem, though, is that only rbiobufwrite is public API in io.h,
rbiobufread is declared private in internal.h and rbcloexecdup is
semi-public in intern.h.

Could we make rbiobufread public API in io.h as well? What about
rbcloexecdup?

[1] http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/41321
[2] https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_bio.c#L17

History

#1 Updated by Eric Wong over 2 years ago

Martin Bosslet Martin.Bosslet@googlemail.com wrote:

This is related to the proposal in [1].

I'd like to take advantage of streaming IO in an extension I am
working on. The problem I'm having is that I don't want to call
IO#read on the rb_funcall level because that would kill the
performance due to wrapping the bytes into Ruby objects back and
forth again.

Is starting with Ruby String objects (with binary encoding) and then
having read(2)/write(2) hit RSTRING_PTR not possible?

I saw two solutions to my problem:

  1. Duplicating the file descriptor to obtain a pure FILE* like it is done in ext/openssl/ossl_bio.c[2] and continue working on the raw FILE*.

That may be from the old 1.8 days when all IO objects wrapped FILE *.
It might be better to use BIOnewfd() nowadays instead since 1.9
generally prefers bare file descriptors (for all fd > 2).

  1. Since I really only need to read and write on the stream, I was looking for public Ruby C API that would support me in the process, and I found
  • ssizet rbiobufwrite(VALUE io, const void *buf, sizet size)
  • ssizet rbiobufread(VALUE io, void *buf, sizet size)

Is userspace buffering really necessary in your case?

If you're working with sockets/pipes, I would reckon not (Ruby already
defaults to IO#sync=false on sockets/pipes when writing). If you're
reading (and probably parsing), you would need to do your own read
buffering anyways, no?

I think both cases are valid use cases, 1. is likely necessary
if there is the need to pass a FILE* on to an external C library,

It's not easily possible to share userspace buffers in FILE * with
userspace buffers in rbiot. Userspace buffering is pretty miserable
and error-prone whenever/wherever IPC is concerned.

  1. is for cases like mine where there is the need to operate on raw C data types for performance reasons.

It depends on what you're doing, but if performance is a concern you
should try to work on largish chunks off the file descriptor and
skip the userspace buffering stages. Userspace buffering can improve
performance by reducing syscalls, but it can also double the memory
bandwidth required to do things.

#2 Updated by Martin Bosslet over 2 years ago

Eric Wong wrote:

First off, thanks for your comments.

Martin Bosslet Martin.Bosslet@googlemail.com wrote:

This is related to the proposal in [1].

I'd like to take advantage of streaming IO in an extension I am
working on. The problem I'm having is that I don't want to call
IO#read on the rb_funcall level because that would kill the
performance due to wrapping the bytes into Ruby objects back and
forth again.

Is starting with Ruby String objects (with binary encoding) and then
having read(2)/write(2) hit RSTRING_PTR not possible?

You mean reading String chunks from the underlying IO? I'm afraid not.
The only way I could right now is calling the Ruby methods for
IO#read/write using rb_funcall. But there's a lot of overhead involved,
VM roundtrip plus lots of short-lived objects that trigger GC. It would
likely end up being slower than the current ASN1.decode, a situation I'd
like to avoid.

I saw two solutions to my problem:

  1. Duplicating the file descriptor to obtain a pure FILE* like it is done in ext/openssl/ossl_bio.c[2] and continue working on the raw FILE*.

That may be from the old 1.8 days when all IO objects wrapped FILE *.
It might be better to use BIOnewfd() nowadays instead since 1.9
generally prefers bare file descriptors (for all fd > 2).

Good point, I will look into using it instead.

  1. Since I really only need to read and write on the stream, I was looking for public Ruby C API that would support me in the process, and I found
  • ssizet rbiobufwrite(VALUE io, const void *buf, sizet size)
  • ssizet rbiobufread(VALUE io, void *buf, sizet size)

Is userspace buffering really necessary in your case?

No, not really, but currently it's the only way the C API allows
me to do C-level streaming on an IO.

If you're working with sockets/pipes, I would reckon not (Ruby already
defaults to IO#sync=false on sockets/pipes when writing). If you're
reading (and probably parsing), you would need to do your own read
buffering anyways, no?

see below

I think both cases are valid use cases, 1. is likely necessary
if there is the need to pass a FILE* on to an external C library,

It's not easily possible to share userspace buffers in FILE * with
userspace buffers in rbiot. Userspace buffering is pretty miserable
and error-prone whenever/wherever IPC is concerned.

  1. is for cases like mine where there is the need to operate on raw C data types for performance reasons.

It depends on what you're doing, but if performance is a concern you
should try to work on largish chunks off the file descriptor and
skip the userspace buffering stages. Userspace buffering can improve
performance by reducing syscalls, but it can also double the memory
bandwidth required to do things.

Yes, I would have to do my own buffering during parsing in any case, so
double buffering means unneccesary waste of memory. I guess making a clean
cut and working on the file descriptor directly seems like the best solution.

Still, I am wondering if there is the need for a low-level C API for doing
IO on Ruby IO objects, or is the "clean cut approach" using the file descriptor
directly the recommended solution in any case?

#3 Updated by Eric Wong over 2 years ago

Martin Bosslet Martin.Bosslet@googlemail.com wrote:

Eric Wong wrote:

Martin Bosslet Martin.Bosslet@googlemail.com wrote:

This is related to the proposal in [1].

I'd like to take advantage of streaming IO in an extension I am
working on. The problem I'm having is that I don't want to call
IO#read on the rb_funcall level because that would kill the
performance due to wrapping the bytes into Ruby objects back and
forth again.

Is starting with Ruby String objects (with binary encoding) and then
having read(2)/write(2) hit RSTRING_PTR not possible?

You mean reading String chunks from the underlying IO? I'm afraid not.
The only way I could right now is calling the Ruby methods for
IO#read/write using rb_funcall. But there's a lot of overhead involved,
VM roundtrip plus lots of short-lived objects that trigger GC. It would
likely end up being slower than the current ASN1.decode, a situation I'd
like to avoid.

You can avoid short-lived objects by passing Strings as the second
argument to IO#read-like methods:
`
buf = ""
while r.read(16384, buf)
w.write(buf)
end

Without GC calls happening, I don't expect significant overhead from the VM.

If you're working with sockets/pipes, I would reckon not (Ruby already
defaults to IO#sync=false on sockets/pipes when writing).

Err, typo on my part, IO#sync=true is the default (meaning no userspace
buffering is the default).

Still, I am wondering if there is the need for a low-level C API for doing
IO on Ruby IO objects, or is the "clean cut approach" using the file descriptor
directly the recommended solution in any case?

I recommend directly working off the file descriptor. The only tricky
part is making sure there's nothing in the userspace buffers beforehand.

You'd probably need to call IO#rewind/IO#seek to sync read buffers up
and IO#flush (and then IO#sync=true) for write bufffers.

#4 Updated by Nobuyoshi Nakada about 2 years ago

  • Status changed from Open to Assigned
  • Assignee set to Akira Tanaka

MartinBosslet (Martin Bosslet) wrote:

  1. Duplicating the file descriptor to obtain a pure FILE* like it is done in ext/openssl/ossl_bio.c[2] and continue working on the raw FILE*.

Can't you use rbiostdiofile()?
And OpenSSL seems providing BIO
new_fd() too.

The problem, though, is that only rbiobufwrite is public API in io.h,
rbiobufread is declared private in internal.h and rbcloexecdup is
semi-public in intern.h.

Could we make rbiobufread public API in io.h as well? What about
rbcloexecdup?

It doesn't seem bad to me.

They are all added to internal.h by akr.

#5 Updated by Martin Bosslet about 2 years ago

nobu (Nobuyoshi Nakada) wrote:

Can't you use rbiostdiofile()?
And OpenSSL seems providing BIO
new_fd() too.

True, and that's also what I should be using there :) I'll fix it.

The problem, though, is that only rbiobufwrite is public API in io.h,
rbiobufread is declared private in internal.h and rbcloexecdup is
semi-public in intern.h.

Could we make rbiobufread public API in io.h as well? What about
rbcloexecdup?

It doesn't seem bad to me.

They are all added to internal.h by akr.

I've done a lot of IO in C over the last months, now
I have a much clearer picture of it.

I think my only problem was that there is no unified read/write
that allows working on arbitrary IOs efficiently (I could fall
back to rbfuncall, but then I'd give away the performance benefits).
I had to make the distinction between IOs based on rb
io_t,
between StringIO and raw Strings. I ended up in writing my own
wrapper that would abstract away the differences. It would be
really nice to have such an abstraction directly in the API.
Would this be an option?

#6 Updated by Akira Tanaka over 1 year ago

  • Target version changed from 2.0.0 to next minor

Currently Ruby doesn't provide an abstract layer dedicated for IO and IO-like classes.

Or, in other words, Ruby provide such layer by method dispatching by OOP (polymorphism).

I guess it's difficult to introduce another dispatch mechanism for Ruby.

Making rbiobufread and rbcloexecdup as public API is much easier.

#7 Updated by Martin Bosslet over 1 year ago

In hindsight, my major concern with current IO C API is the impossibility to optimize something for StringIO. Since StringIO is not part of core, a C extension must typically fall back to calling methods of the Ruby IO API, which is not very efficient.

It would be nice to abstract the implementation details away, calling C API methods that take care of the details and efficiency - but I agree, that would be a lot of work. But still, is a revised IO API possible in the future? I think IO-heavy C extensions would almost certainly benefit from it.

#8 Updated by Yui NARUSE over 1 year ago

I heard yugui is planning (or something) such an abstract implementation before.
How's going? > yugui

#9 Updated by Yuki Sonoda over 1 year ago

=begin
The target of the project I am working on is to provide an easy way to write a IO-compatible class in Ruby.
The project does not aim to achieve high performance. So what Martin wants sounds little different from my project.

((URL:http://github.com/yugui/ioable))
=end

#10 Updated by Akira Tanaka about 1 year ago

  • Status changed from Assigned to Feedback

It seems no one design/implement seriously on such an IO framework.

Also available in: Atom PDF