MSDN Magazine > Home > All Issues > 2007 > October >  CLR Inside Out: IronPython과 동적 언어 런타임
CLR Inside Out
IronPython과 동적 언어 런타임
Bill Chiles

Microsoft® .NET Framework는 CLR(공용 언어 런타임)이라는 단일 런타임을 통해 다양한 종류의 프로그래밍 언어를 지원하도록 설계되었습니다. CLR은 이러한 언어에 가비지 수집, JIT(Just-In-Time) 컴파일, 샌드박스 보안 모델, 도구 통합 지원을 포함한 공유 서비스를 제공합니다. 이러한 기능을 공유할 수 있다는 것은 언어 구현 개발자에게 두 가지 큰 혜택이 됩니다. 우선 저수준 엔지니어링 작업의 많은 부분이 이미 완료되어 있으므로 언어를 구현하기가 쉽습니다. 그리고 CLR에서 실행되는 언어는 매끄럽게 통합됩니다. 라이브러리와 프레임워크를 공유하면 CLR에 새로 추가된 언어에서도 기존의 작업을 활용할 수 있게 됩니다.
CLR에는 또한 IronPython 1.0(www.codeplex.com/ironpython)에서 보여 주듯이 동적 언어에 대한 지원이 포함되어 있습니다. 2006년 8월 "CLR Inside Out"(msdn.microsoft.com/msdnmag/issues/06/10/CLRInsideOut)에서 James Schementi가 설명한 것처럼 동적 프로그래밍 언어는 프로그래머를 위한 강력하고 생산적인 환경을 제공합니다. 이제 Microsoft에서는 동적 언어의 요구에 맞게 특별히 개발된 서비스 집합을 CLR에 추가하는 DLR(동적 언어 런타임)을 개발하고 있습니다. DLR은 공유된 동적 형식 시스템, 표준 호스팅 모델 및 동적 코드와 기호 테이블의 신속한 생성을 위한 지원과 같은 기능을 추가합니다. 이러한 추가 기능 덕분에 수준 높은 .NET용 동적 언어 구현을 구축하기가 훨씬 쉬워졌습니다. 또한 이러한 기능을 통해 DLR 기반 동적 언어가 다른 동적 언어 또는 CLR 기반 정적 언어로 작성된 라이브러리를 공유할 수 있습니다.
DLR을 통해 가장 좋아하는 동적 언어를 위한 최상의 환경을 구축할 수 있습니다. 이것은 언어 코딩 환경을 항상 일정하게 유지할 수 있다는 의미입니다. 또한 탁월한 도구와 뛰어난 성능, 그리고 공유 라이브러리를 활용할 수 있습니다. DLR을 사용한다는 것은 표준 기능을 공유할 수 있다는 의미이므로 필요한 고유 기능을 구현하는 데 집중할 수 있습니다. 예를 들면 가비지 수집, 코드 생성 및 메서드 캐싱 등을 구현할 필요가 없습니다.
DLR에서는 빠른 동적 프로그램을 생성하기 위해 적응 메서드 캐싱을 사용하며 DLR의 모든 언어 구현이 이러한 공유 작업의 혜택을 누릴 수 있습니다. 즉, 클래스 우선 순위 목록을 반복적으로 검색할 필요가 없어지므로 코드 실행 속도가 향상됩니다. 또한 개체에서 호출을 수행할 때마다 .NET 메서드에 대해 오버로드를 확인할 필요도 없어집니다(예: o라는 개체에서 foo라는 메서드 호출).
IronPython 및 DLR의 소스 코드는 Codeplex에서 BSD(Berkeley Software Distribution) 형식의 Microsoft 허용 라이선스(2.0 알파 릴리스 참조)에 따라 제공됩니다. 아직 상당히 초기 단계의 릴리스이며 보강이 필요한 부분이 많이 남아 있습니다. 예를 들어 저수준 메커니즘을 설계하고 성능을 향상시켜야 하며 설명서를 작성해야 합니다. 여러분이 이 기사를 읽을 때쯤이면 IronPython 외에도 IronRuby에 대한 DLR 코드용 Codeplex 프로젝트가 제공될 것입니다. DLR JScript® 및 VBX 구현에 대한 작업도 진행 중이지만 이러한 항목은 이진 형식으로만 제공될 것으로 예상됩니다.

IronPython Hello World
간단한 Python 프로그램(변형 Hello World)을 차례대로 살펴보고 ipy.exe에서 실행될 때 어떤 일이 일어나는지 알아보겠습니다. 여기서 살펴볼 프로그램은 msdnmag.py입니다.
def yo(yourname):
   text = "hello, "
   return text + yourname

print yo("bill")
IronPython 구현의 흥미로운 측면에 대해 더 이야기할 수 있도록 기존 "print hello" 기능 이외에 몇 가지 내용을 더 추가했습니다. 예를 들어 식별자 참조를 위한 이름 바인딩과 yo 함수 내에 "+"의 의미를 캐싱하는 데 대해 설명합니다.
여기에서는 IronPython 2.0 알파 버전을 사용하므로 필자와 함께 진행할 수 있도록 잠시 시간을 내어 이를 다운로드하십시오.
먼저 Visual Studio® 2005에서 ironpython.sln 파일을 열고 디버그 솔루션 구성으로 솔루션을 빌드합니다. IronPythonConsole 프로젝트를 마우스 오른쪽 단추로 클릭하고 왼쪽 탐색 모음에서 디버그 탭을 선택합니다. 이 프로젝트를 시작 프로젝트로 설정합니다(시작 작업 아래에 있는 첫 번째 라디오 단추 집합). 시작 옵션 아래에서 명령줄 인수로 msdnmag.py를 입력합니다. 마지막으로, 솔루션을 빌드할 때 생성된 \bin\debug\ 디렉터리에 앞서 살펴본 msdnmag.py Python 코드를 msdnmag.py라는 파일로 복사합니다.

