PyPy의 메타-트레이싱 JIT가 가상 객체를 사용한 이스케이프 분석으로 박싱과 타입 디스패치 오버헤드를 줄이는 방법을 예제를 통해 설명합니다.

동적 언어를 위한 just-in-time 컴파일러의 목표는 해석을 사용하는 언어 구현보다 언어의 속도를 향상시키는 것입니다. 따라서 JIT의 첫 번째 목표는 인터프리테이션 오버헤드, 즉 바이트코드(또는 AST) 디스패치의 오버헤드와 피연산자 스택 등 인터프리터의 자료구조 오버헤드를 제거하는 것입니다. 동적 언어용 JIT가 해결해야 하는 두 번째 중요한 문제는 원시 타입의 박싱과 타입 디스패칭 오버헤드를 어떻게 다룰 것인가입니다. 이러한 문제는 보통 정적 타입 언어에서는 존재하지 않습니다.
원시 타입의 박싱이란 동적 언어가 정수, 부동소수점 등까지 포함한 모든 객체를 사용자 정의 인스턴스와 같은 방식으로 다룰 수 있어야 함을 뜻합니다. 따라서 이러한 원시 타입은 보통 boxed 되며, 즉 실제 값을 담고 있는 작은 힙 구조가 그들을 위해 할당됩니다.
타입 디스패칭은 일반 연산을 수행할 때 현재 다루는 객체들에 적용 가능한 구체적인 구현을 찾는 과정입니다. 예를 들어 두 객체의 덧셈을 생각할 수 있습니다. 덧셈은 더해야 할 구체적인 객체가 무엇인지 확인하고, 그에 맞는 구현을 선택해야 합니다.
작년에 우리는 블로그 글과 PyPy의 메타-JIT 접근 방식이 어떻게 동작하는지에 대한 논문을 썼습니다. 이 글들은 메타-트레이싱 JIT가 어떻게 바이트코드 디스패치의 오버헤드를 제거할 수 있는지 설명합니다. 이번 글에서는, 그리고 아마 후속 글에서도, 우리의 메타-트레이싱 JIT가 만들어낸 트레이스가 어떻게 최적화되어 박싱 오버헤드와 타입 디스패칭처럼 동적 언어와 더 밀접하게 관련된 오버헤드의 일부도 제거하는지를 설명하고자 합니다. 이를 달성하는 가장 중요한 기법은 우리가 가상 객체 라고 부르는 이스케이프 분석의 한 형태입니다. 이것은 예제를 통해 설명하는 것이 가장 좋습니다.
이 블로그 글의 목적을 위해, 우리는 정수와 부동소수점 타입만 지원하는 매우 단순한 객체 모델을 사용할 것입니다. 이 객체들은 두 가지 연산만 지원합니다. 하나는 두 객체를 더하는 add이고, 혼합 덧셈에서는 정수를 부동소수점으로 승격합니다. 다른 하나는 수가 0보다 큰지 여부를 반환하는 is_positive입니다. add의 구현은 고전적인 Smalltalk 스타일의 이중 디스패칭을 사용합니다. 이 클래스들은 RPython으로 작성된 매우 단순한 인터프리터 구현의 일부일 수 있습니다.
class Base(object):
def add(self, other):
""" add self to other """
raise NotImplementedError("abstract base")
def add__int(self, intother):
""" add intother to self, where intother is a Python integer """
raise NotImplementedError("abstract base")
def add__float(self, floatother):
""" add floatother to self, where floatother is a Python float """
raise NotImplementedError("abstract base")
def is_positive(self):
""" returns whether self is positive """
raise NotImplementedError("abstract base")
class BoxedInteger(Base):
def __init__ (self, intval):
self.intval = intval
def add(self, other):
return other.add__int(self.intval)
def add__int(self, intother):
return BoxedInteger(intother + self.intval)
def add__float(self, floatother):
return BoxedFloat(floatother + float(self.intval))
def is_positive(self):
return self.intval > 0
class BoxedFloat(Base):
def __init__ (self, floatval):
self.floatval = floatval
def add(self, other):
return other.add__float(self.floatval)
def add__int(self, intother):
return BoxedFloat(float(intother) + self.floatval)
def add__float(self, floatother):
return BoxedFloat(floatother + self.floatval)
def is_positive(self):
return self.floatval > 0.0
이 클래스들을 사용해 산술을 구현해 보면 동적 언어 구현이 갖는 기본적인 문제가 드러납니다. 모든 숫자는 BoxedInteger 또는 BoxedFloat의 인스턴스이므로 힙 공간을 소비합니다. 많은 산술 연산을 수행하면 많은 가비지가 빠르게 생성되어 가비지 컬렉터에 부담을 줍니다. 숫자 타입 체계를 구현하기 위해 이중 디스패칭을 사용하면 산술 연산당 두 번의 메서드 호출이 필요하며, 이는 메서드 디스패치 때문에 비용이 큽니다.
문제를 더 직접적으로 이해하기 위해, 객체 모델을 사용하는 간단한 함수를 살펴보겠습니다:
def f(y):
res = BoxedInteger(0)
while y.is_positive():
res = res.add(y).add(BoxedInteger(-100))
y = y.add(BoxedInteger(-1))
return res
이 루프는 y번 반복되며, 그 과정에서 어떤 계산을 수행합니다. 이 함수의 실행이 왜 느린지 이해하기 위해, y가 BoxedInteger일 때 트레이싱 JIT가 생성하는 트레이스를 보겠습니다:
# arguments to the trace: p0, p1
# inside f: res.add(y)
guard_class(p1, BoxedInteger)
# inside BoxedInteger.add
i2 = getfield_gc(p1, intval)
guard_class(p0, BoxedInteger)
# inside BoxedInteger.add__int
i3 = getfield_gc(p0, intval)
i4 = int_add(i2, i3)
p5 = new(BoxedInteger)
# inside BoxedInteger.__init__
setfield_gc(p5, i4, intval)
# inside f: BoxedInteger(-100)
p6 = new(BoxedInteger)
# inside BoxedInteger.__init__
setfield_gc(p6, -100, intval)
# inside f: .add(BoxedInteger(-100))
guard_class(p5, BoxedInteger)
# inside BoxedInteger.add
i7 = getfield_gc(p5, intval)
guard_class(p6, BoxedInteger)
# inside BoxedInteger.add__int
i8 = getfield_gc(p6, intval)
i9 = int_add(i7, i8)
p10 = new(BoxedInteger)
# inside BoxedInteger.__init__
setfield_gc(p10, i9, intval)
# inside f: BoxedInteger(-1)
p11 = new(BoxedInteger)
# inside BoxedInteger.__init__
setfield_gc(p11, -1, intval)
# inside f: y.add(BoxedInteger(-1))
guard_class(p0, BoxedInteger)
# inside BoxedInteger.add
i12 = getfield_gc(p0, intval)
guard_class(p11, BoxedInteger)
# inside BoxedInteger.add__int
i13 = getfield_gc(p11, intval)
i14 = int_add(i12, i13)
p15 = new(BoxedInteger)
# inside BoxedInteger.__init__
setfield_gc(p15, i14, intval)
# inside f: y.is_positive()
guard_class(p15, BoxedInteger)
# inside BoxedInteger.is_positive
i16 = getfield_gc(p15, intval)
i17 = int_gt(i16, 0)
# inside f
guard_true(i17)
jump(p15, p10)
(들여쓰기는 트레이스된 함수들의 스택 수준에 해당합니다).
이 트레이스가 비효율적인 이유는 몇 가지가 있습니다. 한 가지 문제는 guard_class 명령을 사용해 주변 객체의 클래스를 반복적이고 중복적으로 확인한다는 점입니다. 게다가 일부 새로운 BoxedInteger 인스턴스는 new 연산으로 생성되지만, 한 번만 사용되고 잠시 뒤 잊혀집니다. 다음 절에서는 이스케이프 분석을 사용해 이것을 어떻게 개선할 수 있는지 보겠습니다.
지난 절에서 보인 코드를 개선하는 핵심 통찰은, 트레이스에서 new 연산으로 생성된 일부 객체가 매우 짧게만 살아남고 할당 직후 곧바로 가비지 컬렉터에 의해 수집된다는 점입니다. 또한 이들은 루프 내부에서만 사용되므로, 프로그램의 다른 어느 곳도 이들에 대한 참조를 저장하지 않는다는 것을 쉽게 증명할 수 있습니다. 따라서 코드를 개선하는 아이디어는 루프를 절대 벗어나지 않는 객체를 분석하고, 그런 객체는 아예 할당하지 않을 수 있게 하는 것입니다.
이 과정을 이스케이프 분석 이라고 합니다. 우리의 트레이싱 JIT에서 이스케이프 분석은 가상 객체 를 사용해 동작합니다. 트레이스를 처음부터 끝까지 순회하면서 new 연산을 볼 때마다 그 연산을 제거하고 가상 객체를 하나 구성합니다. 이 가상 객체는 원래 트레이스의 이 위치에서 할당되던 객체의 형태를 요약하며, 이스케이프 분석이 트레이스를 개선하는 데 사용됩니다. 이 형태는 할당된 객체의 필드에 저장되었을 값들이 어디에서 오는지를 기술합니다. 최적화기가 가상 객체에 기록하는 setfield를 보면, 그 형태 요약을 갱신하고 해당 연산을 제거할 수 있습니다. 최적화기가 가상 객체로부터의 getfield를 만나면, 결과는 가상 객체에서 읽어 오고 그 연산 또한 제거됩니다.
지난 절의 예제에서 다음 연산들은 두 개의 가상 객체를 만들고, 최적화된 트레이스에서는 완전히 제거됩니다:
p5 = new(BoxedInteger)
setfield_gc(p5, i4, intval)
p6 = new(BoxedInteger)
setfield_gc(p6, -100, intval)
p5에 저장된 가상 객체는 자신이 BoxedInteger이며 intval 필드가 i4를 담고 있다는 것을 알고 있습니다. p6에 저장된 가상 객체는 자신의 intval 필드가 상수 -100을 담고 있다는 것을 알고 있습니다.
그다음 p5와 p6를 사용하는 다음 연산들은 그 지식을 이용해 최적화될 수 있습니다:
guard_class(p5, BoxedInteger)
i7 = getfield_gc(p5, intval)
# inside BoxedInteger.add
guard_class(p6, BoxedInteger)
# inside BoxedInteger.add__int
i8 = getfield_gc(p6, intval)
i9 = int_add(i7, i8)
guard_class 연산은 제거될 수 있습니다. p5와 p6의 클래스가 BoxedInteger라는 사실이 이미 알려져 있기 때문입니다. getfield_gc 연산도 제거될 수 있으며 i7과 i8은 단순히 i4와 -100으로 치환됩니다. 따라서 최적화된 트레이스에 남는 유일한 연산은 다음과 같습니다:
i9 = int_add(i4, -100)
트레이스의 나머지 부분도 비슷한 방식으로 최적화됩니다.
지금까지 우리는 가상 객체가 그 필드를 읽고 쓰는 연산에 사용될 때 무슨 일이 일어나는지만 설명했습니다. 가상 객체가 다른 어떤 연산에 사용되면, 더는 가상 상태를 유지할 수 없습니다. 예를 들어 가상 객체가 전역적으로 접근 가능한 위치에 저장된다면, 그 객체는 실제로 할당되어야 합니다. 루프의 한 번의 반복보다 더 오래 살아남게 되기 때문입니다.
위의 트레이스 끝에서 jump 연산을 만났을 때 바로 이런 일이 일어납니다. 이 시점의 jump 인자들은 가상 객체입니다. jump가 방출되기 전에, 그것들은 강제화 됩니다. 이것은 최적화기가 적절한 타입의 새 객체를 할당하고, 가상 객체가 가진 필드 값들로 그 필드들을 설정하는 코드를 생성한다는 뜻입니다. 즉 jump 대신 다음 연산들이 방출됩니다:
p15 = new(BoxedInteger)
setfield_gc(p15, i14, intval)
p10 = new(BoxedInteger)
setfield_gc(p10, i9, intval)
jump(p15, p10)
이 두 인스턴스를 만드는 연산이 트레이스 아래쪽으로 이동했다는 점에 주목하십시오. 이 연산들에 대해서는 실제로 큰 이득이 없어 보일 수도 있습니다. 객체들이 결국 끝에서 여전히 할당되기 때문입니다. 하지만 이런 경우에도 최적화는 여전히 가치가 있었습니다. 강제화된 가상 객체에 대해 수행되던 일부 연산, 즉 몇몇 getfield_gc 연산과 guard_class 연산이 제거되었기 때문입니다.
예제의 최종 최적화된 트레이스는 다음과 같습니다:
# arguments to the trace: p0, p1
guard_class(p1, BoxedInteger)
i2 = getfield_gc(p1, intval)
guard_class(p0, BoxedInteger)
i3 = getfield_gc(p0, intval)
i4 = int_add(i2, i3)
i9 = int_add(i4, -100)
guard_class(p0, BoxedInteger)
i12 = getfield_gc(p0, intval)
i14 = int_add(i12, -1)
i17 = int_gt(i14, 0)
guard_true(i17)
p15 = new(BoxedInteger)
setfield_gc(p15, i14, intval)
p10 = new(BoxedInteger)
setfield_gc(p10, i9, intval)
jump(p15, p10)
최적화된 트레이스에는 원래 다섯 번이었던 할당이 두 번만 남고, guard_class 연산도 원래 일곱 번에서 세 번만 남습니다.
이 블로그 글에서는 하나의 루프 범위 안에서 단순한 이스케이프 분석이 어떻게 동작하는지 설명했습니다. 이 최적화는 인터프리터에서 빠르게 가비지가 되는 많은 중간 자료구조의 할당을 줄여 줍니다. 또한 타입 디스패칭 오버헤드도 상당 부분 제거합니다. 다음 글에서는 이 최적화를 어떻게 더 개선할 수 있는지 설명하겠습니다.
동적 언어를 위한 just-in-time 컴파일러의 목표는 해석을 사용하는 언어 구현보다 언어의 속도를 향상시키는 것입니다. 따라서 JIT의 첫 번째 목표는 인터프리테이션 오버헤드, 즉 바이트코드(또는 AST) 디스패치의 오버헤드와 피연산자 스택 등 인터프리터의 자료구조 오버헤드를 제거하는 것입니다. 동적 언어용 JIT가 해결해야 하는 두 번째 중요한 문제는 원시 타입의 박싱과 타입 디스패칭 오버헤드를 어떻게 다룰 것인가입니다. 이러한 문제는 보통 정적 타입 언어에서는 존재하지 않습니다.
원시 타입의 박싱이란 동적 언어가 정수, 부동소수점 등까지 포함한 모든 객체를 사용자 정의 인스턴스와 같은 방식으로 다룰 수 있어야 함을 뜻합니다. 따라서 이러한 원시 타입은 보통 boxed 되며, 즉 실제 값을 담고 있는 작은 힙 구조가 그들을 위해 할당됩니다.
타입 디스패칭은 일반 연산을 수행할 때 현재 다루는 객체들에 적용 가능한 구체적인 구현을 찾는 과정입니다. 예를 들어 두 객체의 덧셈을 생각할 수 있습니다. 덧셈은 더해야 할 구체적인 객체가 무엇인지 확인하고, 그에 맞는 구현을 선택해야 합니다.
작년에 우리는 블로그 글과 PyPy의 메타-JIT 접근 방식이 어떻게 동작하는지에 대한 논문을 썼습니다. 이 글들은 메타-트레이싱 JIT가 어떻게 바이트코드 디스패치의 오버헤드를 제거할 수 있는지 설명합니다. 이번 글에서는, 그리고 아마 후속 글에서도, 우리의 메타-트레이싱 JIT가 만들어낸 트레이스가 어떻게 최적화되어 박싱 오버헤드와 타입 디스패칭처럼 동적 언어와 더 밀접하게 관련된 오버헤드의 일부도 제거하는지를 설명하고자 합니다. 이를 달성하는 가장 중요한 기법은 우리가 가상 객체 라고 부르는 이스케이프 분석의 한 형태입니다. 이것은 예제를 통해 설명하는 것이 가장 좋습니다.
이 블로그 글의 목적을 위해, 우리는 정수와 부동소수점 타입만 지원하는 매우 단순한 객체 모델을 사용할 것입니다. 이 객체들은 두 가지 연산만 지원합니다. 하나는 두 객체를 더하는 add이고, 혼합 덧셈에서는 정수를 부동소수점으로 승격합니다. 다른 하나는 수가 0보다 큰지 여부를 반환하는 is_positive입니다. add의 구현은 고전적인 Smalltalk 스타일의 이중 디스패칭을 사용합니다. 이 클래스들은 RPython으로 작성된 매우 단순한 인터프리터 구현의 일부일 수 있습니다.
class Base(object):
def add(self, other):
""" add self to other """
raise NotImplementedError("abstract base")
def add__int(self, intother):
""" add intother to self, where intother is a Python integer """
raise NotImplementedError("abstract base")
def add__float(self, floatother):
""" add floatother to self, where floatother is a Python float """
raise NotImplementedError("abstract base")
def is_positive(self):
""" returns whether self is positive """
raise NotImplementedError("abstract base")
class BoxedInteger(Base):
def __init__ (self, intval):
self.intval = intval
def add(self, other):
return other.add__int(self.intval)
def add__int(self, intother):
return BoxedInteger(intother + self.intval)
def add__float(self, floatother):
return BoxedFloat(floatother + float(self.intval))
def is_positive(self):
return self.intval > 0
class BoxedFloat(Base):
def __init__ (self, floatval):
self.floatval = floatval
def add(self, other):
return other.add__float(self.floatval)
def add__int(self, intother):
return BoxedFloat(float(intother) + self.floatval)
def add__float(self, floatother):
return BoxedFloat(floatother + self.floatval)
def is_positive(self):
return self.floatval > 0.0
이 클래스들을 사용해 산술을 구현해 보면 동적 언어 구현이 갖는 기본적인 문제가 드러납니다. 모든 숫자는 BoxedInteger 또는 BoxedFloat의 인스턴스이므로 힙 공간을 소비합니다. 많은 산술 연산을 수행하면 많은 가비지가 빠르게 생성되어 가비지 컬렉터에 부담을 줍니다. 숫자 타입 체계를 구현하기 위해 이중 디스패칭을 사용하면 산술 연산당 두 번의 메서드 호출이 필요하며, 이는 메서드 디스패치 때문에 비용이 큽니다.
문제를 더 직접적으로 이해하기 위해, 객체 모델을 사용하는 간단한 함수를 살펴보겠습니다:
def f(y):
res = BoxedInteger(0)
while y.is_positive():
res = res.add(y).add(BoxedInteger(-100))
y = y.add(BoxedInteger(-1))
return res
이 루프는 y번 반복되며, 그 과정에서 어떤 계산을 수행합니다. 이 함수의 실행이 왜 느린지 이해하기 위해, y가 BoxedInteger일 때 트레이싱 JIT가 생성하는 트레이스를 보겠습니다:
# arguments to the trace: p0, p1
# inside f: res.add(y)
guard_class(p1, BoxedInteger)
# inside BoxedInteger.add
i2 = getfield_gc(p1, intval)
guard_class(p0, BoxedInteger)
# inside BoxedInteger.add__int
i3 = getfield_gc(p0, intval)
i4 = int_add(i2, i3)
p5 = new(BoxedInteger)
# inside BoxedInteger.__init__
setfield_gc(p5, i4, intval)
# inside f: BoxedInteger(-100)
p6 = new(BoxedInteger)
# inside BoxedInteger.__init__
setfield_gc(p6, -100, intval)
# inside f: .add(BoxedInteger(-100))
guard_class(p5, BoxedInteger)
# inside BoxedInteger.add
i7 = getfield_gc(p5, intval)
guard_class(p6, BoxedInteger)
# inside BoxedInteger.add__int
i8 = getfield_gc(p6, intval)
i9 = int_add(i7, i8)
p10 = new(BoxedInteger)
# inside BoxedInteger.__init__
setfield_gc(p10, i9, intval)
# inside f: BoxedInteger(-1)
p11 = new(BoxedInteger)
# inside BoxedInteger.__init__
setfield_gc(p11, -1, intval)
# inside f: y.add(BoxedInteger(-1))
guard_class(p0, BoxedInteger)
# inside BoxedInteger.add
i12 = getfield_gc(p0, intval)
guard_class(p11, BoxedInteger)
# inside BoxedInteger.add__int
i13 = getfield_gc(p11, intval)
i14 = int_add(i12, i13)
p15 = new(BoxedInteger)
# inside BoxedInteger.__init__
setfield_gc(p15, i14, intval)
# inside f: y.is_positive()
guard_class(p15, BoxedInteger)
# inside BoxedInteger.is_positive
i16 = getfield_gc(p15, intval)
i17 = int_gt(i16, 0)
# inside f
guard_true(i17)
jump(p15, p10)
(들여쓰기는 트레이스된 함수들의 스택 수준에 해당합니다).
이 트레이스가 비효율적인 이유는 몇 가지가 있습니다. 한 가지 문제는 guard_class 명령을 사용해 주변 객체의 클래스를 반복적이고 중복적으로 확인한다는 점입니다. 게다가 일부 새로운 BoxedInteger 인스턴스는 new 연산으로 생성되지만, 한 번만 사용되고 잠시 뒤 잊혀집니다. 다음 절에서는 이스케이프 분석을 사용해 이것을 어떻게 개선할 수 있는지 보겠습니다.
지난 절에서 보인 코드를 개선하는 핵심 통찰은, 트레이스에서 new 연산으로 생성된 일부 객체가 매우 짧게만 살아남고 할당 직후 곧바로 가비지 컬렉터에 의해 수집된다는 점입니다. 또한 이들은 루프 내부에서만 사용되므로, 프로그램의 다른 어느 곳도 이들에 대한 참조를 저장하지 않는다는 것을 쉽게 증명할 수 있습니다. 따라서 코드를 개선하는 아이디어는 루프를 절대 벗어나지 않는 객체를 분석하고, 그런 객체는 아예 할당하지 않을 수 있게 하는 것입니다.
이 과정을 이스케이프 분석 이라고 합니다. 우리의 트레이싱 JIT에서 이스케이프 분석은 가상 객체 를 사용해 동작합니다. 트레이스를 처음부터 끝까지 순회하면서 new 연산을 볼 때마다 그 연산을 제거하고 가상 객체를 하나 구성합니다. 이 가상 객체는 원래 트레이스의 이 위치에서 할당되던 객체의 형태를 요약하며, 이스케이프 분석이 트레이스를 개선하는 데 사용됩니다. 이 형태는 할당된 객체의 필드에 저장되었을 값들이 어디에서 오는지를 기술합니다. 최적화기가 가상 객체에 기록하는 setfield를 보면, 그 형태 요약을 갱신하고 해당 연산을 제거할 수 있습니다. 최적화기가 가상 객체로부터의 getfield를 만나면, 결과는 가상 객체에서 읽어 오고 그 연산 또한 제거됩니다.
지난 절의 예제에서 다음 연산들은 두 개의 가상 객체를 만들고, 최적화된 트레이스에서는 완전히 제거됩니다:
p5 = new(BoxedInteger)
setfield_gc(p5, i4, intval)
p6 = new(BoxedInteger)
setfield_gc(p6, -100, intval)
p5에 저장된 가상 객체는 자신이 BoxedInteger이며 intval 필드가 i4를 담고 있다는 것을 알고 있습니다. p6에 저장된 가상 객체는 자신의 intval 필드가 상수 -100을 담고 있다는 것을 알고 있습니다.
그다음 p5와 p6를 사용하는 다음 연산들은 그 지식을 이용해 최적화될 수 있습니다:
guard_class(p5, BoxedInteger)
i7 = getfield_gc(p5, intval)
# inside BoxedInteger.add
guard_class(p6, BoxedInteger)
# inside BoxedInteger.add__int
i8 = getfield_gc(p6, intval)
i9 = int_add(i7, i8)
guard_class 연산은 제거될 수 있습니다. p5와 p6의 클래스가 BoxedInteger라는 사실이 이미 알려져 있기 때문입니다. getfield_gc 연산도 제거될 수 있으며 i7과 i8은 단순히 i4와 -100으로 치환됩니다. 따라서 최적화된 트레이스에 남는 유일한 연산은 다음과 같습니다:
i9 = int_add(i4, -100)
트레이스의 나머지 부분도 비슷한 방식으로 최적화됩니다.
지금까지 우리는 가상 객체가 그 필드를 읽고 쓰는 연산에 사용될 때 무슨 일이 일어나는지만 설명했습니다. 가상 객체가 다른 어떤 연산에 사용되면, 더는 가상 상태를 유지할 수 없습니다. 예를 들어 가상 객체가 전역적으로 접근 가능한 위치에 저장된다면, 그 객체는 실제로 할당되어야 합니다. 루프의 한 번의 반복보다 더 오래 살아남게 되기 때문입니다.
위의 트레이스 끝에서 jump 연산을 만났을 때 바로 이런 일이 일어납니다. 이 시점의 jump 인자들은 가상 객체입니다. jump가 방출되기 전에, 그것들은 강제화 됩니다. 이것은 최적화기가 적절한 타입의 새 객체를 할당하고, 가상 객체가 가진 필드 값들로 그 필드들을 설정하는 코드를 생성한다는 뜻입니다. 즉 jump 대신 다음 연산들이 방출됩니다:
p15 = new(BoxedInteger)
setfield_gc(p15, i14, intval)
p10 = new(BoxedInteger)
setfield_gc(p10, i9, intval)
jump(p15, p10)
이 두 인스턴스를 만드는 연산이 트레이스 아래쪽으로 이동했다는 점에 주목하십시오. 이 연산들에 대해서는 실제로 큰 이득이 없어 보일 수도 있습니다. 객체들이 결국 끝에서 여전히 할당되기 때문입니다. 하지만 이런 경우에도 최적화는 여전히 가치가 있었습니다. 강제화된 가상 객체에 대해 수행되던 일부 연산, 즉 몇몇 getfield_gc 연산과 guard_class 연산이 제거되었기 때문입니다.
예제의 최종 최적화된 트레이스는 다음과 같습니다:
# arguments to the trace: p0, p1
guard_class(p1, BoxedInteger)
i2 = getfield_gc(p1, intval)
guard_class(p0, BoxedInteger)
i3 = getfield_gc(p0, intval)
i4 = int_add(i2, i3)
i9 = int_add(i4, -100)
guard_class(p0, BoxedInteger)
i12 = getfield_gc(p0, intval)
i14 = int_add(i12, -1)
i17 = int_gt(i14, 0)
guard_true(i17)
p15 = new(BoxedInteger)
setfield_gc(p15, i14, intval)
p10 = new(BoxedInteger)
setfield_gc(p10, i9, intval)
jump(p15, p10)
최적화된 트레이스에는 원래 다섯 번이었던 할당이 두 번만 남고, guard_class 연산도 원래 일곱 번에서 세 번만 남습니다.
이 블로그 글에서는 하나의 루프 범위 안에서 단순한 이스케이프 분석이 어떻게 동작하는지 설명했습니다. 이 최적화는 인터프리터에서 빠르게 가비지가 되는 많은 중간 자료구조의 할당을 줄여 줍니다. 또한 타입 디스패칭 오버헤드도 상당 부분 제거합니다. 다음 글에서는 이 최적화를 어떻게 더 개선할 수 있는지 설명하겠습니다.
PyPy의 JIT에서의 이스케이프 분석
Carl Friedrich Bolz-Tereick 작성, 16:33![]()
이메일로 보내기BlogThis!X에 공유Facebook에 공유Pinterest에 공유

Anonymous 님의 말... 아름다운 글이네요. 사람들이 블로그 형식에서 좀 더 '고급' 주제를 과감하게 꺼내는 것을 저는 좋아합니다.
Carl Friedrich Bolz-Tereick 님의 말... 정말 감사합니다 :-).
jdb 님의 말... +1, 감사합니다
구독: 게시물 댓글 (Atom)
Blogger 제공.