Unity 검색

기능 프리뷰: Unity 2022.1 베타의 IL2CPP Full Generic Sharing

2021년 12월 15일 테크놀로지 | 12 분 소요
Beta feature preview header
Beta feature preview header
다루는 주제
공유

Full Generic Sharing을 사용하면 테스트하기 쉬운 명료한 코드를 작성할 수 있습니다. 런타임에서 감지되는 모든 종류의 스크립팅 오류를 제거할 수 있을 뿐만 아니라, 모바일 기기와 콘솔 등의 플랫폼에서 코드가 보다 예상대로 작동하도록 할 수 있습니다. 아래 내용을 읽고 해당 기능에 대해 자세히 알아보세요.

Full Generic Sharing 소개

제네릭(Generic)은 C#이 가진 강력한 기능입니다. 제네릭을 사용하면 코드가 타입에 관계없이 동작을 표현하도록 할 수 있습니다. 개발자는 List이 List나 List처럼 작동할 것이라고 예상합니다. 여기서 T는 모든 타입을 나타냅니다.

수년간 IL2CPP에서는 T가 레퍼런스 타입(문자열, 오브젝트 등)인 경우 제네릭 공유를 사용했습니다. C#의 레퍼런스 타입은 항상 포인터로 표시되기 때문에 문제없이 잘 작동하며 List의 크기 및 구현은 List의 크기 및 구현과 일치합니다. 하지만 포인터가 8바이트인 64비트 시스템에서 T가 int(4바이트)인 경우 어떻게 될까요? IL2CPP는 List, List, List 등에 대한 특수 코드를 생성해야 합니다.

따라서 Unity 2022.1에서는 IL2CPP가 모든 T와 레퍼런스, 값 타입에 대해 List를 처리할 수 있도록 특수 코드를 생성합니다. 이 기술을 Full Generic Sharing이라고 합니다.

Full Generic Sharing으로 해결할 수 있는 문제

제네릭 가상 메서드는 JIT(just-in-time) 컴파일과 잘 작동하는 C#의 주요 기능으로 IL2CPP와 같은 AOT(ahead-of-time) 컴파일에서는 구현하기 어렵습니다. 이때 Full Generic Sharing을 사용해야 합니다.

Unity 매뉴얼에서 발췌한 제네릭 가상 메서드 예제를 살펴보겠습니다.

이 코드는 제네릭 가상 메서드의 간결함을 보여 줍니다. 다시 말해서, IManager 인터페이스를 구현하는 모든 클래스에서 IReceiver 인터페이스를 구현하는 모든 클래스로 모든 타입의 데이터(메시지)를 보낼 수 있습니다. Unity 2021.2의 IL2CPP를 사용하면 간단해 보이는 위 코드가 작동하지 않습니다. 런타임 중 플레이어 로그에 아래와 같은 오류가 표시됩니다.

ExecutionEngineException: Attempting to call method 'Test::OnMessage' for which no ahead of time (AOT) code was generated.  Consider increasing the --generic-virtual-method-iterations=1 argument
  at Manager.SendMessage[T] (IReceiver target, T value) [0x00000] in <00000000000000000000000000000000>:0 
  at Test.Start () [0x00000] in <00000000000000000000000000000000>:0

이 오류를 자세히 살펴보겠습니다. 

Start 메서드에서 Send Message 호출은 인터페이스(제네릭 가상의 '가상' 부분인 IManager)를 통해 발생하기 때문에 IL2CPP는 코드가 컴파일될 때 런타임에 어떤 메서드가 호출되는지 감지하지 않습니다.

왜 IL2CPP가 메서드 호출을 감지하지 않는지 궁금할 수 있는데요. 사실은 감지할 수 있습니다. 실제로 IL2CPP는 컴파일 시 주어지는 모든 코드를 검색하고 호출이 어디서 일어날지 알 수 있습니다. 하지만 이러한 검색은 비용이 들며, 프로젝트가 빌드되기까지 귀중한 시간을 소모해야 합니다. 또한 IL2CPP에서 호출되지 않을 추가 코드를 생성함으로써 최종 실행 파일의 크기가 늘어날 수 있습니다. 