Ipy.exe 시작
다음은 ipy.exe가 어떻게 시작되는지 살펴보겠습니다. DLR에서는 언어 구현 개발자가 손쉽게 인터프리터를 작동시킬 수 있도록 몇 가지 기본 콘솔 클래스를 제공하고 있습니다. 개발하는 언어에 대한 파서와 런타임 구현을 작성하는 일은 여전히 구현 개발자의 몫이지만 콘솔 클래스로 입력 및 출력, 명령줄 스위치, 그리고 DLR이 구현하는 몇 가지 공유 진단 스위치를 처리할 수 있습니다. 또한 DLR에서 몇 가지 최초 코드를 실행하기 위한 언어 구현 개발자의 작업 부담을 덜고 테스트 도구도 제공합니다.
먼저 <installdir>\Src\IronPython\Compiler\Generation\PythonScriptCompiler.cs를 엽니다. 그런 다음 ParseFile 함수의 "using (Parser parser = ...." 줄에 중단점을 설정합니다.
F5(디버그 | 디버깅 시작) 키를 눌러 프로그램을 실행합니다. Python 실행 엔진을 초기화하는 코드 조각을 구문 분석할 때 처음 중단점에 도달합니다. 코드 조각은 시스템별 초기화를 위해 Python site.py 파일을 로드합니다. 어떻게 이 명령이 실행되는 것일까요? 호출 스택을 살펴보면 ipy.exe가 PythonConsoleHost의 Main 함수에서 시작하는 것을 볼 수 있습니다. 이 함수는 공유된 DLR 편의 제공 코드를 호출합니다. 실행은 다시 IronPython 전용 코드로 돌아와서 명령줄 처리를 수행하여 여기에서 msdnmag.py 파일이 실행됩니다.
PythonConsoleHost 클래스는 매우 작습니다. 여기에는 LanguageProvider(실행 엔진 포함)를 IronPython 구현으로 설정하는 매우 짧은 Initialize 메서드가 있습니다. PythonConsoleHost의 Main 메서드는 간단히 DLR에서 제공하는 기본 ConsoleHost 클래스를 호출합니다.
ConsoleHost는 언어 구현 개발자가 신속하게 cmd.exe 스타일의 대화식 셸을 구축할 수 있도록 해 주는 도우미 클래스입니다. ConsoleHost 클래스는 LanguageProvider로부터 CommandLine 도우미 개체를 받습니다. ConsoleHost가 CommandLine 개체의 Initialize 메서드를 호출하는 CommandLine 개체를 실행할 때 코드는 PythonCommandLine 개체 실행을 시작합니다.
해당 Initialize 메서드 내에서 PythonCommandLine 개체는 모듈 로딩을 위한 Python 경로를 설정하고, 몇 가지 기본 제공 모듈을 구성하며, IronPython의 site.py 파일을 가져옵니다. 이것이 모두 표준 Python 동작입니다. PythonCommandLine은 코드 조각 문자열 "import site.py"에서 IScriptEngine.Execute를 실행하여 site.py 파일을 가져옵니다.
"import site.py" 구문 분석 때문에 PythonScriptCompiler.ParseFile로의 호출에서 처음 중단점에 도달하게 됩니다. msdnmag.py는 아직 실행되고 있지 않으므로 계속 진행합니다.

AST 소개
ipy.exe가 msdnmag.py를 실행할 때 IronPython 구현과 DLR은 파일의 코드를 컴파일하기 위해 함께 작업합니다. 기본적인 수준에서 컴파일러는 코드를 나타내는 데이터 구조체를 생성하는 파서로 시작하는 파이프라인이라 할 수 있습니다. 이 데이터 구조체는 컴파일러 용어로 AST(추상 구문 트리)라고 합니다. 컴파일러는 코드 분석 단계로 들어가면 AST를 변환하거나 새로운 데이터 구조체를 만듭니다. 그런 다음 컴파일러는 결과 구조체를 바탕으로 기계어 코드를 생성하거나 CLR과 같은 가상 시스템을 위한 IL(Intermediate Language) 코드를 생성합니다.
IronPython 코드는 먼저 IronPython 전용 AST를 생성한 다음, 이 트리를 DLR AST로 매핑합니다. 일부 언어에서는 분석을 수행하거나 자체 AST에 대한 도구(코드 편집기)를 지원하기 위한 자체 중간 트리를 가집니다. 도구에는 사용자가 편집기에 입력한 소스 코드와 가까운 트리가 필요합니다. DLR의 AST와 비슷한 AST를 가진 언어는 많지만 DLR AST에는 명시적으로 나타내는 의미에 대한 더 많은 정보가 있습니다. DLR AST를 컴파일러의 이후 단계에서 필요한 다양한 변환 중 하나라고 생각할 수 있습니다.
흥미로운 점은 DLR이 코드를 실행할 때 AST의 일부를 나중에 컴파일한다는 것입니다. 이것은 CLR이 코드를 실행할 때만 JIT 컴파일을 사용하는 것과 비슷합니다. 처음 코드를 실행할 때는 성능이 약간 저하되면 이후 실행 시에는 빠르게 실행됩니다. 조금 뒤에는 특정 코드 줄이 실행되기 전까지 AST 조각이 컴파일되지 않는다는 것을 확인해 보겠습니다.

