programing

루비 : Proc # call vs yield

nasanasas 2020. 10. 22. 08:05
반응형

루비 : Proc # call vs yield


루비의 다음 두 구현 방식의 동작 차이는 무엇입니까 thrice?

module WithYield
  def self.thrice
    3.times { yield }      # yield to the implicit block argument
  end
end

module WithProcCall
  def self.thrice(&block)  # & converts implicit block to an explicit, named Proc
    3.times { block.call } # invoke Proc#call
  end
end

WithYield::thrice { puts "Hello world" }
WithProcCall::thrice { puts "Hello world" }

"행동 적 차이"로 오류 처리, 성능, 도구 지원 등을 포함합니다.


첫 번째 것은 실제로 다른 것의 통사론 적 설탕이라고 생각합니다. 즉, 행동 차이가 없습니다.

두 번째 형식이 허용하는 것은 변수에 블록을 "저장"하는 것입니다. 그런 다음 블록은 콜백이라는 다른 시점에서 호출 될 수 있습니다.


확인. 이번에는 빠른 벤치 마크를 수행했습니다.

require 'benchmark'

class A
  def test
    10.times do
      yield
    end
  end
end

class B
  def test(&block)
    10.times do
      block.call
    end
  end
end

Benchmark.bm do |b|
  b.report do
    a = A.new
    10000.times do
      a.test{ 1 + 1 }
    end
  end

  b.report do
    a = B.new
    10000.times do
      a.test{ 1 + 1 }
    end
  end

  b.report do
    a = A.new
    100000.times do
      a.test{ 1 + 1 }
    end
  end

  b.report do
    a = B.new
    100000.times do
      a.test{ 1 + 1 }
    end
  end

end

결과는 흥미 롭습니다.

      user     system      total        real
  0.090000   0.040000   0.130000 (  0.141529)
  0.180000   0.060000   0.240000 (  0.234289)
  0.950000   0.370000   1.320000 (  1.359902)
  1.810000   0.570000   2.380000 (  2.430991)

이것은 block.call 을 사용 하는 것이 yield를 사용하는 것보다 거의 2 배 느리다 는 것을 보여줍니다 .


다음은 Ruby 2.x 업데이트입니다.

ruby 2.0.0p247 (2013-06-27 개정 41674) [x86_64-darwin12.3.0]

벤치 마크를 수동으로 작성하는 것이 지겨워서 Benchable 이라는 작은 러너 모듈을 만들었습니다.

require 'benchable' # https://gist.github.com/naomik/6012505

class YieldCallProc
  include Benchable

  def initialize
    @count = 10000000    
  end

  def bench_yield
    @count.times { yield }
  end

  def bench_call &block
    @count.times { block.call }
  end

  def bench_proc &block
    @count.times &block
  end

end

YieldCallProc.new.benchmark

산출

                      user     system      total        real
bench_yield       0.930000   0.000000   0.930000 (  0.928682)
bench_call        1.650000   0.000000   1.650000 (  1.652934)
bench_proc        0.570000   0.010000   0.580000 (  0.578605)

여기 가장 놀라운 것은 그 생각 bench_yield보다 느린입니다 bench_proc. 왜 이런 일이 일어나는지 조금 더 이해했으면 좋겠습니다.


블록을 전달하는 것을 잊으면 다른 오류 메시지가 표시됩니다.

> WithYield::thrice
LocalJumpError: no block given
        from (irb):3:in `thrice'
        from (irb):3:in `times'
        from (irb):3:in `thrice'

> WithProcCall::thrice
NoMethodError: undefined method `call' for nil:NilClass
        from (irb):9:in `thrice'
        from (irb):9:in `times'
        from (irb):9:in `thrice'

그러나 "일반"(비 블록) 인수를 전달하려고하면 동일하게 작동합니다.

> WithYield::thrice(42)
ArgumentError: wrong number of arguments (1 for 0)
        from (irb):19:in `thrice'

