콘텐츠로 이동

Unity IL2CPP 최적화 전략

IL2CPP(Intermediate Language To C++)는 Unity의 C# IL 바이트코드를 C++ 소스로 변환한 뒤 플랫폼 네이티브 코드로 컴파일하는 백엔드입니다. Mono보다 런타임 성능이 우수하고 iOS/콘솔 플랫폼에서 필수이지만, 빌드 시간이 길고 리플렉션에 제약이 있습니다.


C# 소스 → IL 바이트코드 → IL2CPP 변환 → C++ 코드 → 네이티브 바이너리
(csc/roslyn) (IL2CPP 툴) (MSVC/Clang/GCC)
- AOT(Ahead-of-Time) 컴파일: JIT 없음
- 스트리핑: 사용하지 않는 타입/메서드 제거
- 제네릭: 사용된 타입 조합만 코드 생성

Assets/link.xml
<!-- 스트리핑으로 제거되면 안 되는 타입/어셈블리 보존 -->
<linker>
<!-- 어셈블리 전체 보존 -->
<assembly fullname="UnityEngine.UI" preserve="all"/>
<!-- 특정 타입만 보존 -->
<assembly fullname="Assembly-CSharp">
<type fullname="MyGame.NetworkMessage" preserve="all"/>
<type fullname="MyGame.SaveData" preserve="all"/>
</assembly>
<!-- 리플렉션으로 접근하는 타입 보존 -->
<assembly fullname="System">
<type fullname="System.Collections.Generic.Dictionary`2" preserve="all"/>
</assembly>
<!-- Newtonsoft.Json 사용 시 -->
<assembly fullname="Newtonsoft.Json" preserve="all"/>
</linker>

// IL2CPP에서 리플렉션은 스트리핑으로 실패할 수 있음
// ❌ 위험: 런타임 타입 검색
var types = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.HasAttribute<RegisteredComponent>());
// ✅ 안전: Source Generator나 수동 등록
[assembly: RegisterComponent(typeof(PlayerController))]
[assembly: RegisterComponent(typeof(EnemyAI))]
// 또는 PreserveAttribute로 스트리핑 방지
[UnityEngine.Scripting.Preserve]
public class MyReflectedClass
{
[UnityEngine.Scripting.Preserve]
public void ReflectedMethod() { }
}

// IL2CPP는 사용된 제네릭 조합만 컴파일
// 런타임에 새 조합이 생기면 실패
// 문제 발생 패턴
var list = (IList)Activator.CreateInstance(
typeof(List<>).MakeGenericType(someType)); // 위험
// 해결 1: 더미 메서드로 타입 조합 사전 등록
[UnityEngine.Scripting.Preserve]
static void RegisterGenericInstances()
{
_ = new List<MyStruct>(); // List<MyStruct> AOT 생성
_ = new Dictionary<int, MyStruct>(); // 이 조합도 생성
// 실제로 호출되지 않아도 됨 — 컴파일러에게 힌트만 제공
}
// 해결 2: 비제네릭 인터페이스로 추상화
public interface ISerializable
{
byte[] Serialize();
void Deserialize(byte[] data);
}

Player Settings > Other Settings:
- Scripting Backend: IL2CPP
- Api Compatibility Level: .NET Standard 2.1 (크기 감소)
- IL2CPP Code Generation: Faster runtime (기본)
또는 Faster (smaller) builds (빌드 크기 우선)
- Managed Stripping Level:
- Minimal: 안전, 크기 절감 적음
- Low: 권장 시작점
- Medium: link.xml 필요
- High: 적극적 제거, 꼼꼼한 테스트 필요
- Strip Engine Code: ON (엔진 미사용 모듈 제거)

// IL2CPP에서 성능 향상 패턴
// 1. 박싱 제거: 값 타입 인터페이스 사용 주의
interface IProcess { void Run(); }
struct MyStruct : IProcess
{
public void Run() { }
}
// (IProcess)new MyStruct() → 박싱 발생!
// → class로 변경하거나 제네릭 제약 사용
void Execute<T>(T item) where T : IProcess => item.Run(); // 박싱 없음
// 2. 델리게이트 캐싱
// ❌ 매 프레임 델리게이트 생성
void Update() => StartCoroutine(DoSomething());
// ✅ 델리게이트 캐시
private Action _cachedAction;
void Awake() => _cachedAction = DoSomething;
void Update() => _cachedAction();
// 3. string.Format 대신 StringBuilder 또는 보간
// ❌ GC 발생
string msg = string.Format("HP: {0}/{1}", hp, maxHp);
// ✅ Span 기반 (IL2CPP에서도 동작)
Span<char> buf = stackalloc char[32];
hp.TryFormat(buf, out _);

// 1. 불필요한 패키지 제거
// Package Manager에서 미사용 패키지 언인스톨
// 2. 텍스처 압축 설정 확인
// Player Settings > Android: ETC2 or ASTC
// 3. 스크립트 최적화 레벨
// Build Settings > Development Build 해제 시
// IL2CPP compiler configuration: Release
// 4. 어셈블리 분리로 스트리핑 효율화
// Editor-only 코드를 별도 Assembly Definition으로 분리
// [assembly: AssemblyIsEditorAssembly] → 런타임 빌드 제외

IL2CPP 빌드에서 런타임 오류의 90%는 스트리핑 문제입니다. link.xml로 리플렉션 대상 타입을 보존하고, [Preserve] 어트리뷰트로 동적 접근 메서드를 보호하세요. 제네릭 AOT 문제는 더미 등록 메서드로 사전에 차단하고, Managed Stripping Level은 Medium부터 시작해 디바이스 테스트를 통해 조금씩 올리세요.