IronPython AST 살펴보기
프로그램 실행으로 돌아가서 IronPython AST에 대해 살펴보겠습니다. 먼저 F5(디버그 | 계속) 키를 눌러 IronPython 엔진이 site.py의 내용을 실행하기 위해 구문 분석을 시작할 때 다시 중단점에서 중지하도록 하십시오. Visual Studio 지역 창을 보면 "cc"와 "SourceUnit"를 순서대로 확장하고 Name이나 DisplayName을 보면 ParseFile이 처리하고 있는 것이 무엇인지 볼 수 있습니다.
다시 F5 키를 누릅니다. 이 중단점에서 중지하는 것은 마지막입니다. 지금 지역 창을 보면 cc.SourceUnit.DisplayName이 msdnmag.py임을 볼 수 있습니다. 바로 이 파일에 ipy.exe 명령줄 인수에 추가한 파일입니다. 지역 창에서 cc.SourceUnit.Name은 "__main__"입니다. 이것은 Python이 기본 실행 모듈에 이렇게 이름을 지정하기 때문입니다.
이제 F10(디버그 | 프로시저 단위 실행) 키를 3번 눌러 프로시저 단위로 실행합니다.
ast = parser.ParseFileInput()
이제 그림 1과 같이 디버거에서 IronPython AST를 볼 수 있습니다.
그림 1 디버거에 표시된 IronPython AST 
AST 루트 노드는 일련의 문을 나타내는 SuiteStatement이며 여기에 Statements 멤버 한 개가 있음을 볼 수 있습니다. 첫 번째 문(0번 색인)을 확장하면 포함된 FunctionDefinition 노드를 볼 수 있습니다. 이 노드에는 함수 정의 문에 대한 SuiteStatement인 Body 멤버가 있으며 결과 함수 개체에 바인딩될 이름인 Name 멤버가 있습니다.
그림 2에서는 전체 msdnmag.py 파일에 대한 IronPython AST를 일부 세부 사항을 생략하고 개략적으로 보여 줍니다.
그림 2 msdnmag.py에 대한 IronPython 추상 구문 트리 (더 크게 보려면 이미지를 클릭하십시오.)
이 트리에는 시각적 표현이 혼합되어 있습니다. 대부분의 경우 AST 노드에 다른 AST 노드를 가리키는 멤버가 있으면 그림에서는 하위 노드를 가리키는 화살표를 보여 줍니다. 일반적으로 하위 노드의 이름은 부모 노드에 있는 멤버의 형식에 따라 지정됩니다. 예를 들어 FunctionDefinition 노드에는 SuiteStatement 형식의 멤버가 있으며 다음 노드는 SuiteStatement입니다. 노드에 들여쓰기 및 중괄호를 사용한 하위 노드의 설명이 포함되는 경우도 있습니다. 필자는 다이어그램을 간소화하기 위해 이러한 노드를 표시하도록 선택했습니다.

