From: ko1@... Date: 2020-11-02T05:41:36+00:00 Subject: [ruby-core:100687] [Ruby master Feature#17298] Ractor's basket communication APIs Issue #17298 has been updated by ko1 (Koichi Sasada). I rewrote copying logic without marshal protocol (https://github.com/ruby/ruby/pull/3728). Additional benchmark: ```ruby Warning[:experimental] = false if ENV['MODE'] == 'share' MODE = :copy SHAREABLE = true else MODE = ENV['MODE'].to_sym SHAREABLE = false end receive2yield = Ractor.new do loop do case MODE when :basket Ractor.yield_basket Ractor.receive_basket when :move Ractor.yield Ractor.receive, move: true when :copy Ractor.yield Ractor.receive else raise end end end to = receive2yield receive2send = Ractor.new to do |to| loop do case MODE when :basket obj = Ractor.receive_basket to.send_basket obj when :move to.send Ractor.receive, move: true when :copy to.send Ractor.receive end end end take2yield = Ractor.new receive2yield do |from| loop do case MODE when :basket Ractor.yield_basket from.take_basket when :move Ractor.yield from.take, move: true when :copy Ractor.yield from.take end end end take2send = Ractor.new take2yield, Ractor.current do |from, to| loop do case MODE when :basket to.send_basket from.take_basket when :move to.send from.take, move: true when :copy to.send from.take end end end AN = ENV['AN'].to_i LN = 10_000 obj = Array.new(AN/10){Array.new{AN}} Ractor.make_shareable(obj) if SHAREABLE LN.times{ receive2send.send obj Ractor.receive } __END__ obj = Array.new(AN){} user system total real share/0 0.000235 0.000000 2.084210 ( 1.244361) basket/0 0.000186 0.000000 2.157927 ( 1.309404) move/0 0.000193 0.000000 2.117845 ( 1.294796) copy/0 0.000164 0.000000 2.324445 ( 1.431408) share/30 0.000113 0.000000 2.086155 ( 1.235768) basket/30 0.000158 0.000000 2.197975 ( 1.335114) move/30 0.000211 0.000000 2.190369 ( 1.352113) copy/30 0.000171 0.000000 2.389695 ( 1.490143) share/60 0.000206 0.000000 1.997523 ( 1.191689) basket/60 0.000160 0.000000 2.237804 ( 1.356830) move/60 0.000145 0.000000 2.268976 ( 1.397970) copy/60 0.000185 0.000000 2.538057 ( 1.580106) share/90 0.000168 0.000000 2.069107 ( 1.224712) basket/90 0.000148 0.000000 2.186683 ( 1.327779) move/90 0.000158 0.000000 2.154334 ( 1.330732) copy/90 0.000139 0.000000 2.504655 ( 1.561725) share/120 0.000189 0.000000 2.120684 ( 1.255453) basket/120 0.000202 0.000000 2.270650 ( 1.375921) move/120 0.000151 0.000000 2.416082 ( 1.491159) copy/120 0.000156 0.000000 2.436406 ( 1.554357) share/150 0.000216 0.000000 2.103629 ( 1.248967) basket/150 0.000219 0.000000 2.323634 ( 1.408301) move/150 0.000187 0.000000 2.123332 ( 1.325794) copy/150 0.000000 0.000205 2.519390 ( 1.592612) share/180 0.000000 0.000149 1.916865 ( 1.143092) basket/180 0.000000 0.000229 2.339701 ( 1.416612) move/180 0.000000 0.000188 2.384908 ( 1.497792) copy/180 0.000000 0.000163 2.503268 ( 1.593776) share/210 0.000000 0.000128 1.987748 ( 1.176819) basket/210 0.000000 0.000167 2.228943 ( 1.353803) move/210 0.000000 0.000177 2.431323 ( 1.519974) copy/210 0.000000 0.000190 2.592614 ( 1.652112) share/240 0.000000 0.000206 2.063813 ( 1.221907) basket/240 0.000000 0.000222 2.266633 ( 1.383860) move/240 0.000000 0.000140 2.291410 ( 1.442910) copy/240 0.000000 0.000129 2.360580 ( 1.519305) share/270 0.000000 0.000200 2.187821 ( 1.292601) basket/270 0.000000 0.000141 2.186951 ( 1.335748) move/270 0.000000 0.000175 2.331970 ( 1.462081) copy/270 0.000000 0.000201 2.544173 ( 1.631321) share/300 0.000000 0.000220 2.103491 ( 1.259986) basket/300 0.000000 0.000208 2.470096 ( 1.533962) move/300 0.000000 0.000177 2.477407 ( 1.575883) copy/300 0.000000 0.000161 2.577555 ( 1.664693) obj = Array.new(AN/10){Array.new{AN}} user system total real share/0 0.000178 0.000000 1.999250 ( 1.192009) basket/0 0.000186 0.000000 2.113732 ( 1.276346) move/0 0.000111 0.000000 2.079669 ( 1.272024) copy/0 0.000261 0.000000 2.301534 ( 1.419984) share/30 0.000141 0.000000 2.133833 ( 1.264703) basket/30 0.000157 0.000000 2.314221 ( 1.400378) move/30 0.000152 0.000000 2.349555 ( 1.461622) copy/30 0.000180 0.000000 2.255883 ( 1.421459) share/60 0.000217 0.000000 2.095241 ( 1.243948) basket/60 0.000206 0.000000 2.239443 ( 1.372667) move/60 0.000129 0.000000 2.477502 ( 1.550481) copy/60 0.000298 0.000000 2.510642 ( 1.610342) share/90 0.000159 0.000000 2.241600 ( 1.323923) basket/90 0.000293 0.000000 2.184617 ( 1.339768) move/90 0.000239 0.000000 2.524579 ( 1.590508) copy/90 0.000157 0.000000 2.610485 ( 1.696873) share/120 0.000130 0.000000 2.164260 ( 1.283117) basket/120 0.000190 0.000000 2.250227 ( 1.383655) move/120 0.000239 0.000000 2.299717 ( 1.459356) copy/120 0.000220 0.000000 2.624845 ( 1.720889) share/150 0.000175 0.000000 2.024881 ( 1.203234) basket/150 0.000214 0.000000 2.213022 ( 1.367973) move/150 0.000230 0.000000 2.351162 ( 1.508806) copy/150 0.000261 0.000000 2.600007 ( 1.729760) share/180 0.000128 0.000000 1.981322 ( 1.176595) basket/180 0.000218 0.000000 2.202996 ( 1.368278) move/180 0.000190 0.000000 2.684911 ( 1.733626) copy/180 0.000176 0.000000 2.877426 ( 1.940715) share/210 0.000162 0.000000 2.096774 ( 1.243471) basket/210 0.000119 0.000000 2.227222 ( 1.387271) move/210 0.000277 0.000000 2.654121 ( 1.725943) copy/210 0.000198 0.000000 2.823281 ( 1.916791) share/240 0.000166 0.000000 2.209040 ( 1.309859) basket/240 0.000169 0.000000 2.367142 ( 1.475902) move/240 0.000144 0.000000 2.600334 ( 1.702996) copy/240 0.000164 0.000000 2.859249 ( 1.956904) share/270 0.000212 0.000000 2.100495 ( 1.244568) basket/270 0.000234 0.000000 2.346845 ( 1.464750) move/270 0.000165 0.000000 2.695428 ( 1.770649) copy/270 0.000184 0.000000 2.934659 ( 2.020429) share/300 0.000169 0.000000 2.145326 ( 1.269930) basket/300 0.000160 0.000000 2.286473 ( 1.426499) move/300 0.000213 0.000000 2.706252 ( 1.789096) copy/300 0.000270 0.000000 3.063131 ( 2.136394) obj = "*" * (100 * AN) user system total real share/0 0.000000 0.000225 2.113924 ( 1.256272) basket/0 0.000000 0.000234 2.103315 ( 1.275444) move/0 0.000000 0.000170 2.270854 ( 1.393821) copy/0 0.000000 0.000160 2.311942 ( 1.424493) share/30 0.000000 0.000209 2.190677 ( 1.290621) basket/30 0.000000 0.000126 2.201340 ( 1.338486) move/30 0.000000 0.000133 2.362480 ( 1.438814) copy/30 0.000000 0.000155 2.564007 ( 1.623138) share/60 0.000000 0.000230 2.045274 ( 1.215421) basket/60 0.000000 0.000175 2.240818 ( 1.358289) move/60 0.000000 0.000230 2.241105 ( 1.366148) copy/60 0.000147 0.000000 2.582220 ( 1.621733) share/90 0.000181 0.000000 2.045982 ( 1.219508) basket/90 0.000152 0.000000 2.259071 ( 1.368956) move/90 0.000229 0.000000 2.215262 ( 1.353770) copy/90 0.000193 0.000000 2.311903 ( 1.465815) share/120 0.000189 0.000000 2.220438 ( 1.309290) basket/120 0.000182 0.000000 2.337383 ( 1.415203) move/120 0.000216 0.000000 2.185714 ( 1.342087) copy/120 0.000174 0.000000 2.547647 ( 1.612203) share/150 0.000175 0.000000 2.111274 ( 1.247957) basket/150 0.000199 0.000000 2.352135 ( 1.436637) move/150 0.000232 0.000000 2.341878 ( 1.440591) copy/150 0.000119 0.000000 2.567323 ( 1.629583) share/180 0.000176 0.000000 2.206774 ( 1.303557) basket/180 0.000153 0.000000 2.123505 ( 1.295564) move/180 0.000272 0.000000 2.295597 ( 1.406772) copy/180 0.000201 0.000000 2.537667 ( 1.620875) share/210 0.000236 0.000000 2.159986 ( 1.278420) basket/210 0.000231 0.000000 2.357100 ( 1.449504) move/210 0.000187 0.000000 2.434216 ( 1.514514) copy/210 0.000273 0.000000 2.581147 ( 1.654714) share/240 0.000220 0.000000 2.187199 ( 1.290690) basket/240 0.000134 0.000000 2.125558 ( 1.308496) move/240 0.000205 0.000000 2.289436 ( 1.411198) copy/240 0.000158 0.000000 2.561948 ( 1.649240) share/270 0.000184 0.000000 1.949823 ( 1.151623) basket/270 0.000181 0.000000 2.305483 ( 1.408048) move/270 0.000202 0.000000 2.097124 ( 1.294308) copy/270 0.000139 0.000000 2.613040 ( 1.683508) share/300 0.000264 0.000000 2.147143 ( 1.270971) basket/300 0.000165 0.000000 2.260574 ( 1.377311) move/300 0.000224 0.000000 2.186270 ( 1.345459) copy/300 0.000280 0.000000 2.640306 ( 1.709781) ``` * Most of case, tuple or something small objects are used to communicate (`[command, param1, param2]` for example) and it is enough fast (on current inefficient implementation). ``` obj = Array.new(AN){} user system total real share/0 0.000235 0.000000 2.084210 ( 1.244361) basket/0 0.000186 0.000000 2.157927 ( 1.309404) move/0 0.000193 0.000000 2.117845 ( 1.294796) copy/0 0.000164 0.000000 2.324445 ( 1.431408) share/30 0.000113 0.000000 2.086155 ( 1.235768) basket/30 0.000158 0.000000 2.197975 ( 1.335114) move/30 0.000211 0.000000 2.190369 ( 1.352113) copy/30 0.000171 0.000000 2.389695 ( 1.490143) obj = Array.new(AN/10){Array.new{AN}} user system total real share/0 0.000178 0.000000 1.999250 ( 1.192009) basket/0 0.000186 0.000000 2.113732 ( 1.276346) move/0 0.000111 0.000000 2.079669 ( 1.272024) copy/0 0.000261 0.000000 2.301534 ( 1.419984) share/30 0.000141 0.000000 2.133833 ( 1.264703) basket/30 0.000157 0.000000 2.314221 ( 1.400378) move/30 0.000152 0.000000 2.349555 ( 1.461622) copy/30 0.000180 0.000000 2.255883 ( 1.421459) obj = "*" * (100 * AN) user system total real share/0 0.000000 0.000225 2.113924 ( 1.256272) basket/0 0.000000 0.000234 2.103315 ( 1.275444) move/0 0.000000 0.000170 2.270854 ( 1.393821) copy/0 0.000000 0.000160 2.311942 ( 1.424493) share/30 0.000000 0.000209 2.190677 ( 1.290621) basket/30 0.000000 0.000126 2.201340 ( 1.338486) move/30 0.000000 0.000133 2.362480 ( 1.438814) copy/30 0.000000 0.000155 2.564007 ( 1.623138) ```` * On nested data structure, "basket" seems fine. ---------------------------------------- Feature #17298: Ractor's basket communication APIs https://bugs.ruby-lang.org/issues/17298#change-88328 * Author: ko1 (Koichi Sasada) * Status: Open * Priority: Normal ---------------------------------------- This ticket proposes `send_basket`/`send_receive`, `yield_basket`/`take_basket` APIs to make effective and flexible bridge ractors. ## Background When we want to send an object as a message, we usually need to copy it. Copying is achieved according to marshal protocol, and the receiver loads it immediately. If we want to make a bridge ractor that receives a message and sends it to another ractor, immediate loading is not effective. ```ruby bridge = Ractor.new do Ractor.yield Ractor.receive end consumer = Ractor.new bridge do |from| obj = from.take do_task(obj) end msg = [1, 2, 3] bridge.send msg ``` In this case, the array (`[1, 2, 3]`) is * (1) dumped at the first `bridge.send msg` * (2) loaded at `Ractor.receive` * (3) dumped again at `Ractor.yield` * (4) loaded at `from.take` Essentially, we only need one dump/load pair, but now it needs two pairs. Mixing "moving" status is more complex. Now there is no way to pass the "moving" status to bridge ractors, so we cannot make a moving bridge. ## Proposal To make more effective and flexible bridge ractors, we propose new basket APIs * `Ractor.receive_basket` * `Ractor#send_basket` * `Ractor.take_basket` * `Ractor.yield_basket` They receive a message, retains the dumped state, and sends it without dumping again. We can rewrite the above example with these APIs. ```ruby bridge = Ractor.new do Ractor.yield_basket Ractor.receive_basket end consumer = Ractor.new bridge do |from| obj = from.take do_task(obj) end msg = [1, 2, 3] bridge.send msg ``` In this case, * (1) dumped at the first `bridge.send msg` * (2) loaded at `from.take` we only need one dump/load pair. ## Implementation https://github.com/ruby/ruby/pull/3725 ## Evaluation The following program makes four types of bridges and passes an array as a message through them. ```ruby USE_BASKET = false receive2yield = Ractor.new do loop do if USE_BASKET Ractor.yield_basket Ractor.receive_basket else Ractor.yield Ractor.receive end end end receive2send = Ractor.new receive2yield do |r| loop do if USE_BASKET r.send_basket Ractor.receive_basket else r.send Ractor.receive end end end take2yield = Ractor.new receive2yield do |from| loop do if USE_BASKET Ractor.yield_basket from.take_basket else Ractor.yield from.take end end end take2send = Ractor.new take2yield, Ractor.current do |from, to| loop do if USE_BASKET to.send_basket from.take_basket else to.send from.take end end end AN = 1_000 LN = 10_000 ary = Array.new(AN) # 1000 LN.times{ receive2send << ary Ractor.receive } # This program passes the message as: # main -> # receive2send -> # receive2yield -> # take2yield -> # take2send -> # main ``` The result is: ``` w/ basket API 0m2.056s w/o basket API 0m5.974s ``` on my machine (=~ x3 faster). (BTW, if we have a TVar, we can change the value `USE_BASKET` dynamically) ## Discussion ### Naming Of course, naming is an issue. Now, I named it "_basket" because the source code uses this terminology. There are other candidates: * container metaphor * package * parcel * box * envelope * packet (maybe bad idea because of confusion of networking) * bundle (maybe bad idea because of confusion of bin/bundle) * "don't touch the content" metaphor * raw * sealed * unopened I like "basket" because I like picnic. ### Feature Now, basket is represented by "Ractor::Basket" and there are no methods. We can add the following feature: * `Ractor::Basket#sender` returns the sending ractor. * `Ractor::Basket#sender = a_ractor` changes the sending ractor. * `Ractor::Basket#value` returns the content. There was another proposal `Ractor.recvfrom`, but we only need these APIs. -- https://bugs.ruby-lang.org/ Unsubscribe: