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

Ruby가 코드를 해석하는 방법 살펴보기

새로운 Ruby Magic 기사에 오신 것을 환영합니다! 이번에는 Ruby가 코드를 해석하는 방법과 이 지식을 유리하게 사용할 수 있는 방법을 살펴보겠습니다. 이 게시물은 코드가 해석되는 방식과 이것이 더 빠른 코드로 이어지는 방법을 이해하는 데 도움이 될 것입니다.

기호 간의 미묘한 차이

이전 Ruby Magic 기사에서 Ruby에서 문자 이스케이프 처리에 대한 예가 줄 바꿈을 이스케이프 처리하는 방법에 대해 설명했습니다.

아래 예에서 더하기 + 기호 또는 백슬래시 \ .

"foo" +
  "bar"
=> "foobar"
 
# versus
 
"foo" \
  "bar"
=> "foobar"

이 두 가지 예는 비슷해 보이지만 상당히 다르게 동작합니다. 이것들을 읽고 해석하는 방법의 차이점을 알기 위해서는 일반적으로 Ruby 인터프리터에 대한 핵심을 알아야 합니다. 또는 Ruby에게 차이점이 무엇인지 물어볼 수 있습니다.

지시 순서

RubyVM::InstructionSequence 사용 클래스에서 Ruby에게 우리가 제공한 일부 코드를 어떻게 해석하는지 물어볼 수 있습니다. 이 클래스는 Ruby의 내부를 엿볼 수 있는 도구 세트를 제공합니다.

아래 예에서 반환되는 것은 YARV 인터프리터가 이해하는 Ruby 코드입니다.

YARV 인터프리터

