celluloid / celluloid-io

UNMAINTAINED: See celluloid/celluloid#779 - Evented sockets for Celluloid actors
https://celluloid.io
MIT License
879 stars 93 forks source link

Nonblock without exception #166

Open HoneyryderChuck opened 8 years ago

HoneyryderChuck commented 8 years ago

Related to #139 .

Should boost performance, as exception handling is a quite expensive VM operation.

Left support for ruby 2.0.0, although the ball is on someone else to drop it.

digitalextremist commented 8 years ago

@TiagoCardoso1983 are there benchmarks available? I'm curious to see some

HoneyryderChuck commented 8 years ago

@digitalextremist not for celluloid-io, but maybe @tarcieri has some, as the project README claims some numbers comparing it to eventmachine and node.js.

HoneyryderChuck commented 8 years ago

And then I looked at the benchmarks directory.... :)

with ruby 2.1.6

# master
Calculating -------------------------------------
               spawn       154 i/100ms
               calls       415 i/100ms
         async calls       165 i/100ms
-------------------------------------------------
               spawn     1607.1 (±4.4%) i/s -       8162 in   5.088789s
               calls     4123.4 (±14.7%) i/s -      20335 in   5.027527s
         async calls     8537.5 (±17.2%) i/s -      41415 in   5.012115s

#nonblock without exceptions
Calculating -------------------------------------                       
               spawn       155 i/100ms                                  
               calls       446 i/100ms                                  
         async calls       173 i/100ms                                  
-------------------------------------------------                       
               spawn     1650.4 (±7.1%) i/s -       8215 in   5.004736s 
               calls     4059.4 (±9.4%) i/s -      20516 in   5.094898s 
         async calls     8781.6 (±14.8%) i/s -      42904 in   5.001161s

There is improvement, although I was expecting more.

HoneyryderChuck commented 8 years ago

Then I looked at the benchmark file, and it's not doing any IO. Maybe we better wait for @tarcieri to answer.

tarcieri commented 8 years ago

I think the performance implications are going to vary widely depending on which Ruby VM you're on. Exceptional async I/O used to create a new exception object and mix IO::WaitReadable/WaitWritable into the instance on MRI 1.9.3/2.0, which busts the method cache. This was fixed in later versions, but these are also the versions that support this feature.

I don't have hard numbers.

HoneyryderChuck commented 8 years ago

actually I was wrong, celluloid-io doesn't have any benchmarks, reel does. @digitalextremist , it's your house, right?

digitalextremist commented 8 years ago

The Reel benchmarks are outdated badly if not irrelevant, unfortunately :-1:

ioquatix commented 8 years ago

The traditional expectation that exception handling is expensive is true in many compiled languages since everything else is really fast, but in Ruby that may not hold true.

HoneyryderChuck commented 8 years ago

it is true, and by the reasons stated before. check performance difference between raise/resque and throw/catch, for example. This can be indeed a huge performance boost for IO scenarios.

tarcieri commented 8 years ago

@ioquatix the situation was actually a lot more dire than that in MRI for quite awhile, as the exceptions generated for EAGAIN events in the async I/O APIs mixed a module into the exception instance. In addition to creating a new metaclass each time an exception is thrown (allocating even more memory and increasing GC pressure that much more) this also busts the method cache, and in the versions of MRI that did this MRI only has a single global method cache. This results in the entire application running much slower because the class hierarchy changes each time one of these exceptions is thrown. It's pretty much the same thing I was describing in this blog post:

https://tonyarcieri.com/dci-in-ruby-is-completely-broken

I believe in 2.2 they added special exception classes (IO::EAGAINWaitReadable and IO::EAGAINWaitWritable) which already have IO::WaitReadable and IO::WaitWritable mixed in so that no longer happens (this is exactly what JRuby and Rubinius were already doing). Using exception: false at least avoids allocating objects for the exception and collecting the stack trace (as many more objects).

ioquatix commented 8 years ago

@tarcieri sounds like the whole thing is over engineered up the wazoo, your blog was informative :)

HoneyryderChuck commented 8 years ago

When handled in UDPSocket class, will probably solve https://github.com/celluloid/celluloid-io/issues/156

HoneyryderChuck commented 8 years ago

Just checked the documentation, it seems that the same usage of exceptions doesn't apply there...

HoneyryderChuck commented 8 years ago

Wrong again, apparently support was added in ruby 2.3.0: http://docs.ruby-lang.org/en/2.3.0/UDPSocket.html

HoneyryderChuck commented 8 years ago

is this good to merge? or is the build an artifact coming from celluloid behaving bad?

tarcieri commented 8 years ago

I don't think so. It doesn't look complete. Nowhere are you actually passing :exception => false to a read_nonblock or write_nonblock call...

HoneyryderChuck commented 8 years ago

ups, completely overlooked.

HoneyryderChuck commented 8 years ago

better?

HoneyryderChuck commented 8 years ago

better? It seems that the default for ruby 2.1+ is still exceptionless. Isn't it the purpose? Because I don't see otherwise the condition to be used. Should I test if I'm inside an actor and then switch to exceptionless mode?

tarcieri commented 8 years ago

FWIW, we've received reports of problems in http.rb's timeout implementation which this PR is copypasta'd from, so I think there's probably some more work needed here:

https://github.com/httprb/http/issues/298#issuecomment-173069847

HoneyryderChuck commented 8 years ago

@tarcieri are you sure that it's about the exception handling? The problem in http may arise also from the io/wait API usage in favour of IO.select, as one favours poll() syscall over select(), and maybe the interval handling is different (?). possible guess.

tarcieri commented 8 years ago

@TiagoCardoso1983 we haven't figured out the exact cause yet

HoneyryderChuck commented 8 years ago

@tarcieri I just checked you reverted to using IO.select in http.rb . So, what is the gist? Unusable with timeouts? Funky with fd's other than network sockets? I had complaints on another project where using io/wait calls for IO.pipe fds has a different behaviour than using them with IO.select.

tarcieri commented 8 years ago

@TiagoCardoso1983 see this issue:

https://github.com/httprb/http/issues/298

Switching from IO.select to io/wait has an indeterminate difference with respect to timeout behavior. They are not semantically equivalent.

I've been meaning to dig deeper into this issue but haven't yet. The result was a complete breakage of timeout handling.

HoneyryderChuck commented 8 years ago

I see. See https://github.com/net-ssh/net-ssh/pull/303#issuecomment-192680848 for what I meant with API incompatibility. As I was investigating, io/wait uses poll API in Linux and falls back to select in MacOS and Windows, but I suspect that the calls semantics are different. A bit unfortunate IMO.

tarcieri commented 8 years ago

I'd like to pull an strace-style syscall profile against the two APIs to see what actually differs. Just haven't had the time yet.