DLR AST 살펴보기
IronPython은 이제 최종 단계의 컴파일을 실행하기 위해 DLR AST를 빌드할 수 있습니다. ParseFile의 중단점 아래에서 IronPython AST를 DLR 트리로 변환하는 BindAndTransform에 대한 호출을 볼 수 있습니다. 이러한 처리를 수행하는 동안 IronPython은 DLR 트리 내의 해당 표현이 식별자가 나타내는 변수에 대한 선언/할당 정보를 나타내도록 모든 식별자를 확인합니다. 물론 일부 식별자의 경우에는 런타임에 값 검색을 나타내는 AST 노드로 컴파일해야 합니다. 예를 들어 식별자가 런타임에 추가되는 모듈 멤버로 확인되거나 모듈의 사전을 통해 호스트에 의해 제공되는 런타임에 바인딩 값으로 확인될 수 있습니다.
이 섹션에는 언어를 DLR로 이식하려는 경우에 관심이 있을 만한 다양한 세부 내용이 있습니다. DLR 트리의 큰 그림을 보려는 경우에는 MethodCallExpression 노드와 ActionExpression 노드가 있습니다. MethodCallExpression 노드는 작업을 수행하는 IronPython 런타임 함수에 대한 호출로 컴파일됩니다. 이 함수는 호출될 때마다 인수로 전달된 동일한 작업을 수행합니다.
ActionExpression 노드는 추상 작업이나 동작을 나타내므로 특히 관심의 대상입니다. 추상 동작을 예로 들면 멤버 가져오기, 개체 인덱싱, 개체 호출, 개체에 더하기 수행 등이 포함됩니다. DLR은 ActionExpression 노드를 동적 호출 사이트로 컴파일합니다. 동적 사이트는 동작과 인수 형식의 특정 조합에 해당하는 메서드를 캐시하는 런타임 메커니즘입니다. 이를 통해서 컴파일 시에 정적 형식 정보가 없거나 적은 상태에서도 동적 코드를 빠르게 실행시킬 수 있습니다. 동적 사이트에 대해서는 조금 뒤에 자세히 설명하겠습니다.
프로그램의 코드를 단계별로 다시 실행해 보겠습니다. Shift+F11(디버그 | 프로시저 나가기)을 두 번 눌러 SourceUnit.Compile에서 중지한 다음, F10(프로시저 단위 실행)을 사용하여 전체 파일에 대한 DLR AST의 루트를 포함할 변수 "block"에 대한 할당을 수행합니다.
이제 DLR AST를 살펴볼 수 있습니다. Visual Studio 지역 창에서 CodeBlock AST 노드인 block이라는 노드를 확장합니다. 이것은 Python 코드의 파일에 대한 루트 노드입니다. 언어의 전용 구문 트리를 DLR의 AST로 변환하는 과정에는 변수가 선언된 위치(명시적 또는 암시적으로)와 식별자가 참조하는 변수를 명확하게 지정하는 것이 포함됩니다. block의 변수 멤버를 확장하면 모듈의 범위에 선언된 두 개의 변수 "__name__" 및 "yo"를 볼 수 있습니다. 변수 개체의 이름 멤버를 보려면 요소 0과 1을 확장하십시오. 첫 변째 변수는 Python에서 요구하고 모듈을 위해 만드는 바인딩된 이름입니다. 두 번째는 함수 정의를 저장하는 모듈 변수입니다.
본문을 세부적으로 살펴보기 전에 그림 3에서 앞서 보았던 IronPython AST에 대한 비슷한 표현을 반영하는 DLR AST의 부분적인 확장을 잠시 살펴보십시오.
그림 3 DLR AST (더 크게 보려면 이미지를 클릭하십시오.)
Body 속성을 확장합니다. Visual Studio 디버거의 표시 특성 때문에 실제로 BlockStatement의 내용을 보려면 본문 노드 아래의 첫 번째 노드를 확장해야 합니다. 이 DLR BlockStatement AST 노드는 IronPython AST의 SuiteStatement 노드에 대응되며 각 노드가 일련의 문을 나타냅니다. 문 속성을 확장합니다.
첫 번째 문(0번 색인)은 EmptyStatement이며, 이것은 IronPython의 DLR 트리로의 변환에 따른 산물입니다. IronPython은 모듈 설명서 문자열이 있는 경우 두 개의 문을 할당하며 이 경우 __doc__에 대한 할당이 생성됩니다. 이 경우에 첫 번째 문은 설명서 문자열이 없기 때문에 자리 표시자 EmptyStatement로 유지됩니다.
두 번째 문(1번 색인)은 흥미롭게도 필자가 작성한 코드인 파일의 나머지 부분에 대한 BlockStatement입니다. 이 BlockStatement에는 ExpressionStatement와 BlockStatement(뒤에 설명)가 포함되어 있습니다. ExpressionStatement 노드는 스택으로부터 할당의 결과 값을 꺼내올 수 있도록 BoundAssignment 노드를 래핑합니다. Python 할당은 값을 반환하지 않습니다. BoundAssignment에는 이름과 값 멤버가 있습니다. 이들은 지역 변수 yo(Python 코드가 실행되는 모델에 대해 지역)를 함수 호출의 결과로 설정하는 과정을 나타냅니다. MethodCallExpression이 나타내는 함수 호출은 IronPython 함수 개체를 생성하는 런타임 도우미 함수를 호출합니다. MethodCallExpression에는 호출이 MakeFunction라는 이름의 메서드에 대한 것이며 호출이 받는 인수를 나타내는 멤버가 있습니다. 한 인수는 함수 본문에 대한 CodeBlockExpression AST 노드이며, 다른 인수는 함수에서 받을 생성된 매개 변수의 배열입니다.
BoundAssignment의 값 멤버인 MethodCallExpression AST 노드를 확장합니다. Arguments 속성의 세 번째 요소(2번 색인)을 보면 yo의 본문에 대한 CodeBlockExpression을 볼 수 있습니다. 여기에는 CodeBlock인 Block 속성이 있습니다. CodeBlockExpression은 CodeBlock이 이를 모듈 변수에 할당할 수 있는 호출 가능 값으로 변경하도록 합니다. yo 함수의 AST로 들어가기에 앞서 msdnmag.py에 대한 전체 DLR AST 다이어그램인 그림 4를 살펴보겠습니다.
그림 4 msdnmag.py에 대한 전체 DLR AST (더 크게 보려면 이미지를 클릭하십시오.)
다이어그램에 대해 몇 가지 설명할 내용이 있습니다. 앞서 살펴본 다이어그램과 마찬가지로 트리에 시각적 표현이 혼합되어 있습니다. 대부분의 경우 AST 노드에 다른 AST 노드를 가리키는 멤버가 있으면 그림에서는 하위 노드를 가리키는 화살표를 보여 줍니다. 노드에 추가 화살표와 상자 대신 들여쓰기 및 중괄호를 사용한 하위 노드의 설명이 포함되는 경우도 있습니다. Visual Studio의 지역 창 스냅샷은 다이어그램 왼쪽에서 MethodCallExpression 노드까지 AST를 보여 줍니다. 첫 번째 화살표는 그림에서 BlockStatement를 EmptyExpression으로 생략했기 때문에 점선으로 표시됩니다. 지역 창에서 데이터 구조체를 확장하여 다이어그램에서 표시되는 것처럼 트리의 전체 구조를 볼 수 있습니다.
yo 함수의 본문을 나타내는 CodeBlock 노드에는 변수, 매개 변수 및 AST 본문에 대한 멤버가 있습니다. 이것은 전체 msdnmag.py 파일을 나타내는 CodeBlock과 비슷합니다. 나중에 살펴보겠지만 파일에 대한 CodeBlock은 모듈에 대한 Initialize 메서드로 변환됩니다. yo 함수에는 text라는 한 개의 지역 변수가 있습니다. 이것을 보려면 변수 멤버, 요소 0을 확장한 다음 Variable 개체를 확장하면 됩니다. 비슷한 확장 과정을 따라 yo 함수에 yourname이라는 한 개의 매개 변수가 있음을 볼 수 있습니다.
CodeBlock의 본문은 BlockStatement이며 여기에는 할당과 반환을 위한 두 개의 문이 있습니다. yo에 대한 할당에서 보았듯이 BlockStatement에는 먼저 BoundAssignment를 보관하는 ExpressionStatement가 있습니다. 이 할당은 텍스트를 "hello, "로 설정하는 것을 나타냅니다. BoundAssignment 개체에는 ConstantExpression 값 멤버와 변수 멤버가 있습니다. Variable 개체는 텍스트를 지역 변수로 설명하며 지역 변수의 범위로서 다시 CodeBlock을 가리키는 블록 멤버가 있습니다.
BlockStatement의 두 번째 문은 식 멤버가 ActionExpression인 ReturnStatement입니다. 앞서 설명했듯이 이 AST 노드는 특히 흥미롭습니다. 이 ActionExpression 노드에는 DoOperationAction인 동작 멤버가 있으며 이 예에서는 Add 작업을 나타냅니다. ActionExpression에는 BoundExpression 개체인 요소 두 개가 있는 인수 멤버가 있습니다. 첫 번째 BoundExpression에는 "hello, "로 설정했었던 지역 텍스트를 나타내는 변수 멤버가 있습니다. 두 번째는 매개 변수 yourname을 나타내는 변수 멤버가 있습니다.
이제 ExpressionStatment 두 개를 포함하는 BlockStatement까지 트리를 거슬러 올라가 보겠습니다. 첫 번째는 함수 개체에 대한 yo의 BoundAssignment를 위한 것입니다. 두 번째 ExpressionStatement 역시 식의 값을 스택에서 꺼내는 코드를 컴파일러가 내보내기 위해 존재합니다. ExpressionStatement에는 MethodCallExpression인 식 멤버가 있습니다.
MethodCallExpression은 print 문을 구현하는 IronPython의 런타임 함수에 대한 호출을 나타냅니다. 여기에서 print를 설명하는 .NET RuntimeMethodInfo인 메서드 멤버가 있습니다. MethodCallExpression에는 ActionExpression인 인수 멤버가 있습니다. 이는 위에서 설명한 대로 빠른 호출을 위해 동적 사이트로 컴파일됩니다. ActionExpression에는 yo에 대한 호출을 위한 CallAction인 동작 멤버가 있습니다. 이 노드에는 또한 두 개의 요소가 있는 인수 멤버가 있습니다. 한 요소는 식이 지역 변수 yo(yo는 정의된 모듈에 대해 지역임)의 값이라는 것을 나타내는 변수 멤버가 있는 BoundExpression입니다. 다른 요소는 문자열 "bill"을 보관하는 값 멤버가 있는 ConstantExpression입니다.
이것으로 DLR AST에 대한 내용이 모두 끝났습니다. IronPython 트리가 어떻게 직접 Python 코드를 나타내는지 알 수 있을 것입니다. DLR AST에는 더 명시적인 정보가 포함되어 있습니다. 식별자 바인딩 정보와 범위를 나타내기 위한 명시적 블록이 포함되어 있으며, 동적 사이트로 컴파일되는 동작이 아니라 런타임 메서드 호출의 명시적인 표현이 포함되어 있습니다. DLR AST에는 또한 코드가 Python 의미 체계를 유지할 수 있도록 식의 결과를 스택에서 꺼내도록 하는 노드가 포함되어 있습니다.

