Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

루비스트 메모이제이션 가이드

오늘 저는 성능 향상을 위해 제가 가장 좋아하는 기술 중 하나에 대해 이야기하고 싶었습니다. 이는 결국 합산되고 가끔만 응용 프로그램을 연기가 나는 잔해 더미로 줄이는 작은 성능 향상의 원천입니다. 아주 가끔만.

이 기술을 "메모이제이션"이라고 합니다. 10달러의 컴퓨터 과학 용어에도 불구하고 메서드를 호출할 때마다 동일한 작업을 수행하는 대신 반환 값을 변수에 저장하고 대신 사용한다는 의미입니다.

다음은 의사 코드에서 보이는 것입니다.

def my_method
  @memo = <work> if @memo is undefined
  return @memo
end

Ruby에서 수행하는 방법은 다음과 같습니다. 이것은 가장 강력한 접근 방식이지만 매우 장황합니다. 나중에 논의할 보다 간결한 다른 접근 방식이 있습니다.

class MyClass
  def my_method
    unless defined?(@my_method)
      @my_method = begin 
         # Do your calculation, database query
         # or other long-running thing here. 
      end
    end
    @my_method
  end
end

위의 코드는 세 가지 작업을 수행합니다.

  1. @my_method라는 인스턴스 변수가 있는지 확인합니다. .
  2. 있는 경우 일부 작업을 수행하고 결과를 @my_method에 저장합니다. .
  3. @my_method를 반환합니다.

my_method라는 메서드와 인스턴스 변수가 모두 있다는 사실에 혼동하지 마십시오. . 내 변수의 이름을 무엇이든 지정할 수 있지만 메모화되는 메서드의 이름을 따서 이름을 지정하는 것이 관례입니다.

속기 버전

위 코드의 한 가지 문제는 다소 번거롭다는 것입니다. 이 때문에 거의 동일한 작업을 수행하는 단축 버전을 볼 가능성이 훨씬 더 높습니다.

class MyClass
  def my_method1
    @my_method1 ||= some_long_calculation
  end

  def my_method2
    @my_method2 ||= begin
      # The begin-end block lets you easily 
      # use multiple lines of code here. 
    end
  end
end

둘 다 Ruby의 a ||= b를 사용합니다. 연산자, a || (a = b) , 그 자체가 다음의 약칭입니다:

# You wouldn't use return like this in real life. 
# I'm just using it to express to beginners the idea
# that the conditional evaluates to whatever winds up in `a`.
if a
  return a
else
  a = b
  return a
end

버그 소스

매우 주의를 기울이면 축약형 버전이 메모 변수의 존재 여부를 확인하는 대신 메모 변수의 "진실성"을 평가한다는 사실을 알았을 것입니다. 이것은 속기 버전의 주요 제한 사항 중 하나입니다. nil을 메모하지 않습니다. 또는 false .

이것이 중요하지 않은 사용 사례가 많이 있습니다. 그러나 메모를 할 때마다 항상 마음속에 간직해야 하는 성가신 사실 중 하나입니다.

인수가 있는 메모 작성 방법

지금까지 우리는 단일 값을 메모하는 것만 다루었습니다. 하지만 항상 같은 결과를 반환하는 함수는 많지 않습니다. 기술 인터뷰 중 가장 좋아하는 피보나치 수열을 살펴보겠습니다.

다음과 같이 Ruby에서 피보나치 수열을 재귀적으로 계산할 수 있습니다.

class Fibonacci
  def self.calculate(n)
    return n if n == 0 || n == 1
    calculate(n - 1) + calculate(n - 2)
  end
end

Fibonacci.calculate(10) # => 55

이 구현의 문제는 비효율적이라는 것입니다. 이를 증명하기 위해 print를 추가해 보겠습니다. n 값을 보기 위한 문 .

class Fibonacci
  def self.calculate(n)
    print "#{ n } "
    return n if n == 0 || n == 1
    calculate(n - 1) + calculate(n - 2)
  end
end

Fibonacci.calculate(4)

# Outputs: 4 3 2 1 0 1 2 1 0

보시다시피 calculate n의 동일한 값을 많이 사용하여 반복적으로 호출되고 있습니다. . 실제로 calculate 호출 횟수 n과(와) 함께 기하급수적으로 성장할 것입니다. .

이 문제를 해결하는 한 가지 방법은 calculate의 결과를 메모하는 것입니다. . 그렇게 하는 것은 우리가 다룬 다른 메모이제이션 예제와 크게 다르지 않습니다.

class Fibonacci
  def self.calculate(n)
    @calculate ||= {}
    @calculate[n] ||= begin
      print "#{ n } "
      if n == 0 || n == 1
        n
      else
        calculate(n - 1) + calculate(n - 2)
      end
    end
  end
end

Fibonacci.calculate(4)

# Outputs: 4 3 2 1 0

이제 calculate를 메모했습니다. , 호출 수가 더 이상 n으로 기하급수적으로 증가하지 않습니다. .

Fibonacci.calculate(20)

# Outputs: 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

무효화

메모이제이션은 캐싱과 매우 유사하지만 메모이제된 결과는 자동으로 만료되지 않으며 일반적으로 설정되면 쉽게 지우는 방법이 없습니다.

피보나치 수열 생성기와 같은 사용 사례의 경우 이것은 거의 중요하지 않습니다. Fibonacci.calculate(10) 항상 같은 결과를 반환합니다. 그러나 다른 사용 사례에서는 중요합니다.

예를 들어 다음과 같은 코드를 볼 수 있습니다.

# Not the best idea
class User
  def full_name
    @full_name ||= [first_name, last_name].join(" ")
  end
end

개인적으로 이름이나 성이 변경되면 전체 이름이 업데이트되지 않을 수 있기 때문에 여기서는 메모이제이션을 사용하지 않습니다.

조금 더 여유를 가질 수 있는 곳은 Rails 컨트롤러 내부입니다. 다음과 같은 코드를 보는 것은 매우 일반적입니다.

class ApplicationController
  def current_user
    @current_user ||= User.find(...)
  end
end

각 웹 요청 후에 컨트롤러 인스턴스가 파괴되기 때문에 괜찮습니다. 현재 로그인한 사용자가 정상적인 요청 중에 변경될 가능성은 거의 없습니다.

ActionCable과 같은 스트리밍 연결을 다룰 때는 더 조심해야 할 수도 있습니다. 모르겠어요. 나는 그것을 사용한 적이 없다.

과용

마지막으로 메모이제이션을 너무 멀리 할 수 ​​있다는 점을 지적해야 할 것 같습니다. 메모 변수의 수명 동안 절대 변경되지 않는 값비싼 작업에만 적용해야 하는 기술입니다.