오류 메시지에 언급된 --generic-virtual-method-iterations 인수를 사용하면 IL2CPP가 검색에 사용할 시간을 지정할 수 있습니다. JIT 컴파일러에서는 이러한 제네릭 가상 메서드 호출이 매우 간단합니다. 런타임에서 타겟 메서드를 직접 확인하고 올바른 작업을 수행할 수 있습니다. Unity 2022.1에서는 IL2CPP에 똑같은 방법을 적용했습니다. 이제 IL2CPP는 새롭고 특별한, 완전히 공유된 버전의 SendMessage를 생성합니다.

이 버전은 T나 레퍼런스, 값 타입과 관계없이 작동합니다. 따라서 IL2CPP가 컴파일 시 타겟 메서드를 확인할 수 없는 경우 완전히 공유된 버전을 대신 호출합니다. 이러한 C# 코드는 똑같이 표현력이 뛰어나고 런타임에 작동하며 빠르게 컴파일됩니다.

더 자세한 내용

Full Generic Sharing 기술은 AOT 플랫폼의 코드가 JIT 플랫폼의 코드처럼 작동하게 할 수 있어 매우 유용합니다. 이는 런타임 시 예상치 못한 상황을 줄여 줍니다.

이러한 ExecutionEngineException 오류는 다른 경우에서도 나타납니다. IL2CPP가 실행할 코드를 결정하지 못할 때도 ExecutionEngineException 오류가 발생할 수 있습니다. 새로 직렬화된 데이터 일부가 IL2CPP가 추측할 수 없는 타입으로 역직렬화되는 시리얼라이저에서 이와 같은 오류가 자주 발생합니다. 하지만 Unity 2022.1에서는 IL2CPP가 더는 ExecutionEngineException을 생성하지 않으므로 해결하기 어려운 모든 종류의 오류가 제거됩니다.

또한 일부 코드는 중첩된 재귀적 제네릭 타입을 사용하기도 합니다. IL2CPP가 재귀적 제네릭 타입을 컴파일 시간에 무한정 처리할 수 있으므로 빌드 프로세스에 소모되는 시간을 제한해야 합니다.

이전에 IL2CPP는 런타임에 이러한 중첩된 타입 중 일부가 필요할 때 다음과 같은 오류를 생성했습니다: 'IL2CPP encountered a managed type that it cannot convert ahead of time. The type uses generic or array types, which are nested beyond the maximum depth that can be converted.' 이제 Full Generic Sharing으로 IL2CPP는 절대 실패하지 않는 구현을 사용할 수 있으므로 더 이상 이러한 오류 메시지가 표시되지 않습니다.

규모를 최대한 줄이려는 프로젝트가 있다고 가정해 보겠습니다. List, List, List 등 여러 실행 코드가 있겠지만, 다양한 구현 방식 간의 균형을 재고할 필요가 있습니다. 

모든 List에 대해 완전히 공유되는 단 하나의 제네릭 구현을 사용하는 것이 최선일 것입니다. 그렇다면 Player Settings에서 IL2CPP Code Generation 옵션의 'Faster (smaller) builds'를 사용해 보세요. 이 옵션은 Full Generic Sharing을 사용해 빠른 증분 빌드는 물론 최대한 작은 크기의 실행 코드로 최대한 빠르게 빌드합니다. 프로젝트에서 List 또는 다른 T를 사용할 경우 IL2CPP는 구현을 위해 더는 새 코드를 생성하거나 컴파일할 필요가 없습니다.

베타 시작하기

IL2CPP Full Generic Sharing을 활용하여 코드를 작성하고 싶다면 Unity Hub나 다운로드 페이지에서 Unity 2022.1 베타를 다운로드하세요. 베타 버전은 정식 제작 단계의 프로젝트에는 사용이 권장되지 않기 때문에 기존 프로젝트를 반드시 백업하시기 바랍니다.

Unity 2022.1을 사용하고 의견을 공유해 주세요. 베타 포럼을 방문하여 의견을 공유해 주시기 바랍니다. Full Generic Sharing 또는 현재 사용 중인 다른 기능에 대한 피드백을 남겨 주세요. 재현 가능한 고유의 버그를 보고할 때마다 참여도가 높아지므로 이벤트 경품에 당첨될 확률이 높아집니다. 자세한 내용은 베타 릴리스 블로그 포스팅에서 확인하세요.

2021년 12월 15일 테크놀로지 | 12 분 소요
다루는 주제