YARV(Yet Another Ruby VM)는 Ruby 버전 1.9에 도입된 Ruby 인터프리터로 원래 인터프리터인 MRI(Matz's Ruby Interpreter)를 대체합니다.

인터프리터를 사용하는 언어는 중간 컴파일 단계 없이 코드를 직접 실행합니다. 이것은 Ruby가 C, Rust, Go와 같은 언어를 컴파일하는 최적화된 기계어 프로그램으로 프로그램을 먼저 컴파일하지 않는다는 것을 의미합니다.

Ruby에서 프로그램은 먼저 Ruby VM용 명령어 세트로 변환된 다음 즉시 실행됩니다. 이 지침은 Ruby 코드와 Ruby VM에서 실행 중인 코드 사이의 중간 단계입니다.

이러한 지침을 통해 Ruby VM은 구문별 해석을 처리하지 않고도 Ruby 코드를 더 쉽게 이해할 수 있습니다. 이 지침을 만드는 동안 처리됩니다. 명령 시퀀스는 해석된 코드를 나타내는 최적화된 작업입니다.

Ruby 프로그램이 정상적으로 실행되는 동안에는 이러한 지침이 표시되지 않지만 이를 보면 Ruby가 코드를 올바르게 해석했는지 확인할 수 있습니다. InstructionSequence 사용 YARV가 실행하기 전에 어떤 종류의 명령어를 생성하는지 확인할 수 있습니다.

Ruby 인터프리터를 구성하는 모든 YARV 명령어를 이해할 필요는 없습니다. 대부분의 명령은 스스로 말할 것입니다.

"foo" +
  "bar"
RubyVM::InstructionSequence.compile('"foo" + "bar"').to_a
# ... [:putstring, "foo"], [:putstring, "bar"] ...
 
# versus
 
"foo" \
  "bar"
RubyVM::InstructionSequence.compile('"foo" "bar"').to_a
# ... [:putstring, "foobar"] ...

실제 출력에는 나중에 살펴볼 설정 명령이 조금 더 포함되어 있지만 여기서 "foo" + "bar" 간의 실제 차이점을 볼 수 있습니다. 및 "foo" "bar" .

전자는 두 개의 문자열을 만들어 결합합니다. 후자는 하나의 문자열을 생성합니다. 즉, "foo" "bar" "foo" + "bar"를 사용하여 3개가 아닌 하나의 문자열만 만듭니다. .

  1       2           3
  ↓       ↓           ↓
"foo" + "bar" # => "foobar"

물론 이것은 우리가 사용할 수 있는 가장 기본적인 예일 뿐이지만 Ruby 언어의 작은 세부 사항이 잠재적으로 얼마나 큰 영향을 미칠 수 있는지에 대한 좋은 사용 사례를 보여줍니다.

  • 추가 할당:모든 String 개체가 별도로 할당됩니다.
  • 메모리 사용량 증가:할당된 모든 String 개체가 메모리를 차지합니다.
  • 더 긴 가비지 수집:모든 개체는 수명이 짧더라도 가비지 수집기가 정리하는 데 시간이 걸립니다. 더 많은 할당은 더 긴 가비지 수집 시간을 의미합니다.

분해

또 다른 사용 사례는 논리 문제를 디버깅하는 것입니다. 다음은 큰 결과를 초래할 수 있는 쉬운 실수입니다. 차이점을 발견할 수 있습니까?

1 + 2 * 3
# versus
(1 + 2) * 3

Ruby를 사용하여 이 약간 더 복잡한 예제에서 차이점을 찾을 수 있습니다.

이 코드 예제를 분해하여 Ruby가 수행하는 명령에 대해 더 읽기 쉬운 표를 인쇄하도록 할 수 있습니다.

1 + 2 * 3
# => 7
puts RubyVM::InstructionSequence.compile("1 + 2 * 3").disasm
# == disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
# 0000 trace            1                                               (   1)
# 0002 putobject_OP_INT2FIX_O_1_C_
# 0003 putobject        2
# 0005 putobject        3
# 0007 opt_mult         <callinfo!mid:*, argc:1, ARGS_SIMPLE>
# 0009 opt_plus         <callinfo!mid:+, argc:1, ARGS_SIMPLE>
# 0011 leave
 
# versus
 
(1 + 2) * 3
# => 9
puts RubyVM::InstructionSequence.compile("(1 + 2) * 3").disasm
# == disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
# 0000 trace            1                                               (   1)
# 0002 putobject_OP_INT2FIX_O_1_C_
# 0003 putobject        2
# 0005 opt_plus         <callinfo!mid:+, argc:1, ARGS_SIMPLE>
# 0007 putobject        3
# 0009 opt_mult         <callinfo!mid:*, argc:1, ARGS_SIMPLE>
# 0011 leave

위의 예는 YARV 명령어의 수와 조금 더 관련되어 있지만, 인쇄되고 실행되는 순서에서 한 쌍의 괄호가 만들 수 있는 차이를 알 수 있습니다.

1 + 2 주위의 괄호 사용 수학에서 연산 순서를 위로 이동하여 덧셈이 먼저 수행되도록 합니다.

디스어셈블리 출력 자체에는 실제로 괄호가 표시되지 않고 나머지 코드에 미치는 영향만 표시됩니다.

분해

디스어셈블리 출력은 즉시 이해할 수 없는 많은 것을 인쇄합니다.

인쇄되는 테이블 형식에서 모든 행은 작업 번호로 시작합니다. 그런 다음 작업을 언급하고 마지막으로 작업에 대한 인수를 언급합니다.

지금까지 본 작업의 작은 샘플:

  • trace - 추적을 시작합니다. 자세한 내용은 TracePoint의 문서를 참조하세요.
  • putobject - 스택에 개체를 푸시합니다.
  • putobject_OP_INT2FIX_O_1_C_ - 정수 1 푸시 스택에. 최적화된 운영. (01 최적화되어 있습니다.)
  • putstring - 스택에 문자열을 푸시합니다.
  • opt_plus - 추가 작업(내부 최적화).
  • opt_mult - 곱하기 연산(내부적으로 최적화됨).
  • leave - 현재 코드 컨텍스트를 유지합니다.

이제 Ruby 인터프리터가 개발자에게 친숙하고 읽기 쉬운 Ruby 코드를 YARV 명령어로 변환하는 방법을 알았으므로 이를 사용하여 애플리케이션을 최적화할 수 있습니다.

전체 메소드와 전체 파일을 RubyVM::InstructionSequence에 전달할 수 있습니다. .

puts RubyVM::InstructionSequence.disasm(method(:foo))
puts RubyVM::InstructionSequence.compile_file("/tmp/hello.rb").disasm

일부 코드가 작동하는 이유와 작동하지 않는 이유를 알아보십시오. 특정 기호로 인해 코드가 다른 기호와 다르게 동작하는 이유를 알아보세요. 악마는 세부 사항에 있습니다. Ruby 코드가 앱에서 어떻게 작동하는지, 어떤 식으로든 최적화할 수 있는지 아는 것이 좋습니다.

최적화

인터프리터 수준에서 코드를 보고 최적화하는 것 외에 InstructionSequence를 사용할 수 있습니다. 코드를 더욱 최적화할 수 있습니다.

InstructionSequence 사용 , Ruby의 내장 성능 최적화로 특정 명령어를 최적화할 수 있습니다. 사용 가능한 최적화의 전체 목록은 RubyVM::InstructionSequence.compile_option =에서 확인할 수 있습니다. 방법 문서.

이러한 최적화 중 하나는 꼬리 호출 최적화입니다. .

RubyVM::InstructionSequence.compile 메서드는 다음과 같이 이 최적화를 활성화하는 옵션을 허용합니다.

some_code = <<-EOS
def fact(n, acc=1)
  return acc if n <= 1
  fact(n-1, n*acc)
end
EOS
puts RubyVM::InstructionSequence.compile(some_code, nil, nil, nil, tailcall_optimization: true, trace_instruction: false).disasm
RubyVM::InstructionSequence.compile(some_code, nil, nil, nil, tailcall_optimization: true, trace_instruction: false).eval

RubyVM::InstructionSequence.compile_option =을 사용하여 모든 코드에 대해 이 최적화를 켤 수도 있습니다. . 다른 코드보다 먼저 로드해야 합니다.

RubyVM::InstructionSequence.compile_option = {
  tailcall_optimization: true,
  trace_instruction: false
}

Ruby에서 Tail Call Optimization이 작동하는 방식에 대한 자세한 내용은 Ruby의 Tail Call Optimization 및 Ruby의 Tail Call Optimization:Background 문서를 참조하세요.

결론

RubyVM::InstructionSequence를 사용하여 Ruby가 코드를 해석하는 방법에 대해 자세히 알아보십시오. 코드가 실제로 수행하는 작업을 확인하여 성능을 높일 수 있습니다.

InstructionSequence에 대한 이 소개는 Ruby가 내부적으로 작동하는 방식에 대해 더 많이 배울 수 있는 재미있는 방법일 수도 있습니다. 누가 알아? Ruby의 코드 자체에 대한 작업에 관심이 있을 수도 있습니다.

이것으로 Ruby의 코드 컴파일에 대한 짧은 소개를 마칩니다. 이 기사가 마음에 들었는지, 기사에 대해 질문이 있거나 다음에 읽고 싶은 내용이 있다면 @AppSignal로 알려주시기 바랍니다.