Project

General

Profile

Actions

Feature #14045

closed

Lazy Proc allocation for block parameters

Added by ko1 (Koichi Sasada) over 4 years ago. Updated over 4 years ago.

Status:
Closed
Priority:
Normal
Target version:
-
[ruby-core:83536]

Description

Background

If we need to pass given block, we need to capture by block parameter as a Proc object and pass it parameter as a block argument. Like that:

def block_yield
  yield
end

def block_pass &b
  # do something
  block_yield(&b)
end

There are no way to pass given blocks to other methods without using block parameters.

One problem of this technique is performance. Proc creation is one of heavyweight operation because we need to store all of local variables (represented by Env objects in MRI internal) to heap. If block parameter is declared as one of method parameter, we need to make a new Proc object for the block parameter.

Proposal: Lazy Proc allocation for

To avoid this overhead, I propose lazy Proc creation for block parameters.

Ideas:

  • At the beginning of method, a block parameter is nil
  • If block parameter is accessed, then create a Proc object by given block.
  • If we pass the block parameter to other methods like block_yield(&b) then don't make a Proc, but pass given block information.

We don't optimize b.call type block invocations. If we call block with b.call, then create Proc object.We need to hack more because Proc#call is different from yield statement (especially they can change $SAFE).

Evaluation

def iter_yield
  yield
end

def iter_pass &b
  iter_yield(&b)
end

def iter_yield_bp &b
  yield
end

def iter_call &b
  b.call
end

N = 10_000_000 # 10M

require 'benchmark'
Benchmark.bmbm(10){|x|
  x.report("yield"){
    N.times{
      iter_yield{}
    }
  }
  x.report("yield_bp"){
    N.times{
      iter_yield_bp{}
    }
  }
  x.report("yield_pass"){
    N.times{
      iter_pass{}
    }
  }
  x.report("send_pass"){
    N.times{
      send(:iter_pass){}
    }
  }
  x.report("call"){
    N.times{
      iter_call{}
    }
  }
}

__END__

ruby 2.5.0dev (2017-10-24 trunk 60392) [x86_64-linux]
                 user     system      total        real
yield        0.634891   0.000000   0.634891 (  0.634518)
yield_bp     2.770929   0.000008   2.770937 (  2.769743)
yield_pass   3.047114   0.000000   3.047114 (  3.046895)
send_pass    3.322597   0.000002   3.322599 (  3.323657)
call         3.144668   0.000000   3.144668 (  3.143812)

modified
                 user     system      total        real
yield        0.582620   0.000000   0.582620 (  0.582526)
yield_bp     0.731068   0.000000   0.731068 (  0.730315)
yield_pass   0.926866   0.000000   0.926866 (  0.926902)
send_pass    1.110110   0.000000   1.110110 (  1.109579)
call         2.891364   0.000000   2.891364 (  2.890716)

Related work

To delegate the given block to other methods, Single & block parameter had been proposed (https://bugs.ruby-lang.org/issues/3447#note-18) (using like: def foo(&); bar(&); end). This idea is straightforward to represent block passing. Also we don't need to name a block parameter.

The advantage of this ticket proposal is we don't change any syntax. We can write compatible code for past versions.

Thanks,
Koichi


Related issues 2 (0 open2 closed)

Related to Ruby master - Feature #11256: anonymous block forwardingClosedmatz (Yukihiro Matsumoto)Actions
Related to Ruby master - Feature #14267: Lazy proc allocation introduced in #14045 creates regressionClosedActions
Actions

Also available in: Atom PDF