본문 바로가기
Unity 게임 개발/Unity IL2CPP

유니티 성능향상 - IL2CPP 4: IL2CPP의 제한사항 (Generic)

by daisy0461 2022. 4. 3.

IL2CPP의 마지막 글이다.
혹시 이전 글들을 읽고 오지 않았다면 읽고 오는 것을 추천한다.

유니티 성능향상 - IL2CPP 3: IL2CPP란? - https://daisy0461.tistory.com/m/52

 

유니티 성능향상 - IL2CPP 3: IL2CPP란?

혹시 앞의 글을 읽지 않았다면 유니티 성능향상 - IL2CPP 2: mono, JIT방식, IL이란? - https://daisy0461.tistory.com/m/51 유니티 성능향상 - IL2CPP 2: mono, JIT방식, IL이란? 유니티 성능향상 - IL2CPP 1: C#..

daisy0461.tistory.com

이전 글과 동일하게 아래의 동영상을 참고해서 공부를 진행하였다.

https://youtu.be/-9X965jXrn8


IL2CPP의 제한사항

IL2CPP를 사용할 때 제한사항도 존재한다. C#의 Generic Sharing이 대표적인 제한사항이다. C#이랑 C++의 파생은 C로 동일한데 언어가 많이 다르기 때문에 쉽게 매치되는 부분이 있지만 그렇지 않은 부분도 존재한다. 매칭이 잘 되지 않는 부분 중 하나가 Generic 함수이다.

Generic

Generic은 하나의 클래스 또는 메소드를 가지고 여러타입에 재사용할 수 있도록 하는 것이다. 여러 타입에 적용이 가능해서 코딩 시간을 많이 단축시킨다. Generic은 C#에서 자주 사용하는 코드 중 하나이다.
C++에도 Template라는 형태로 Generic함수같은 형태를 제공한다. Template는 예를 들어

sub <const T, const T>{
. . .
}

라고 만들었을 때 int타입을 넣으면 sub의 파라미터가 전부 int형으로 알아서 ‘작성’해준다. float형이면 float형으로 작성을 해준다.

sub<int T, int T>{
. . .
}

sub<float T, float T>{
. . .
}

즉, Template를 사용하면 편하게 코드를 작성할 수 있지만 코드의 길이가 길어진다.

여기까지 보면 C++의 Template랑 Generic 함수랑동일하다고 생각할 수 있다. 차이점은 call by value(데이터를 복제해서 넘김) vs call by reference(데이터의 위치 정보를 넘김)에 있다.

call by value vs call by reference

call by value는 데이터를 하나 더 만드는 것이고 call by reference는 포인터 개념으로 위치를 넘겨준다. call by value와 call by reference의 차이점 하나를 더 말하자면 call by value는 사용자의 입장에서는 편하지만 데이터를 통째로 넘겨주기 때문에 데이터 전달 속도가 느리다. call by reference는 메모리만 넘겨주기 때문에 데이터 전달 속도가 빠르다. 하지만 사용자 측면에서는 코딩 난이도가 증가한다.

Struct A{
	int a, b, c, d, e, f, g
}

위와 같은 Struct가 있다면 call by value방식으로 A를 불렀을 때 28byte가 넘어간다. 즉 구조체 통째로 넘어가는 것이다. 하지만 call by reference방식으로 A를 부르면 8byte(주소값의 크기가 8byte)가 넘어간다. 그렇기 때문에 더 빠르다.

C#은 call by value를 사용하냐 call by reference를 사용하냐는 어떤 데이터형을 사용하는지에 따라 자동적으로 결정된다.
call by value: struct, int, char, bool…등 기본타입, 구조체 (데이터 탭 작은것들)
call by reference: Class, string, array… 등 value에 포함되는 것 외의 것들
C#에서 Generic은 매우 자주 사용된다. 예를 들면 GetComponent<>형이 있다.

여기서 생기는 문제는 Generic하고 C++하고 call by value냐 call by reference이냐 그리고 AOT방식이냐 JIT방식이냐의 문제들 때문에 Generic을 바로 Template로 바꿀 수 없다.

C#의 Generic이 C++로 변환되면 Generic은 굉장히 길게 풀어서 적어야한다. (Generic의 call by reference의 경우 Generic을 하나로 만들어서 공유할 수 있다. 이것이 Generic sharing이다.)
길게 풀어서 적어야하기 때문에 C#에서 Generic을 많이 사용하면 사용할수록 IL2CPP컴파일 또는 변환 과정이 굉장히 오래걸린다.

Project Setting에서  Build방식

Project Setting -> Player -> IL2CPP Code로 순차적으로 들어가면 Generation에서 Faster runtime으로 할것인지 Faster(smaller) buids를 할 것인지 두가지 선택이 있다.

Faster runtime은 기존 방식 그대로 call by value타입이면 코드 복제본을 만들어서 코드 사이즈가 늘어나는 과정을 실시해서 컴파일 과정이 오래 걸린다.
Faster build는 call by value type도 pointer로 한겹 더 싸서 하나의 공유 코드로 퉁쳐서 사용할 수 있게 한다. 이러면 코드를 풀어내는 것이 아니기 때문에 코드 사이즈를 절약하고 그로인해 빌드 사이즈도 줄어든다. 다만 포인터로 받아가서 type 추론도 하고 추론한 것을 다시 원래 데이터로 복구하는 과정이 추가적으로 들어가서 런타임 성능이 줄어든다.

빌드 후 실험을 할때는 Faster build를 사용하면 개발 이터레이션이 줄어든다. 개발과정에서만 사용하고 릴리즈할 때에는 Faster runtime으로 사용하는 것을 권장한다. 이 두가지 선택지는 프로젝트의 사이즈가 크면 클수록 빌드할 때 속도차이가 많이 난다.
Faster build가 full Generic sharing을 사용. 말그대로 call by value type도 퉁쳐서 하나의 Generic코드로 공유해서 사용할 수 있도록 한다.