유니티 최적화 핸드북 입니다. 당분간은 한국어로만 쓰여짐.
Xcode를 통해 GPU 프로파일링을 하는 과정에서 사용합니다. 따라서 XCode의 Instruments 등을 통해 GPU를 자세히 프로파일링 하지 않는 경우 의미없으므로 해제하는 것이 미세한 오버헤드를 제거하는데 도움이 됩니다.
유니티 프로젝트 설정의 빌드 설정(플레이어 설정)에는 Mipmap strip 설정이 있다. 이 설정은 Quality 설정의 각 레벨에서 사용되는 텍스쳐 해당도 단계를 수집하여, 사용되지 않을 단계의 밉맵을 빌드에 포함하지 않는다.
예를 들어 해당 프로젝트의 퀄리티 설정이 High와 Low가 있고, High가 Full Res, Low가 Half Res를 사용한다면, 텍스쳐의 밉맵 중 이 두가지 단계를 제외한 밉맵들이 빌드에서 제거된다.
우선, 트랜스폼 변화는 자식들에게 전파되므로, 복잡하고 깊은 부모-자식 관계는 연산량이 많아진다.
또한, 유니티는 트랜스폼 연산을 최적화 하기위해 각 루트 트랜스폼에서, 루트 트랜스폼에서 모든 자식까지의 트랜스폼 연산 결과를 캐싱한다. 예를 들어 전체 트랜스폼이 얼마나 많든, 부모가 없는 10개의 트랜스폼이 있다면, 캐싱된 트랜스폼 구조체는 10개가 존재한다.
그런데 수많은 트랜스폼을 소수의 트랜스폼의 자식으로 넣게 되면, 계산해서 캐싱해둔 트랜스폼 연산 결과를 자주 버려야 한다. 또한 자식 트랜스폼이 하나만 변경되도, 해당 자식 트랜스폼에서 루트 부모까지 이어지는 트랜스폼 정보를 다시 계산해야 하기 때문이다.
따라서 꼭 어떤 게임 오브젝트의 자식으로 들어갈 필요가 없는 경우, 패런팅 하지 않는다. 페런팅이 적어질수록, 캐싱해둔 트랜스폼 연산 결과를 강제로 갱신해야하는 상황에 부딫치는 일이 적어진다.
Enum.ToString()의 성능 오버헤드를 막기 위해 필요하다면 캐싱된 string을 반환하는 확장 메서드를 사용. 이러한 확장 메서드를 자동 생성해주는 패키지도 있다.
- https://www.meziantou.net/caching-enum-tostring-to-improve-performance.htm
- https://www.nuget.org/packages/Meziantou.Framework.FastEnumToStringGenerator/
Renderer 컴포넌트에서 현재 사용중인 머티리얼 인스턴스들은 materials를 통해 접근할 수 있다. 하지만 이 프러퍼티를 통해 머티리얼 목록을 접근할떄마다 할당이 일어난다.
따라서 추천되는 방법은 해당 랜더러가 사용하는 머티리얼들이 변경되지 않는다고 확정된 순간에, GetMaterials()를 통해 머티리얼 목록을 배열로 받아 캐싱하고, 이후에는 랜더러로부터 매번 머티리얼 목록을 조회하는 것이 아니라 캐싱한 머티리얼 배열을 사용하는 것이다.
아래 조건을 모두 만족하는 경우 class가 아니라 struct로 구현한다.
- 어떤 프레임에서 휘발성으로 대량으로 생성하여 사용하고
- 다음 프레임에서는 잊어버리는 데이터
정리하면
- 휘발성으로 레퍼런스 타입의 오브젝트를 대량 생성하여 해당 스코프내에서 사용한 직후, 참조를 유지하지 않는 구현은 오버헤드가 매우 크다.
- 힙은 스택보다 느리다.
생성 직후 값이 변경되지 않는 데이터는 readonly struct를 사용한다.
문자열 string 생성은 할당을 발생시킵니다. 따라서 Debug.Log() 등 로그 함수에 문자열을 전달하는 과정에서 할당이 발생합니다. 플레이어 설정에서 로그 출력 여부를 조정할 수 있지만, 이는 로그 메서드의 입력에 문자열을 전달하면서 발생하는 할당을 제거하는 효과는 없습니다.
다만 개발 편의상 로그는 반드시 필요하기 때문에, 릴리즈 빌드에서 출력할 로그 수준을 결정한 다음, Conditional 전처리기로 묶어 랩핑한 버전의 Log() 메서드를 구현해서 사용합니다. 예를 들어 Log와 Warning 단계만 릴리즈 빌드에서 걷어낼 것이라면 다음과 같이 랩핑한 메서드를 만들어 사용할 수 있습니다.
아래 메서드들은 유니티 에디터(UNITY_EDITOR) 또는 개발자용 디버그 심볼(DEVELOPMENT_BUILD)이 활성화된 빌드에서만 존재하며, 이외의 경우 스트립되어 사라집니다. 따라서 해당 메서드들을 부르면서 생기는 오버헤드가 제거됩니다.
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
public static void Log(string log, Object context)
{
UnityEngine.Debug.Log(log, context);
}
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
public static void LogWarning(string log, Object context)
{
UnityEngine.Debug.LogWarning(log, context);
}
- 성능에 민감한 부분(Update 메서드)에서는 LINQ를 덜 사용하자.
- Update 메서드에서 Linq 또는 같은 동작을 수행하는 코드를 통해 필터링한 결과는 캐싱하는 것이 좋다.
- LINQ 쿼리를 쓸데없이 복잡하게 짜지 않는다.
linq는 깔끔하고 이해하기 쉬운 코드를 만들 수 있다. 하지만 linq는 할당을 발생시킨다. 이는 GC를 좀더 자주 일으킬 수 있다. 또한 쿼리를 무의미하게 복잡하게 짠 경우에는 메모리 뿐만 아니라 CPU 오버헤드를 일으킬 수있다.
잘못 작성된 linq는 linq를 사용하지 않은 코드와 비교했을때 할당/CPU 타임에서 큰 성능 차이를 발생시킨다. 하지만 단순한 linq 쿼리 또는 잘 작성된 linq 쿼리는 for 또는 foreach 문을 사용한 구현의 성능과 유의미한 차이를 발생시키지 않는다. 따라서 프로젝트에서 linq 사용을 금지할 필요는 없다.
단, Update() 메서드는 매우 자주 실행되므로, 작성한 linq가 적은 할당을 일으킨다고 해도 반복적인 할당으로 인한 오버헤드가 생길 수 있다. 따라서 Update 메서드 내에서의 linq 반복 호출은 피한다.
또한 CPU 타임이 문제가 되는 것의 본질은 Linq 사용이 그 자체가 아니라, 반복적이거나 쓸데 없이 복잡한 쿼리, 필터링이 문제일 수 있다. 따라서 Linq를 일반 코드로 전환하는 것 뿐만 아니라, 매번 같은 결과가 예상되는 쿼리를 쓸데없이 반복 수행하지 않도록 쿼리 결과를 캐싱하는 등의 개선 작업이 필요하다.
Any() 메서드는 컬렉션 타입으로부터 Enumerator를 생성하고 MoveNext() 헀을때 true를 반환하는지 검사합니다. 이예 따라 Count나 Length를 바로 검사하는 것보다 오버헤드가 더 발생합니다.
단, 프로퍼티로 제공되는 Count가 아니라 Linq 확장 메서드 Count()를 쓰면 비용이 더 발생하니 주의합니다.
셰이더 스크립트 내에서 Shader Feature로 선언된 키워드는 머티리얼이 사용하지 않으면 스트립됩니다. 그런데 셰이더 컴파일 과정에서 수집되는 셰이더 키워드는 빌드에 포함되는 머티리얼이 아닌, 프로젝트에 포함된 전체 머티리얼입니다.
따라서 사용하지 않는 머티리얼도 셰이더 컴파일 과저엥서 셰이더 키워드 조합을 늘릴 수 있기 때문에 제거하는 것이 좋습니다.
공식 문서에 언급되는 장점은 에디터에서 할당이 발생하지 않는 것이다. 하지만 플레이어 빌드에서의 성능 변화는 언급되어 있지 않다. 하지만 유니티 코리아 엔지니어에게 문의한 결과, 아래 코드들은 플레이어 빌드에서 성능 차이가 없는 것으로 나온다.
if(!TryGetComponent<Component>(out _))
{
Debug.Log("comp is null");
}
if(GetComponent<Component>() == null)
{
Debug.Log("comp is null");
}
IEqualityComparer가 특정되지 않는 타입을 딕셔너리의 키로 사용하면 비교하는 과정에서 ObjectEquilaityComparer가 사용되면서 벨류타입의 경우 박싱이 일어난다. 하지만 유니티에서 활성화활 수 있는 닷넷 버전이 4.x 가 되면서 Dictionary의 키로 사용된 enum 비교에서 EnumEquilityComparer가 사용되면서 이 문제는 더이상 유효하지 않다.