생성된 코드 보기
프로세스가 시작되고 구문 분석을 수행하는 방법을 살펴보았으므로 다음은 생성된 코드를 살펴볼 차례입니다. 이를 위한 가장 쉬운 방법은 ipy.exe의 스위치와 Lutz Roeder의 Reflector 프로그램을 사용하는 것입니다. 스위치를 사용하면 ipy.exe가 정적 코드(경량 코드 생성이나 동적 메서드와는 달리)를 생성하고 이를 디스크에 저장하도록 할 수 있습니다. aisto.com/roeder/dotnet/에서 Reflector를 다운로드한 다음 ironpython.sln을 빌드한 \bin\debug\ 디렉터리에 있는 동안 다음 명령줄을 호출하십시오.
ipy.exe -D -X:SaveAssemblies -X:StaticMethods msdnmag.py
그러면 이 디렉터리에서 snippets1.dll과 msdnmag.exe의 두 파일을 볼 수 있습니다. 이제 필요에 따라 절대 경로를 추가하고 다음 명령줄을 사용하여 이러한 파일을 대상으로 Reflector를 실행합니다.
C:\where\you\put\reflector.exe snippets1.dll, msdnmag.py
DLL과 EXE의 목록에서 msdn과 snippets1을 찾아볼 수 있습니다. msdnmag, msdnmag.exe, "{} -"를 순서대로 확장하고 마지막으로 "__main__$mod_2"를 확장합니다. 다음은 Initialize 메서드를 두 번 클릭합니다. 이것은 msdnmag.py 파일의 본문입니다. DLR이 생성한 IL로부터 디스어셈블한 소스 코드를 볼 수 있습니다. 이전에 Reflector를 사용해 보았다면 뛰어난 도구라는 것을 알고 있겠지만 몇 가지 단점도 있습니다. 예를 들어 극히 일시적인 변수를 생성할 수 있기 때문에 C# 코드를 볼 때 정보가 충분하지 않습니다.
Initialize 함수에 대해 자세히 설명하지는 않겠습니다. 그러나 AST에서 보았던 것처럼 yo가 MakeFunction에 대한 호출의 결과로 설정되는 것을 볼 수 있습니다. 그런 다음 Initialize가 yo 호출의 결과에 대해 런타임 도우미 함수 PythonOps.Print를 호출하는 것을 볼 수 있습니다. "Call-Simple-1.Invoke" 부분을 확인하십시오. Call-Simple-1 개체는 메서드 캐싱 메커니즘의 일부입니다. 메서드 캐싱에 대해서는 yo 함수에서 어떻게 나타나는지를 살펴볼 때 다시 설명하겠습니다.
yo 함수를 살펴보겠습니다. 왼쪽 창에서 yo$1을 두 번 클릭합니다. 함수가 텍스트를 "hello,"로 설정하는 부분과 "DoOperation-Add-0.Invoke(text, yourname)"의 결과를 반환하는 부분을 볼 수 있습니다. 바로 이것이 필자가 Python으로 작성한 코드의 핵심입니다. 잠시 뒤에는 메서드 조회 결과를 캐시하는 개체인 DoOperation-Add-0에 초점을 맞춰 보겠습니다. 또한 DoOperation-Add-0에 대해 집중적으로 설명하는 동안 snippets1에 있는 몇 가지 런타임 생성 함수에 대해서도 살펴보겠습니다.

