|
LLVM은 C++ 기반의 오픈소스 컴파일러 인프라스트럭처이다. LLVM은 방대한 C++ 프로젝트이고 많은 개발자가 참여하고 있는 프로젝트이므로 적절한 코딩 가이드라인을 만드는 것은 필수이다. 좋은 내용이 많이 있기에 여기서 소개하고자 한다.
LLVM, Low Level Virtual Machine이란? LLVM은 정적/동적 컴파일러와 그와 관련된 여러 도구를 만들고 조작할 수 있는 오픈소스 프로젝트이다. 일반적인 정적(static) 컴파일러는 프런트엔드(혹은 파서)가 소스코드를 해석해 중간언어(IR, Intermediate Representation)로 만든다. 이 IR은 컴파일러 백엔드(혹은 미들엔드를 두는 컴파일러도 있음)로 보내 각종 최적화를 거친 후 최종적으로 특정 아키텍처의 기계어로 코드를 생성해낸다. 동적 컴파일러 혹은 인터프리터라면 코드 생성기가 인터프리터와 간단한 가상 머신으로 대체된다. 이제는 성능 향상을 위해 순수 인터프리터를 쓰기 보다는 JIT을 한다. LLVM은 자신만의 IR 언어를 정의하고, 이 IR를 조작하고 코드를 생성/수행하는 일을 한다. LLVM 자체는 프런트엔드를 포함하지는 않고 있다. 각 언어의 프런트엔드는 독립적으로 분리되어있다. 즉, C++ 파서가 소스코드를 분석해 LLVM IR로 만들면 비로소 LLVM 프레임웍을 쓸 수 있다. 1: define i32 @main() { 2: entry: 3: %retval = alloca i32 ; <i32*> [#uses=2] 4: %0 = alloca i32 ; <i32*> [#uses=2]5: %"alloca point" = bitcast i32 0 to i32 ; <i32> [#uses=0] 6: %1 = call i32 @puts(i8* getelementptr inbounds ([14 x i8]* @.str, i64 0, i64 0)) ; <i32> [#uses=0] 7: store i32 0, i32* %0, align 4 8: %2 = load i32* %0, align 4 ; <i32> [#uses=1] 9: store i32 %2, i32* %retval, align 410: br label %return 11: 12: return: ; preds = %entry 13: %retval1 = load i32* %retval ; <i32> [#uses=1] 14: ret i32 %retval1 15: }위 코드는 “Hello, World!” 프로그램을 LLVM IR로 변환한 것이다. 보다시피 스택에 변수 하나 생성하고 함수 호출 과정이 매우 자세히 기록되어있다. 거의 기계어와 1:1로 대응이 된다. 그런데 이 LLVM IR은 특정 아키텍처에 종속적이지 않는 플랫폼 독립적이다. Low Level Virtual Machine은 바로 이런 뜻. LLVM은 대학교에서 시작된 프로젝트였는데 LLVM을 만든 Chirs Lattner가 애플로 감으로써 LLVM은 오프소스이지만 애플이 전폭적으로 지원하는 형태가 되었다. 애플의 주요 툴체인은 이미 LLVM 기반으로 바뀌고 있다. 또, 오픈소스이다보니 여러 개발자의 참여가 매우 활발한데, 나도 처음 경험하는 직접적인 오픈소스여서 배우는 것이 많다. 지금까지 적지 않은 버그를 리포팅 하기도 하였다. (윈도우 플랫폼에서 컴파일은 제대로 되는데 제대로 작동하지 않는 부분이 많이 있음.) LLVM은 내가 지금 하는 것처럼 프로그램을 분석하는 루틴을 작성할 때 아주 편리하다. 클래스 하나를 선언해 내가 원하는 작업을 구현하면 컴파일 단계 중 특정 부분에서 호출할 수 있다. 최근에는 동적 컴파일러 혹은 JIT도 주요한 연구 분야인데, LLVM은 역시 매우 좋은 프레임웍을 제공한다.
LLVM, 매우 훌륭한 C++ 기반 프로젝트의 교과서 LLVM은 C++ 언어적으로 볼 때 매우 잘 만들어진 프로젝트이다. gcc는 C로 만들어져 기존 컴포넌트를 재활용하거나 새로운 것을 추가하기가 상당히 까다로운데, LLVM에서는 약간의 학습만 거치면 놀라울 정도로 편리하게 사용할 수 있다. (물론 제대로 하려면 삽질이 엄청 따르고 버그도 직접 잡아야 하지만). 그 만큼 기초 설계가 매우 튼튼하기 때문이다. 나 역시 LLVM을 하면서 상당히 많은 C++ 공부를 새로 할 수 있었다. LLVM에 코드를 기여하고 싶은 사람은 LLVM Coding Standard를 따라야 한다. 이미 구글 C++ 코딩 가이드라인이 유명해져 많은 개발자가 참고하고 있는데, LLVM 코딩 가이드라인 역시 흥미로운 점이 많이 있다. 서론이 너무 길었고 몇 가지를 본격적으로 살펴본다.
1. Source Code Formatting 이 포맷팅 관련 규칙은 가장 눈에 띄는 스타일 요소라서 프로그래머의 개성이 그대로 드러나고 바꾸는 것에 대한 저항감도 큰 것이 사실이다. 나 역시 그러했다. 나는 C/C++ 프로그래밍의 시작을 Jeffrey Richter와 Charles Petzold의 윈도우 프로그래밍과 같이해서 그들의 방식을 매우 오랫동안 따라왔다. 예를 들어, 헝그리안 표기 방식 같은 것. 그래서 다른 스타일의 코드를 보거나 쓰는 것이 불편하거나 싫어했다. 그런데 나이가 먹어서 그런가(...) 이제는 그런 저항감은 거의 없다. 한 때는 탭은 \t, 4칸이어야 한다고 주장했는데, 이제는 반드시 공백이어야 한다고 주장하고, 심지어는 2칸 공백 탭이 더욱 편리하게 되었다. LLVM은 { 를 다음 줄로 하지 않고 같은 줄에 쓰는 스타일이다. 나 역시 이 스타일은 코드를 너무 빡빡하게 만들어 상당히 싫어했는데 LLVM을 좀 만지니 어느덧 익숙해지고 말았다. 이렇듯 내 경험에 비춰볼 때, 변수 이름 규칙이나, 코드 포맷팅 규칙은 생각보다 쉽게 바꿀 수 있다고 본다. 중요한 것은 코드가 가지는 의미이지 포맷팅이 아니기 때문이다. 1.1 주석 관련 잘 짜여진 코드는 사실 따로 문서가 필요 없다. 코드 자체가 문서이기 때문이다. 그것을 가능케 하는 것이 바로 잘 작성된 주석이다. 주석 다는 것이 사실 굉장히 귀찮은 일이지만 이런 큰 오픈소스 프로젝트에서는 매우 중요하다. 실제 LLVM의 핵심 기본 클래스는 주석이 상당히 잘 되어 있어 별도의 문서를 볼 필요도 없다(없기도 하지만 ㅋ). 구체적으로 파일 헤더, 클래스 개괄, 메소드 별 정보를 주석으로 잘 적도록 지시하고 있다. 보통은 Doxygen이 바로 적용될 수 있도록 그 규칙을 따르기도 한다. /// A Module instance is used to store all the information related to an/// LLVM module. Modules are the top level container of all other LLVM/// Intermediate Representation (IR) objects. Each module directly contains a/// list of globals variables, a list of functions, a list of libraries (or/// other modules) this module depends on, a symbol table, and various data/// about the target's characteristics.////// A module maintains a GlobalValRefMap object that is used to hold all/// constant references to global variables in the module. When a global/// variable is destroyed, it should have no entries in the GlobalValueRefMap./// @brief The main container class for the LLVM Intermediate Representation.class Module {/// @name Types And Enumerations/// @{public: /// The type for the list of global variables. typedef iplist<GlobalVariable> GlobalListType; /// The type for the list of functions. typedef iplist<Function> FunctionListType; /// The type for the list of aliases. typedef iplist<GlobalAlias> AliasListType; /// The type for the list of named metadata. typedef ilist<NamedMDNode> NamedMDListType; 위 코드는 Module.h 파일인데 보다시피 자세한 설명과 Doxygen의 규칙을 따르고 있다. 아래 코드는 Module.cpp 코드의 메소드 구현에 있는 주석이다. //===----------------------------------------------------------------------===// // Methods for easy access to the functions in the module.//// getOrInsertFunction - Look up the specified function in the module symbol// table. If it does not exist, add a prototype for the function and return// it. This is nice because it allows most passes to get away with not handling// the symbol table directly for this common task.//Constant *Module::getOrInsertFunction(StringRef Name, const FunctionType *Ty, AttrListPtr AttributeList) { // See if we have a definition for the specified function already.GlobalValue *F = getNamedValue(Name); if (F == 0) {한편, LLVM은 대량의 코드를 주석 처리할 때, #if 0, #endif 조합을 쓸 것을 권장한다. 적극 동감하는 바이다. 1.2 #include 스타일 //===-- Module.cpp - Implement the Module class ---------------------------===// //// The LLVM Compiler Infrastructure//// This file is distributed under the University of Illinois Open Source// License. See LICENSE.TXT for details.////===----------------------------------------------------------------------===////// This file implements the Module class for the VMCore library.////===----------------------------------------------------------------------===//#include "llvm/Module.h" #include "llvm/InstrTypes.h" #include "llvm/Constants.h" #include "llvm/DerivedTypes.h" #include "llvm/GVMaterializer.h" #include "llvm/LLVMContext.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringExtras.h" #include "llvm/Support/LeakDetector.h" #include "SymbolTableListTraitsImpl.h" #include "llvm/TypeSymbolTable.h" #include <algorithm>#include <cstdarg>#include <cstdlib>using namespace llvm; C/C++의 가장 지저분한 것을 고르라면 단연코 #include 시스템일 것이다. 아무 생각 없이 #include를 남발하면 복잡한 의존성이 생겨버려 코드 한 줄 고치면 전체 파일이 다 컴파일되는 비극도 일어날 수 있다. 특히 큰 프로젝트일수록 깔끔한 #include 관리는 매우 중요하다. LLVM 역시 구체적인 규칙을 명시하고 있다. 위 코드 예에서 보듯, #include는 헤더 주석 뒤에 바로 나와야 한다. 첫 번째 #include는 반드시 해당 파일의 헤더가 와야 한다. 이 예에서는 Module.cpp이므로 Module.h가 보다시피 첫 번째로 온다. 이렇게 하는 이유는 Module.h가 처음에 나옴으로써 Module.h 자체가 필요한 #include는 반드시 Module.h에 있도록 강제할 수 있다. 그러하지 않다면 분명 정의를 못 찾아 컴파일 에러가 뜰 것이기 때문이다. 이렇게 자신의 헤더 파일을 #include 한 뒤에 이 파일만이 사용하는 헤더가 오고, LLVM의 공통 헤더가 알파벳 순서대로 나열된다. 마지막으로 일반적인 C/C++ 헤더가 온다. LLVM 코딩 가이드라인에서는 구체적으로는 말을 안 하고 있는데, 큰 프로젝트에서는 폴더가 필연적으로 많아지니 헤더 파일의 상대경로 역시 적절히 적어주는 것이 중요하다. 위의 예에서는 llvm을 기준점으로 라이브러리별로 #include를 하는 것을 볼 수 있다. 그런데 “SymbolTableListTraitsImpl.h” 처럼 상대 경로 없이 바로 있는 것도 있다. 나는 이걸 좋아하지는 않는다. 묵시적으로 그냥 지금 이 Module.cpp와 같은 폴더에 있다는 뜻이겠지만, 컴파일러 옵션에 별도의 참조할 include 폴더를 정의한다면 다른 폴더가 될 수도 있다. 그래서 예전 회사에서 작업하던 큰 프로젝트에서는 ‘모든’ 헤더 파일에 반드시 상대 경로를 명시하도록 했다. 그리고 컴파일러 옵션에 최상위 폴더 외에 다른 추가 참조 include 폴더는 쓰지 않도록 했다. 1.2 소스 폭, 탭 나처럼 vim/emacs를 싫어하는 사람으로서는 소스 폭이 중요하다고 생각해 본적이 별로 없었다. 그러나 터미널에서 작업하는 것도 중요한 vim/emacs 유저에겐, 또 프린팅을 고려한다면, 소스 폭을 제한하는 것이 합리적이다. 구글에서도 LLVM에서도 모두 80 컬럼으로 제한하고 있다. 모든 코드가 칼 같이 80 컬럼에 맞춰있다. 역시 쉬운 논쟁의 대상인지라 LLVM/구글 모두 80 컬럼을 명확하게 말하고 있다. 또 하나의 이점은 80 컬럼 정도로 제한을 두면 와이드 모니터에서 두 소스를 동시에 놓고 작업할 때 상당히 편리해진다. 처음에는 좀 받아들이기 힘들었는데 강제적인 80 컬럼 제한이 나름 의미가 있다고 생각한다. (Visual Studio 2010에는 익스텐션을 활용하여 80 컬럼에 가이드라인을 그을 수 있다.) 탭 역시 \t이냐 공백이냐 논쟁이 있는데, LLVM (구글 역시) 공백을 사용토록 하고 있다. 다만, 구글은 두 칸의 공백을 쓰도록 했다면, LLVM은 두 칸인지 네 칸인지 명시하지는 않고 있다. 이건 자유에 맡기지만, 결코 2칸, 4칸 탭을 섞어서는 안 된다. 그러나 이미 대부분의 핵심 LLVM 코드는 두 칸 공백을 사용하고 있다. 80 컬럼 제한에서 탭을 4칸으로 하면 너무 쓸 수 있는 코드가 없으므로 사실상 두 칸 공백은 필수적이다. 코드를 어떻게 인덴트할 것인가 역시 큰 논쟁거리다. 또 연산자 사이에 공백을 어떻게 넣을 것인가도. LLVM은 명확하게 규정하고 있지는 않고 잘 알아서 하도록 당부하고 있다.
열 외: 포인터 표기법 이건 LLVM에는 없는 내용이지만 포인터 표기 법에 대한 것이다. 1: Value *DoSomething(Instruction *I); 2: Value* DoSomething(Instruction* I);역시 논쟁이 많다. 나는 후자를 강력하게 지지한다. LLVM은 대부분 1번 방식을 따른다. 내가 2번 방식을 좋아하는 이유는 ‘Value*’가 하나의 ‘타입’이기 때문이다. 마찬가지로 함수 인자로 넘어가는 I도 그 타입이 ‘Instruction*’라는 것이다. typedef로 포인터 타입을 다른 이름으로 대체한다면 이는 더욱 합리적이라고 생각한다. 1번 방식이 자연스러운 경우는 *가 타입 선언이 아니라 값을 읽어오는 연산자로서 사용될 때이다. 즉, ‘*I’는 포인터 변수 I의 값을 읽는다는 뜻이 더욱 있다고 본다. 그래서 나는 1번 표현 방식이 늘 거북하다. 굳이 1번 방식이 좋은 경우라고는: 1: int *a, *b, *c; 2: int* a, *b, *c; 이렇게 포인터 변수를 한 줄에 선언할 때인데, 나는 변수 선언은 가급적 한 줄에 하나씩 하는 것을 좋아하기에 큰 문제가 되지 않는다. 암튼 이 포인터 표기 방법은 논란 거리인데, 역시나.. 그냥 이미 있는 코드가 다른 방식을 쓴다면 그렇게 따라 하는 것이 속 편하다. 어렸을 때는 이런 것도 흥분하며 논쟁했는데, 이제는 그냥 아무것도 다 좋아요. 중요한 건 버그 없고 의미만 명확하면 되니깐.
쓰다 보니 글이 너무 대책 없이 길어져서 다음 글에서 본격적인 C++ 관련 내용을 쓰도록 하겠습니다. 여러분의 다양한 의견 역시 듣고 싶습니다.
최근 등록된 덧글
KTUG에서 배포되는 ko.T..
by babyworm at 05/20 워드의 docx, 파워포인트.. by 나인테일 at 05/19 쓸 때마다, '차라리 이런 게.. by Raymundo at 05/18 LaTeX 쓰면서 ”.toc나 *.. by 아라크넹 at 05/18 지금 회사 전산팀에 있을대 .. by 김아람 at 05/18 TeXworks, TeXstudio .. by 김민장 at 05/18 LyX가 위지위그 스타일이라.. by 김민장 at 05/18 LyX는 어떤가요? 요즘 공.. by 길가던 공대생 at 05/18 최근 등록된 트랙백
메뉴릿
이글루 파인더
|