├── LICENSE └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 I_Jemin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 유니티 최적화 핸드북 (한국어) Unity Optimization Handbook (Korean) 2 | 유니티 최적화 핸드북 입니다. 당분간은 한국어로만 쓰여짐. 3 | 4 | ## 프로젝트 설정 5 | 6 | ### Metal API Validation 7 | Xcode를 통해 GPU 프로파일링을 하는 과정에서 사용합니다. 8 | 따라서 XCode의 Instruments 등을 통해 GPU를 자세히 프로파일링 하지 않는 경우 의미없으므로 해제하는 것이 미세한 오버헤드를 제거하는데 도움이 됩니다. 9 | 10 | ### 사용되지 않는 밉맵 제거 11 | 유니티 프로젝트 설정의 빌드 설정(플레이어 설정)에는 Mipmap strip 설정이 있다. 12 | 이 설정은 Quality 설정의 각 레벨에서 사용되는 텍스쳐 해당도 단계를 수집하여, 사용되지 않을 단계의 밉맵을 빌드에 포함하지 않는다. 13 | 14 | 예를 들어 해당 프로젝트의 퀄리티 설정이 High와 Low가 있고, High가 Full Res, Low가 Half Res를 사용한다면, 텍스쳐의 밉맵 중 이 두가지 단계를 제외한 밉맵들이 빌드에서 제거된다. 15 | 16 | 17 | ## 게임 오브젝트 18 | 19 | ### 어떤 트랜스폼을 다른 트랜스폼의 부모(Parenting)로 지정하는 것을 자제하기 20 | 21 | 우선, 트랜스폼 변화는 자식들에게 전파되므로, 복잡하고 깊은 부모-자식 관계는 연산량이 많아진다. 22 | 23 | 또한, 유니티는 트랜스폼 연산을 최적화 하기위해 각 루트 트랜스폼에서, 루트 트랜스폼에서 모든 자식까지의 트랜스폼 연산 결과를 캐싱한다. 24 | 예를 들어 전체 트랜스폼이 얼마나 많든, 부모가 없는 10개의 트랜스폼이 있다면, 캐싱된 트랜스폼 구조체는 10개가 존재한다. 25 | 26 | 그런데 수많은 트랜스폼을 소수의 트랜스폼의 자식으로 넣게 되면, 계산해서 캐싱해둔 트랜스폼 연산 결과를 자주 버려야 한다. 27 | 또한 자식 트랜스폼이 하나만 변경되도, 해당 자식 트랜스폼에서 루트 부모까지 이어지는 트랜스폼 정보를 다시 계산해야 하기 때문이다. 28 | 29 | 따라서 꼭 어떤 게임 오브젝트의 자식으로 들어갈 필요가 없는 경우, 패런팅 하지 않는다. 30 | 페런팅이 적어질수록, 캐싱해둔 트랜스폼 연산 결과를 강제로 갱신해야하는 상황에 부딫치는 일이 적어진다. 31 | 32 | 33 | ## 할당 34 | 35 | ### Enum의 ToString() 캐싱하기 36 | Enum.ToString()의 성능 오버헤드를 막기 위해 필요하다면 캐싱된 string을 반환하는 확장 메서드를 사용. 이러한 확장 메서드를 자동 생성해주는 패키지도 있다. 37 | 38 | - https://www.meziantou.net/caching-enum-tostring-to-improve-performance.htm 39 | - https://www.nuget.org/packages/Meziantou.Framework.FastEnumToStringGenerator/ 40 | 41 | ### 랜더러의 materials 또는 material 접근을 줄인다 42 | Renderer 컴포넌트에서 현재 사용중인 머티리얼 인스턴스들은 materials를 통해 접근할 수 있다. 43 | 하지만 이 프러퍼티를 통해 머티리얼 목록을 접근할떄마다 할당이 일어난다. 44 | 45 | 따라서 추천되는 방법은 해당 랜더러가 사용하는 머티리얼들이 변경되지 않는다고 확정된 순간에, 46 | GetMaterials()를 통해 머티리얼 목록을 배열로 받아 캐싱하고, 이후에는 랜더러로부터 매번 머티리얼 목록을 조회하는 것이 아니라 캐싱한 머티리얼 배열을 사용하는 것이다. 47 | 48 | ### struct를 반드시 써야할 케이스 49 | 아래 조건을 모두 만족하는 경우 class가 아니라 struct로 구현한다. 50 | 51 | - 어떤 프레임에서 휘발성으로 대량으로 생성하여 사용하고 52 | - 다음 프레임에서는 잊어버리는 데이터 53 | 54 | 정리하면 55 | - 휘발성으로 레퍼런스 타입의 오브젝트를 대량 생성하여 해당 스코프내에서 사용한 직후, 참조를 유지하지 않는 구현은 오버헤드가 매우 크다. 56 | - 힙은 스택보다 느리다. 57 | 58 | ### readonly struct 애용 59 | 생성 직후 값이 변경되지 않는 데이터는 readonly struct를 사용한다. 60 | 61 | ### 로그 통제 62 | 문자열 string 생성은 할당을 발생시킵니다. 따라서 Debug.Log() 등 로그 함수에 문자열을 전달하는 과정에서 할당이 발생합니다. 63 | 플레이어 설정에서 로그 출력 여부를 조정할 수 있지만, 이는 로그 메서드의 입력에 문자열을 전달하면서 발생하는 할당을 제거하는 효과는 없습니다. 64 | 65 | 다만 개발 편의상 로그는 반드시 필요하기 때문에, 릴리즈 빌드에서 출력할 로그 수준을 결정한 다음, Conditional 전처리기로 묶어 랩핑한 버전의 Log() 메서드를 구현해서 사용합니다. 66 | 예를 들어 Log와 Warning 단계만 릴리즈 빌드에서 걷어낼 것이라면 다음과 같이 랩핑한 메서드를 만들어 사용할 수 있습니다. 67 | 68 | 아래 메서드들은 유니티 에디터(UNITY_EDITOR) 또는 개발자용 디버그 심볼(DEVELOPMENT_BUILD)이 활성화된 빌드에서만 존재하며, 이외의 경우 스트립되어 사라집니다. 69 | 따라서 해당 메서드들을 부르면서 생기는 오버헤드가 제거됩니다. 70 | 71 | ``` 72 | [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] 73 | public static void Log(string log, Object context) 74 | { 75 | UnityEngine.Debug.Log(log, context); 76 | } 77 | 78 | [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] 79 | public static void LogWarning(string log, Object context) 80 | { 81 | UnityEngine.Debug.LogWarning(log, context); 82 | } 83 | ``` 84 | 85 | 86 | ## LINQ 87 | 88 | ### Linq를 사용한다면, 잘 사용하자. 89 | 90 | - 성능에 민감한 부분(Update 메서드)에서는 LINQ를 덜 사용하자. 91 | - Update 메서드에서 Linq 또는 같은 동작을 수행하는 코드를 통해 필터링한 결과는 캐싱하는 것이 좋다. 92 | - LINQ 쿼리를 쓸데없이 복잡하게 짜지 않는다. 93 | 94 | linq는 깔끔하고 이해하기 쉬운 코드를 만들 수 있다. 하지만 linq는 할당을 발생시킨다. 이는 GC를 좀더 자주 일으킬 수 있다. 95 | 또한 쿼리를 무의미하게 복잡하게 짠 경우에는 메모리 뿐만 아니라 CPU 오버헤드를 일으킬 수있다. 96 | 97 | 잘못 작성된 linq는 linq를 사용하지 않은 코드와 비교했을때 할당/CPU 타임에서 큰 성능 차이를 발생시킨다. 98 | 하지만 단순한 linq 쿼리 또는 잘 작성된 linq 쿼리는 for 또는 foreach 문을 사용한 구현의 성능과 유의미한 차이를 발생시키지 않는다. 99 | 따라서 프로젝트에서 linq 사용을 금지할 필요는 없다. 100 | 101 | 단, Update() 메서드는 매우 자주 실행되므로, 작성한 linq가 적은 할당을 일으킨다고 해도 반복적인 할당으로 인한 오버헤드가 생길 수 있다. 102 | 따라서 Update 메서드 내에서의 linq 반복 호출은 피한다. 103 | 104 | 또한 CPU 타임이 문제가 되는 것의 본질은 Linq 사용이 그 자체가 아니라, 반복적이거나 쓸데 없이 복잡한 쿼리, 필터링이 문제일 수 있다. 105 | 따라서 Linq를 일반 코드로 전환하는 것 뿐만 아니라, 매번 같은 결과가 예상되는 쿼리를 쓸데없이 반복 수행하지 않도록 쿼리 결과를 캐싱하는 등의 개선 작업이 필요하다. 106 | 107 | ### Any() 보단 Count나 Length를 쓰자. 108 | Any() 메서드는 컬렉션 타입으로부터 Enumerator를 생성하고 MoveNext() 헀을때 true를 반환하는지 검사합니다. 109 | 이예 따라 Count나 Length를 바로 검사하는 것보다 오버헤드가 더 발생합니다. 110 | 111 | 단, 프로퍼티로 제공되는 Count가 아니라 Linq 확장 메서드 Count()를 쓰면 비용이 더 발생하니 주의합니다. 112 | 113 | ## 셰이더 114 | 115 | ### 불필요한 머티리얼 제거 116 | 셰이더 스크립트 내에서 Shader Feature로 선언된 키워드는 머티리얼이 사용하지 않으면 스트립됩니다. 117 | 그런데 셰이더 컴파일 과정에서 수집되는 셰이더 키워드는 빌드에 포함되는 머티리얼이 아닌, 프로젝트에 포함된 전체 머티리얼입니다. 118 | 119 | 따라서 사용하지 않는 머티리얼도 셰이더 컴파일 과저엥서 셰이더 키워드 조합을 늘릴 수 있기 때문에 제거하는 것이 좋습니다. 120 | 121 | ## 미신 122 | 123 | ### TryGetComponent 가 성능이 더 좋다. 124 | 공식 문서에 언급되는 장점은 에디터에서 할당이 발생하지 않는 것이다. 하지만 플레이어 빌드에서의 성능 변화는 언급되어 있지 않다. 125 | 하지만 유니티 코리아 엔지니어에게 문의한 결과, 아래 코드들은 플레이어 빌드에서 성능 차이가 없는 것으로 나온다. 126 | 127 | ``` 128 | if(!TryGetComponent(out _)) 129 | { 130 | Debug.Log("comp is null"); 131 | } 132 | ``` 133 | 134 | ``` 135 | if(GetComponent() == null) 136 | { 137 | Debug.Log("comp is null"); 138 | } 139 | ``` 140 | 141 | 142 | ### enum을 Dictionary의 키로 사용하면 할당이 일어난다. 143 | IEqualityComparer가 특정되지 않는 타입을 딕셔너리의 키로 사용하면 비교하는 과정에서 ObjectEquilaityComparer가 사용되면서 벨류타입의 경우 박싱이 일어난다. 144 | 하지만 유니티에서 활성화활 수 있는 닷넷 버전이 4.x 가 되면서 Dictionary의 키로 사용된 enum 비교에서 EnumEquilityComparer가 사용되면서 이 문제는 더이상 유효하지 않다. --------------------------------------------------------------------------------