동적 사이트의 이해
동적 사이트는 동적 언어 코드가 더 빨리 실행되도록 하기 위한 기술이며, 특정 형식의 개체에 대한 특정 작업을 위한 메서드 캐싱을 관리합니다. DLR에서 사용하는 동적 사이트 메커니즘은 검증된 동적 언어 구현으로 연구와 경험에 바탕을 둔 것입니다. 생성된 코드에서 Call-Simple-1 및 DoOperation-Add-0와 같은 개체를 볼 수 있으며 이러한 항목은 DynamicSite<Targ1, Targ2, Tresult>에서 파생된 형식의 인스턴스입니다. 캐싱 이론에 대해서는 여기에서 간단하게 설명하겠지만 "동적 언어 메서드 캐싱"이나 "다형성 인라인 메서드 캐싱"에 대한 자세한 내용은 웹에서 찾아보십시오.
동적 언어의 성능은 각 호출 사이트에서 발생하는 추가 검사와 검색 때문에 저하됩니다. 간단한 구현이라면 반복적으로 멤버를 위해 클래스 우선 순위 목록을 검색해야 하며 특정 코드 줄을 실행할 때마다 메서드 인수 형식에 대한 오버로드를 확인해야 할 수도 있습니다. o.m(x, y) 또는 x + y와 같은 식이 있다고 할 때 동적 언어는 개체 o가 정확히 어떤 종류이고 o에 바인딩된 m은 무엇이며, x의 형식은 무엇이고, y의 형식은 무엇이며, x와 y의 실제 런타임 형식에 대해 "+"가 의미하는 것은 무엇인지 검사해야 합니다. 형식이 정적으로 지정되는 언어(또는 코드에 충분한 형식 힌트 및 형식 유추가 있는 경우)에서는 각 호출 사이트에 적당한 명령이나 런타임 함수 호출을 정확하게 내보낼 수 있습니다. 이것이 가능한 것은 컴파일 시에 무엇이 필요한지 정적인 형식에서 알 수 있기 때문입니다.
동적 언어는 동적 기능 덕분에 탁월한 생산성 향상과 강력하고 간결한 식을 제공합니다. 그러나 실제 코드에서는 매번 동일한 개체 형식에 대해 실행되는 경향이 있습니다. 이것은 코드 섹션이 처음 실행될 때 메서드 검색의 결과를 기억해두면 성능을 향상시킬 수 있음을 의미합니다. 예를 들어 x + y의 경우 x와 y가 정수인 경우 식이 처음으로 실행될 때 코드 시퀀스를 기억하거나 정확히 어떤 런타임 함수가 두 정수의 덧셈을 수행하는지 기억할 수 있습니다. 그런 다음에는 식이 실행될 때마다 검사를 수행할 필요가 없습니다. 코드는 x와 y가 정수인지를 다시 확인하고 검색을 수행할 필요 없이 올바른 코드로 발송하면 됩니다. 작업의 의미 체계와 사용된 메서드 캐싱 메커니즘에 따라서는 결과적으로 두 번의 형식 검사와 덧셈 명령을 포함하는 인라인 코드 생성으로 완료될 수도 있습니다.

DynamicSite 개체 작동 방식
정적 언어 컴파일러는 덧셈, 멤버 가져오기 및 인덱싱과 같은 작업을 수행하기 위해 컴파일 시에 메서드를 선택하거나 특정 코드를 내보낼 수 있습니다. DLR이 컴파일 시에 무엇을 내보낼지 항상 알 수는 없기 때문에 DynamicSite 개체에 대한 Invoke 메서드 호출을 내보냅니다. 이 개체는 작업과 캐싱 논리를 포함하는 대리자(Invoke에서 호출)를 캡처합니다. 이 캐싱 논리는 대리자가 런타임에 인수 형식의 새로운 조합을 발견할 때마다 업데이트됩니다. 대리자는 실제로 함수에 대한 포인터를 래핑하지만 여기에서는 간결함을 위해 대리자가 함수인 것처럼 사용했습니다. 여기까지가 대략적인 이야기이며 자세한 내용은 곧 설명하겠습니다.
대리자는 UpdateBindingAndInvoke에 대한 호출로 시작됩니다. 호출 사이트가 처음 실행될 때(x + y만 고려), UpdateBindingAndInvoke는 x의 형식과 y의 형식을 확인하기 위해 Add 작업 구현에 인수를 쿼리합니다. 결과를 얻으면 x와 y의 형식이 방금 확인한 형식인지 확인하는 작업을 인코딩하는 새 대리자를 생성합니다. 이후 호출에서 x와 y가 같은 형식이면 새 대리자 코드가 현재 가진 구현을 호출합니다. 이후 호출에서 x나 y가 다른 형식이면 대리자가 모든 캐시 검사에 실패하며 다시 UpdateBindingAndInvoke를 호출하여 새 대리자를 생성합니다. 최신의 대리자 코드는 x와 y의 첫 번째 테스트와 함께 새 테스트를 캡처하며 형식과 일치하는 대상 구현을 호출합니다.
다음은 DLR이 msdnmag.py의 코드를 실행하면서 생성한 DoOperation-Add-0 대리자에 대해 살펴볼 차례입니다. Reflector로 돌아가서 이번에는 snippets1, snippets.dll, "{} -"를 순서대로 확장한 다음 왼쪽 창에서 "Type$_stub_$4" 노드를 확장합니다. Handle 메서드를 클릭하면 오른쪽 창에서 그림 5와 같은 코드를 볼 수 있습니다.
public static object Handle(object[], 
    FastDynamicSite<object, object, object> site1,
    object obj1, object obj2)
{
    if (((obj1 != null) && (obj1.GetType() == typeof(string))) 
        && ((obj2 != null) && (obj2.GetType() == typeof(string))))
    {
        string text;
        string text1 = text = 
            StringOps.Add(Converter.ConvertToString(obj1),
                          Converter.ConvertToString(obj2));
        return text;
     }
     return site1.UpdateBindingAndInvoke(obj1, obj2);
 }