> WithProcCall::thrice(42)
ArgumentError: wrong number of arguments (1 for 0)
        from (irb):20:in `thrice'

다른 답변은 매우 철저하며 Ruby의 Closures 는 기능적 차이점을 광범위하게 다룹니다. 블록 선택적으로 허용하는 방법에 대해 어떤 방법이 가장 잘 수행 될지 궁금해서 몇 가지 벤치 마크를 작성했습니다 ( 이 Paul Mucur 게시물 참조 ). 세 가지 방법을 비교했습니다.

  • & block in method signature
  • 사용 &Proc.new
  • yield다른 블록으로 감싸기

다음은 코드입니다.

require "benchmark"

def always_yield
  yield
end

def sometimes_block(flag, &block)
  if flag && block
    always_yield &block
  end
end

def sometimes_proc_new(flag)
  if flag && block_given?
    always_yield &Proc.new
  end
end

def sometimes_yield(flag)
  if flag && block_given?
    always_yield { yield }
  end
end

a = b = c = 0
n = 1_000_000
Benchmark.bmbm do |x|
  x.report("no &block") do
    n.times do
      sometimes_block(false) { "won't get used" }
    end
  end
  x.report("no Proc.new") do
    n.times do
      sometimes_proc_new(false) { "won't get used" }
    end
  end
  x.report("no yield") do
    n.times do
      sometimes_yield(false) { "won't get used" }
    end
  end

  x.report("&block") do
    n.times do
      sometimes_block(true) { a += 1 }
    end
  end
  x.report("Proc.new") do
    n.times do
      sometimes_proc_new(true) { b += 1 }
    end
  end
  x.report("yield") do
    n.times do
      sometimes_yield(true) { c += 1 }
    end
  end
end

성능은 Ruby 2.0.0p247과 1.9.3p392 사이에서 비슷했습니다. 1.9.3의 결과는 다음과 같습니다.

                  user     system      total        real
no &block     0.580000   0.030000   0.610000 (  0.609523)
no Proc.new   0.080000   0.000000   0.080000 (  0.076817)
no yield      0.070000   0.000000   0.070000 (  0.077191)
&block        0.660000   0.030000   0.690000 (  0.689446)
Proc.new      0.820000   0.030000   0.850000 (  0.849887)
yield         0.250000   0.000000   0.250000 (  0.249116)

&block항상 사용되지 않을 때 명시 적 매개 변수를 추가하면 실제로 메서드가 느려집니다. 블록이 선택 사항 인 경우 메서드 서명에 추가하지 마십시오. 그리고 블록을 통과 할 yield때 다른 블록으로 감싸는 것이 가장 빠릅니다.

즉, 이것은 백만 번의 반복 결과이므로 너무 걱정하지 마십시오. 한 가지 방법이 백만 분의 1 초의 비용으로 코드를 더 명확하게 만들어 준다면 어쨌든 그것을 사용하십시오.


루비가 블록을 생성하도록 강요하는지 여부 (예 : 기존 proc)에 따라 결과가 다르다는 것을 발견했습니다.

require 'benchmark/ips'

puts "Ruby #{RUBY_VERSION} at #{Time.now}"
puts

firstname = 'soundarapandian'
middlename = 'rathinasamy'
lastname = 'arumugam'

def do_call(&block)
    block.call
end

def do_yield(&block)
    yield
end

def do_yield_without_block
    yield
end

existing_block = proc{}

Benchmark.ips do |x|
    x.report("block.call") do |i|
        buffer = String.new

        while (i -= 1) > 0
            do_call(&existing_block)
        end
    end

    x.report("yield with block") do |i|
        buffer = String.new

        while (i -= 1) > 0
            do_yield(&existing_block)
        end
    end

    x.report("yield") do |i|
        buffer = String.new

        while (i -= 1) > 0
            do_yield_without_block(&existing_block)
        end
    end

    x.compare!
end

결과를 제공합니다.

Ruby 2.3.1 at 2016-11-15 23:55:38 +1300

Warming up --------------------------------------
          block.call   266.502k i/100ms
    yield with block   269.487k i/100ms
               yield   262.597k i/100ms
Calculating -------------------------------------
          block.call      8.271M (± 5.4%) i/s -     41.308M in   5.009898s
    yield with block     11.754M (± 4.8%) i/s -     58.748M in   5.011017s
               yield     16.206M (± 5.6%) i/s -     80.880M in   5.008679s

Comparison:
               yield: 16206091.2 i/s
    yield with block: 11753521.0 i/s - 1.38x  slower
          block.call:  8271283.9 i/s - 1.96x  slower

If you change do_call(&existing_block) to do_call{} you'll find it's about 5x slower in both cases. I think the reason for this should be obvious (because Ruby is forced to construct a Proc for each invocation).


BTW, just to update this to current day using:

ruby 1.9.2p180 (2011-02-18 revision 30909) [x86_64-linux]

On Intel i7 (1.5 years oldish).

user     system      total        real
0.010000   0.000000   0.010000 (  0.015555)
0.030000   0.000000   0.030000 (  0.024416)
0.120000   0.000000   0.120000 (  0.121450)
0.240000   0.000000   0.240000 (  0.239760)

Still 2x slower. Interesting.

참고URL : https://stackoverflow.com/questions/1410160/ruby-proccall-vs-yield

반응형