Kori 12월 30일, 2022에 포스트됨 공유하기 12월 30일, 2022에 포스트됨 마르코 칸투 (Marco Cantu)의 "Some Suggestions to Help the Delphi Compiler" 를 번역했습니다. (원문 작성: 2022년 12월, 최종 번역: 2022년 12월) 델파이 컴파일러가 더 빠르게 작동하고, 메모리를 더 적게 쓰도록 돕기 위해 개발자가 고려할만한 조언과 팁 몇가지 (델파이 현재 버전 사용자 기준). --- 지난 몇 년 동안, 엠바카데로는 델파이 오브젝트 파스칼 컴파일러의 성능을 최적화하고, 메모리를 더 적게 사용하도록 상당한 노력을 기울였다. 그 결과, 매우 큰 애플리케이션을 가진 고객들은 델파이가 더 좋아진 것을 직접 느낄 수 있었다. 우리가 주로 집중한 영역은 Win32 컴파일러이지만, 다른 컴파일러들도 역시 향상되었다. 많은 사용 사례(use case)에서 델파이 컴파일러는 현격하게 진보했다. 하지만, 고객들의 많은 프로젝트를 보니 델파이 컴파일러가 기대한대로 작동하지 하지 않는 경우도 아직 있다는 점을 알게 되었다. (컴파일러에게 전달하는 폴더 경로가 잘못된 경우 등) 크게 의미 없는 상황(scenario)들도 포함하여 우리는 우리가 파악한 문제 대부분을 가까운 미래에 해소할 계획이다. 이와 동시에, 우리 고객들은 자신들이 직접 프로젝트 구성이나 소스 코드를 정비하여 개발자들이 더 좋은 상황에서 작업할 수 있도록 할 수 있는 방법이 있는 지를 우리에게 질문했다. 이 글은 그 질문에 대한 대답이다: 델파이 현재 버전을 사용하여, 개발자가 델파이 컴파일러를 더 빠르게 작동하고, 메모리를 더 적게 쓰도록 돕기 위해 고려할 수 있는 (그리고 당신의 코드 베이스에도 적합할 수 있는) 조언과 팁 몇가지를 공유한다. 아래에 언급하는 아이디어 대부분은 컴파일할 때와 델파이 LSP로 작동하는 코드 인사이트(Code Insight)를 사용할 때 모두 도움이 된다. 이 둘은 뒤쪽 동일한 컴파일러가 작동하기 때문이다. 즉, 이 컴파일러를 돕는 것은 코드 인사이트 역시 돕는 것이다. 목차 1 잘못된(invalid) 검색 경로(Search Path) 또는 라이브러리 경로(Library Path) 항목 2 UNC 드라이브 상에 있는 소스 코드 파일 3 유닛 별칭(Unit Aliases) 4 유닛 범위 이름(Unit Scope Names)과 충분히 명시되지 않은 유닛 이름(Unqualified Unit Name) 5 반복되고 중복되는 유닛 이름 (Repeated and Duplicate Unit Names) 6 순환하는(Circular) 유닛 참조(Unit Reference) 7 반복되는 제네릭 타입 인스턴스(Repeated Generic Types Instance) 8 더 있을까? 그렇다. 더 있을 수 있다. 1 잘못된(invalid) 검색 경로(Search Path) 또는 라이브러리 경로(Library Path) 항목 컴파일러는 코드 안에 있는 uses 문에서 참조하는 모든 유닛을 찾는다. 그러기 위해 프로젝트 폴더, 프로젝트의 검색 경로(Search Path), 라이브러리 경로(Library Path)와 기타 위치를 살핀다. 이 경로(Path) 옵션들 안에 잘못된(invalid) 폴더 경로가 있으면, 델파이 컴파일러가 그 경로 찾기를 반복하게 하는 원인이 된다. 운영 체제 수준에서 존재하지 않는 경로를 찾으려는 작업으로 인해 훨씬 느려진다. 우리가 받은 리포트 중에서 ‘Delphi compiler works extremely slowly when several invalid paths are present in LIB PATH (LIB PATH에 잘못된 경로가 몇개 있을 때 델파이 컴파일러가 심각하게 느리게 작동함)' 역시 그 중 하나의 사례이다. https://quality.embarcadero.com/browse/RSP-3931 에서 볼 수 있다. 우리가 만든 작은 애플리케이션에서 몇가지 테스트를 한 결과는 아래와 같다. 잘못된 경로가 있는 경우의 컴파일 시간: 40.3 초 잘못된 경로가 없는 경우의 컴파일 시간: 1.7 초 보다시피 차이가 엄청나게 크다. 우리는 델파이 컴파일러를 수정하여 잘못된(invalid) 경로를 사전에 확인하고 제외하도록 할 계획이다. 하지만, 개발자에게 이런 항목을 직접 정리하는 것은 그리 어렵지 않을 것이다. 2 UNC 드라이브 상에 있는 소스 코드 파일 파일 액세스 관련 작동(특히 누락된 파일 찾기)과 관련된 이슈는 프로젝트 전체를 컴파일 할 때 그 비용이 훨씬 더 크다. 또한 컴파일 대상이 되는 유닛(unit)들이 로컬 드라이브에 있지 않고 네트워크에 연결된 드라이브 장치 안에 있는 경우에 더 심각하다. 사실, 우리는 여러분이 소스 코드를 로컬 SSD 드라이브에 넣어두는 것을 매우 권장한다: 델파이 컴파일은 디스크에 크게 부하가 집중되는 작업이므로 하드 디스크가 속도에 따라 그 속도의 차이가 엄청나게 크다. 3 유닛 별칭(Unit Aliases) 유닛 별칭(Unit Aliases)은 uses 구문 안에 있는 유닛 이름을 다른 실제 유닛이름으로 맵핑(mapping)하는 역할을 한다. 오래된 코드를 마이그레이션 할 때 손쉽게 활용할 수 있으며, 지금까지 오랫동안 델파이에서 핵심 유닛(Unit)이 병합되거나 이름이 바뀌었을 때 사용된 방식이다. 예를 들어, 델파이 초기 버전들 몇몇에서는 아래와 같은 유닛 별칭이 들어 있었다. WinTypes=Windows;WinProcs=Windows;DbiProcs=BDE;DbiTypes=BDE;DbiErrs=BDE 지금은, 새 프로젝트를 만들면 기본으로 정의되는 유닛 별칭이 없다. 하지만, 유닛 별칭을 사용하는 프로젝트들을 가진 개발자들도 아마 있을 것이다. 이것들을 (가급적 하나씩) 제거하여 그 프로젝트 소스 코드가 올바른 유닛을 직접 참조하도록 수정할 것을 권장한다. 유닛 별칭을 많이 사용하는 경우, 컴파일러는 부가 작업을 수행해야 한다. 우리가 아는 한 그 부담은 매우 작긴 하지만 어쨌든 시간이 조금이라도 더 걸린다. 게다가 별칭을 정비하면 당신의 코드 역시 더 깔끔해지고 읽기 쉬워질 것이다. 4 유닛 범위 이름(Unit Scope Names)과 충분히 명시되지 않은 유닛 이름(Unqualified Unit Name) 델파이는 유닛 범위 이름(Unit Scope Names)을 10여 년 전에 도입했다. 유닛 범위 이름이란 기본 라이브러리에 속하는 모든 유닛들의 이름 앞에 붙는 접두사이다. 예를 들면 Vcl.Forms, System.SysUtils 와 같이 유닛 이름 앞에 유닛 범위 이름이 붙는다. 자세한 내용은 도움말을 참조: https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Unit_Scope_Names 기존 프로젝트에서 짧게 유닛 이름만 사용하고 있다면, 이것을 빠르게 마이그레이션하는 방법은 프로젝트 옵션인 Unit Scope Names를 지정하는 것이다. 아래 그림 참조 위 그림에 보이는 것처럼, 새 프로젝트를 생성하면, 델파이 IDE는 프로젝트 옵션에 기본적으로 많은 유닛 범위 이름을 추가한다 (이 글에서 다룰 사항은 아니지만, 우리는 지금 이 옵션을 변경할 필요가 있다고 여기고 있다). 이제 코드 안에 충분히 명시되지 않은 유닛 이름이 있는 경우, 델파이 컴파일러는 검색 경로 또는 라이브러리 경로에 있는 모든 폴더를 찾아보고, 이 옵션에 지정된 접두사가 해당될 수 있는지를 확인하게 된다. 지정된 폴더가 20 개이고, 접두사가 20 개 있다고 가정하면 파일 탐색 대상은 400이 된다. 이에 대한 고객 리포트 역시 당연히 있었다. ‘Unqualified unit names cause massive amounts of file lookups (충분히 명시되지 않은 유닛 이름이 있을 때 파일 검색 작업량이 엄청나게 많음)' 역시 그 것들 중 하나이다. https://quality.embarcadero.com/browse/RSP-18130 에서 볼 수 있다. 델파이 컴파일러 안에도 실제 작업을 줄이기 위한 캐시 로직들이 몇 가지 들어 있기도 하다. 하지만, 기본적으로 이 기능은 코드 마이그레이션을 돕기 위한 것일 뿐이며, 개발자는 결국 uses 절을 정비하고, 엠바카데로 라이브러리 안에 있는 유닛을 사용할 때 충분히 명시된 유닛 이름을 부르는 것이 좋다. 즉 프로젝트 설정 수준에서 지정한 유닛 범위 이름이 있다면 모두 제거하는 것이 옳다. 5 반복되고 중복되는 유닛 이름 (Repeated and Duplicate Unit Names) 유닛 이름에 대해 말하자면, 문제가 되는 또 다른 상황이 있다. 이것은 성능 이슈라기 보다는 작동 실패와 관련된 것이다. 특히 코드 인사이트(Code Insight)와 기타 IDE 도구를 사용할 때 문제가 된다. 다음과 같은 상황에서 문제가 된다. 프로젝트 그룹을 하나를 IDE에서 열었는데, 그 프로젝트 그룹에는 프로젝트가 여러 개 있고, 각 프로젝트에는 유닛이 여러 개 있다. 그 유닛들은 당연히 서로 다른 여러 폴더에 각각 들어 있는데 그 중에서 이름이 같은 유닛이 있을 수 있다. 다시 말해서, MyUnit.pas라는 유닛 파일이 2 개 있는데 각각 서로 다른 폴더 안에 들어 있을 수 있다. 일반적으로 델파이 컴파일러는 이런 상황을 잘 처리한다. 하지만, 항상 그렇지는 않은데, 예를 들어 이 리포트를 통해 알 수 있다. ‘Compiler confuses unit names in project group (프로젝트 그룹 안에 있는 유닛 이름을 컴파일러가 혼동함)' https://quality.embarcadero.com/browse/RSP-39293 에서 볼 수 있다. 우리는 이와 유사한 상황에서 델파이 디버거가 혼동하지 않도록 하는 작업을 몇 년 전에 이미 했다. 하지만, 델파이 LSP는 이름이 같은 유닛을 여전히 혼동할 수 있다. 가능한 한, 서로 다른 유닛이라면 같은 이름을 사용하지 않도록 미리 방지하는 것이 훨씬 좋다. 6 순환하는(Circular) 유닛 참조(Unit Reference) 컴파일러 성능에 영향을 주는 상황으로 돌아가서, 우리가 아는 한 가장 심각하게 영향을 주는 것은 유닛의 implementation 구역 안에 있는 순환하는 유닛 참조 (Circular Unit Reference)이다. 알다시피, 유닛의 interface 구역은 유닛이 서로를(mutual) 참조하거나 또는 순환(circular) 참조하는 것을 허용하지 않는다. 하지만, 유닛의 implementation 구역 안에서는 이론적으로 프로젝트 안에 있는 어떤 유닛이든 참조하는 것이 가능하다. 델파이가 유닛 하나를 컴파일하려면, 그 유닛에서 사용하고 있는 모든 유닛들을 찾고 전체 관계 그래프를 탐색해서 순환 문제가 없는 지 소스를 확인한다. 그래야 검색하는 중에 무한 루프를 피할 수 있다. 수백 개의 유닛이 복합적인 관계 그래프를 구성하는 경우 이 탐색 작업에 드는 비용은 상당하다. 최근 우리 지원 팀은 흥미로운 실험을 했다. 모든 유닛이 모든 다른 유닛들을 사용하는 애플리케이션을 만드는 코드 생성기를 만들었다. 극단적인 상황임에 분명하다 - 여러분은 이런 프로젝트를 만들면 정말 안된다! 이 상황에서 유닛 1 개를 더 추가하면 컴파일 시간은 거의 2 배가 된다. 다시 말해서, 다른 모든 유닛을 사용하는 유닛이 1개가 추가되는데, 그 유닛을 다른 모든 유닛에서도 사용한다면, 컴파일 시간은 기하급수적으로 나빠진다 (존재하는 유닛의 수가 N개 라면 유닛 1개가 추가될 때마다 유닛 참조는 N*2가 더해지기 때문에 기하급수적이다). 다시 말하지만, 델파이 컴파일러가 이미 최적화되어 있을 때, 즉 검색과 이름 찾기를 해시 딕셔너리로 옮기고 최적화된 데이터 구조를 갖춘 상태에서 작업을 하는 경우,의 결과이다. 그리고 우리는 지금 컴파일러를 향상하기 위해 이 부분을 적극적으로 살펴보고 있기도 하다. 델파이 10.4.2에서 해소한 이 리포트 사례를 참고하자: https://quality.embarcadero.com/browse/RSP-28811 하지만, uses 구문에 의한 순환 문제는 좋은 프로그래밍과는 정반대이다. 사용하는 유닛 간의 관계 그래프가 결국에는 스파게티(마구 뒤섞인 상태)가 된다. 유닛들 사이의 그리고 클래스 사이의 의존성을 줄이는 것이 좋은 프로그래밍이다. 한 줄로 이어지는 의존성 (하위 수준 유닛을 상위 수준 유닛들이 사용, 데이터 액세스 유닛들을 UI 유닛들이 사용하는 것, 그 반대로는 하지 않는 것)은 받아들일 수 있지만, 복합적인 관계도가 그려지는 코드는 유지 보수를 오래하기에 적합하지 않다. 게다가, 이것은 컴파일러의 성능과 메모리 사용에 심각한 부하를 준다. 우리가 델파이 개발자들로부터 실제로 받은 리포트들에 의하면, 상당히 큰 애플리케이션인 경우, uses 구문을 재정비하여 순환하는 유닛 참조를 피하면, 컴파일 시간이 최대 90%까지 줄어들었다. 10분 걸리던 컴파일 작업이 1분으로 줄어들고 뿐만 아니라 코드 인사이트의 반응성 역시 좋아지게 된다. 순환 참조 이슈를 해소하고 싶다면, 아래 도구 중 하나를 사용할 것을 권한다. 이 도구들은 애플리케이션 안에 있는 uses의 유닛 관계 그래프를 제공하고 순한 관계를 탐지한다: GExpert의 Project Dependencies (아래 그림 참조): https://gexperts.org/tour/index.html?project_dependencies.html Peganza의 Pascal Analyzer: https://www.peganza.com/#PAL Delphi Unit Dependency Scanner: https://github.com/norgepaul/DUDS 참고로, 델파이 컴파일러는 동일한 유닛이 다른 순서로 uses 되는 것을 싫어한다 (사용하는 유닛들의 의존성 관계 그래프에 영향을 주기 때문이다). 다시 말해서, 유닛 2 개가 있는데 모두가 유닛 A와 유닛 B를 참조한다면 두 유닛 모두에서 “uses A,B;” 라고 쓰는 것이 두번째 유닛에서 “uses B, A;”라고 적은 것보다는 더 좋다. 순환하는 유닛 참조 이슈에 비하면 상당히 사소하지만, 매우 규모가 큰 애플리케이션에서라면 알아둘만한 사항이다. 마지막으로, 불필요한 uses 구문을 제거하는 작업을 고려할 수 있다. 이 작업을 돕와주는 도구 중 하나로 https://delphiparser.com/product/remove-superfluous-uses-optimizer-wizard-evaluation-edition/ 를 꼽을 수 있다. 7 반복되는 제네릭 타입 인스턴스(Repeated Generic Types Instance) 델파이 컴파일러를 돕기 위한 수단으로 마지막으로 알려주고 싶은 이 제안은 다소 논란의 소지가 있으며, 델파이 컴파일러가 몇 년 후에라도 더 좋아져야 하는 것이다. 하지만, 이 방법은 컴파일 시간과 메모리 사용 그리고 메모리 부족(out-of-memory) 컴파일 에러와 관련하여 현격한 차이를 만들어 줄 수 있다. 델파이의 타입(type) 체계는 (클래식한 터보 파스칼에서와 같이) 타입 선언 동일성(equivalence)을 기반으로 한다. 다시 말해서, 타입이 두개 있는데 이름과 구조가 모두 같다고 해도 서로 다른 유닛(unit) 안에 선언된 것이라면 그 두개는 서로 다른 것이다. MyUnit2.TMyType 타입을 위한 변수에 MyUnit1.TMyType 타입을 할당하는 경우 컴파일 에러가 난다. 두 타입의 구조가 완전히 동일하다고 해도 그렇다. 이 규칙을 벗어나는 유일한 예외는 제네릭(generic) 타입이다. 당신이 TList 타입 변수를 선언한다면, 당신은 제네릭 타입인 TList를 기반으로 하는 암묵적인 TList 타입을 생성하는 것이다. 따라서 당신은 서로 다른 유닛에서 선언한 타입 인스턴스를 여러 개 가질 수 있고 그 인스턴스들은 모두 TList와 서명(signature)가 동일하다. 그러면 델파이 컴파일러는 이것들이 서로 호환성이 있다고 간주하기 때문에 서로에게 할당할 수 있도록 허용한다. 뒤쪽에서 작동하는 원리는 다음과 같다. 델파이 컴파일러는 서로 다른 유닛에 있는 타입 각각마다 임시 타입을 새로 생성한다. 그 결과 예를 들어 MyUnit1에 있는 TList를 (임시 타입 이름을 주고) 생성하고, MyUnit2에 있는 TList를 (앞의 것과 다른 임시 타입 이름을 주고) 생성한다. 이 타입 각각을 위해 델파이 컴파일러는 해당 제네릭 타입의 인스턴스를 생성할 뿐만 아니라 각 제네릭 타입을 위한 제네릭 메소드들도 생성한다. 복합적인 제네릭 타입들이 (상속과 포함을 통해) 많이 사용되면, 컴파일러는 상당한 작업을 수행해야 한다. 이 이슈를 해결하는 방법은 제네릭스 사용을 중단하는 것이 아니라, 가능할 때 마다 제네릭 타입의 공유 인스턴스를 생성하는 것이다. 다시 말해서, TList를 수십 개가 넘는 다른 유닛들 안에서 사용하는 것 보다는 아래와 같이 선언을 하고, type TListOfStrings = TList; 이런 TList를 사용하는 모든 곳에서 위와 같이 선언한 TListOfStrings 타입을 사용하는 것이다. 간단한 예제에서는 그 차이가 거의 눈에 띄지 않는다. 하지만, 제네릭 타입을 상당히 많이 사용하는 대규모 애플리케이션에서는 매우 현격한 차이가 생긴다. 다시 말하지만, 우리 모두는 델파이 컴파일러가 이와 유사한 상황을 더 잘 처리할 수도 있을 것이라는 점을 잘 알고 있고, 우리는 지금 이런 상황을 처리할 수 있도록 향상하는 작업을 진행하고 있다 - 그리고 지난 몇 년 동안 꽤 향상시켜왔다. 하지만 여전히 당신의 코드를 이런 방향으로 리팩토링(refactoring)하는 것으로도, 델파이 컴파일러가 더 빨라지고 메모리를 더 적게 사용하도록 도울 수 있다. 해당 제네릭한 타입 동일성이 존재하지 않았던 것처럼 코드를 작성하는 것이다. 8 더 있을까? 그렇다. 더 있을 수 있다. 앞에서 소개한 것들은 우리가 델파이 컴파일러 성능을 점검하는 동안 발견한 것들이다. 이처럼 코드 변경을 통해서 델파이 컴파일러의 성능을 일부 해소할 수 있다. 특히 유닛 별칭 제거하기, 충분히 명시되지 않은 유닛 이름 사용하지 않도록 하기, 유닛 참조 순환 문제를 탐지하여 제거하기 등을 권장했다. 만약 제네릭스(generics)를 상당히 많이 사용한다면 위 제안 중 마지막 팁을 활용해보기 바란다. 이 권장은 특히 대규모 애플리케이션을 가지고 있고, 컴파일이 느려지는 경험을 하고 있는 경우에 검토할 필요가 있는 것들이다. 델파이 개발자 대부분은 델파이 컴파일러의 성능에 만족하고 있고, 이런 조치를 할 필요가 없을 것이다. 코드 변경을 통해 델파이 컴파일러 성능을 향상시키는 방법은 앞에서 제시한 것들 말고도 더 있을 것이다. 경험을 통해 알게된 또 다른 방법을 공유한다면 환영한다! 연말 휴가를 즐겁게 보내고 2023년도 건승하기를 기원한다. 인용하기 이 댓글 링크 다른 사이트에 공유하기 더 많은 공유 선택 사항
Recommended Posts
이 토의에 참여하세요
지금 바로 의견을 남길 수 있습니다. 그리고 나서 가입해도 됩니다. 이미 회원이라면, 지금 로그인하고 본인 계정으로 의견을 남기세요.