이것인 DLR이 "text + yourname"을 실행한 다음 DoOperation-Add-0 대리자를 위해 생성한 IL로부터 디스어셈블한 코드입니다. yo 함수를 다시 호출하면 이 대리자가 실행됩니다. 이 코드에는 약간의 성능 문제가 있으며 이에 대해서는 조금 뒤에 다루겠습니다. 코드는 두 인수가 문자열인지 테스트하고 두 문자열을 지정하고 덧셈을 수행하는 IronPython 런타임 구현을 호출한다는 것을 확인할 수 있습니다.
인수 중 하나가 문자열이 아닌 경우 대리자는 UpdateBindingAndInvoke를 호출하여 새 대리자를 생성합니다. 새 대리자는 현재 대리자(위)를 대체하며 따라서 다음 DoOperation-Add-0.Invoke가 실행될 때는 업데이트된 대리자를 호출합니다. 필자는 yo에 대한 두 번째 호출에서 "text + yourname"이 두 정수에 대해 실행되도록 msdnmag.py를 수정했습니다. 그런 다음 두 번째 호출 이후에 DLR이 생성한 두 번째 대리자를 볼 수 있었는데 그림 6에서 이를 확인할 수 있습니다.
      public static object Handle(object[]
    FastDynamicSite<object, object, object> site1, 
    object obj1, object obj2)
{
    if (((obj1 != null) && (obj1.GetType() == typeof(int))) 
        && ((obj2 != null) && (obj2.GetType() == typeof(int))))
    {
        return Int32Ops.Add(Converter.ConvertToInt32(obj1), 
                            Converter.ConvertToInt32(obj3));
    }
    if (((obj1 != null) && (obj1.GetType() == typeof(string))) 
        && ((obj2 != null) && (obj2.GetType() == typeof(string))))
    {
        return = StringOps.Add(Converter.ConvertToString(obj1),
                               Converter.ConvertToString(obj2));
    }
    return site1.UpdateBindingAndInvoke(obj1, obj3);
  }
      
필자는 Reflector가 삽입한 가짜 임시 변수를 정리했습니다. 필자와 같은 추가 실험을 직접 수행하면 약간 다른 코드를 얻게 될 것입니다. 이제 코드에서 인수가 정수인지 테스트하는 것을 볼 수 있습니다. 인수가 정수인 경우 코드는 정수에 대해 Add를 구현하는 런타임 도우미 함수를 호출합니다. 이전의 동일한 코드는 정수 테스트와 대상 코드를 따르고 있습니다. 필자는 가장 최근 형식을 먼저 테스트하도록 순서를 변경했습니다. 프로그램 실행에 프로파일을 적용하면 올바른 인수의 테스트가 최적의 순서로 생성될 것임을 예상할 수 있습니다. 물론 인수가 이전에 없었던 새로운 형식의 조합인 경우 대리자는 UpdateBindingAndInvoke를 호출해야 합니다.
코드와 관련된 성능 문제를 눈치챘을 수도 있을 것입니다. DLR은 지금이라도 더 나은 코드를 생성할 수 있지만 알파 버전에서는 DLR 기능과 코드 생성 간에 균형을 맞추었습니다. 코드가 형식과 문자열에 대해 테스트한 다음 인수를 문자열로 변환하는 것을 알 수 있을 것입니다. 이와 관련된 두 가지 문제가 있습니다. 첫 번째 문제는 .NET Framework 2.0 JIT가 "if" 테스트로부터 이어지는 코드 블록으로 형식 정보를 전달하지 않는다는 것입니다. Add 구현은 정적으로 형식이 지정된 메서드이므로 인수는 이에 대한 문자열로 지정해야 합니다. 두 번째 문제는 DLR 생성 코드는 런타임 변환 함수에 대한 느린 호출을 수행한다는 것입니다. DLR이 여기에서 캐스트 작업을 내보냈을 수 있습니다.
마지막으로 언급할 성능 문제는 현재 DLR은 동일한 작업을 반복하지 않도록 테스트를 구성하지 않는다는 것입니다. 두 정수나 두 문자열을 빠르게 더하는 방법을 아는 필자의 업데이트된 대리자에서 필자는 인수가 null인지 테스트합니다. 이 테스트는 여러 번 수행할 필요가 없는데도 불구하고 여러 번 수행되고 있습니다. UpdateBindingAndInvoke에서 작업 구현을 검색할 때 실제로는 개체에 작업을 수행하는 방법을 묻는 것이며 DynamicSite에서 나타내고 있는 작업을 수행하는 방법에 대한 규칙을 제공하도록 개체를 호출합니다. 규칙에는 수행할 테스트에 대한 AST와 테스트가 참일 경우 실행할 대상 코드에 대한 AST가 포함됩니다. DLR은 테스트 간에 트리 변환을 수행하여 대리자 코드를 더욱 최적화할 수 있습니다.
마지막으로, 개체의 멤버 가져오기에 동적 사이트를 사용했기 때문에 인스턴스 및 동적 형식에 대한 변경을 처리해야 합니다. DLR은 o.m()의 경우 o가 같은 개체를 가리킬 때 어떤 m을 호출해야 하는지 가정할 수가 없습니다. 일부 언어에서는 런타임에 형식을 변경하고, 멤버를 대체하며 근본적으로 인스턴스의 멤버를 다시 정의할 수 있도록 허용하고 있습니다. DLR은 새 형식과 형식 변경의 경우 증가하는 전역 버전의 카운터를 사용합니다. 멤버 변경과 같이 형식의 버전이 변경되도록 하는 다양한 요인이 있습니다. 해당 형식의 버전이 변경된 개체를 참조하는 모든 DynamicSite 캐시는 올바른 캐시 동작으로 이어지지 않습니다. 그러면 DynamicSite는 다시 검색하고 대리자를 업데이트해야 합니다.

큰 대가
생성된 DynamicSite 형식은 제네릭 형식입니다. msdnmag.py의 Python 예에서는 제네릭 형식을 사용하더라도 그다지 도움이 되지는 않습니다. 그러나 "if x < 1:"와 같은 식의 경우에는 DynamicSite<Object, Int, Bool>을 생성할 수 있습니다. 사이트의 Invoke 메서드는 한 인수를 정수로 받고 부울을 반환하는 정적인 형식으로 지정됩니다. 동적 사이트 내부의 대리자 역시 이와 같은 방식으로 정적인 형식으로 지정되므로 결과 코드가 빠르게 실행됩니다. 대리자는 자체 캐싱에서 한 인수만 테스트하면 되며, 스택이 되감기는 동안 JIT는 부울 값이 올바른 위치로 이동할 것임을 알게 됩니다. 이를 통해서 if 테스트 주변의 캐스트나 검사가 필요 없게 됩니다.
일부 동적 언어는 선택적인 명시적 형식 지정을 가지고 있습니다. 그렇지 않은 경우에도 구현은 .NET 정적 메서드에 대한 호출, 리터럴 상수 등을 통해 형식을 유추할 수 있습니다. 언어는 이 형식 정보를 변수의 수명에 걸쳐 조건문의 분기 등에 전달할 수 있습니다. 언어가 형식 정보를 사용할 수 있으면 완전한 형식 정보가 있는 DynamicSite 형식으로 컴파일되는 DLR AST를 생성할 수 있습니다. 이에 따라 Invoke 메서드 호출에 대한 인라인 코드 생성과 캐시에서의 대상 코드 호출이 가능해집니다.
현재 DLR은 6개 이하의 제네릭 형식 매개 변수로 제네릭 DynamicSite 형식을 생성합니다. 첫 번째 N-1 매개 변수는 인수의 형식을 나타내며 N번째 매개 변수는 작업의 결과 형식을 나타냅니다. 6개 이후의 형식에 대해서는 런타임 도우미 함수에 대한 메서드 호출만 수행하는 AST를 생성해야 합니다. 그러한 함수에서는 런타임에 모든 계산을 수행하기 위해 느린 작업을 해야 합니다. 앞으로는 많은 수의 정적으로 형식이 지정된 인수를 제공할 수 있도록 DLR에서 제네릭 튜플 사용이 허용될 것입니다. 또한 6개라는 수 역시 대부분의 프로그램에 최적의 수를 알아내면 이에 따라 늘릴 수 있습니다.
이제 언어별 AST와 DLR AST에 대한 내용을 알아보는 방법을 충분히 이해할 수 있을 것입니다. 트리에 대한 몇 가지 기본적인 정보가 있으면 DLR에서 특정 언어가 어떻게 구축되었는지 알아볼 수 있습니다. DLR 상에서 언어를 구현하고자 한다면 이 기술을 사용할 수 있습니다. 그렇지 않은 경우에도 내부적으로 어떤 일이 일어나는지 알아보는 것도 흥미로울 것입니다.
DLR이 언어 구현 개발자에게 제공하는 몇 가지 공유 인프라에 대해서도 살펴보아야 합니다. 가비지 컬렉션, JIT 컴파일, 동적 사이트, 공용 동적 형식 모델과 같이 CLR과 DLR을 통해서 활용할 수 있는 장점이 많이 있습니다. .NET 런타임 상에서 언어를 구현함으로써 얻을 수 있는 결과는 현재도 좋으며 계속해서 개선되고 있습니다. DLR 버전 1은 매우 훌륭할 것입니다. .NET에서 동적 언어를 구현하는 과정은 놀라울 정도로 쉬워질 것이며 그 결과 역시 탁월할 것입니다.

질문이나 의견이 있으면 다음 전자 메일 주소로 보내시기 바랍니다: clrinout@microsoft.com.


Bill Chiles는 동적 언어 프로젝트(CMU Common Lisp, Dylan)와 관련 도구를 개발하면서 대부분의 경력을 쌓았으며 현재 Microsoft에서 선임 프로그램 관리자로 일하고 있습니다. Bill은 Visual Studio Core Technologies 팀에서 9년 동안 일했으며 현재는 IronPython 및 동적 언어 런타임에 관련된 개발에 참여하고 있습니다.

Page view tracker