특별 최적화
Apply advanced optimizations only after profiling confirms they address a specific performance problem.
읽는 시간 9분최근 업데이트: 한 달 전
이전 섹션에서는 모든 프로젝트에 적용되는 최적화에 대해 설명했지만, 이 섹션에서는 프로파일링 데이터를 수집하기 전에 적용해서는 안 되는 최적화에 대해 자세히 설명합니다. 최적화를 구현하는 데 많은 작업이 필요하거나, 코드의 청결성 또는 유지 관리 기능이 성능 향상에 영향을 미치거나, 특정 규모에서만 나타나는 문제를 해결할 수 있기 때문일 수 있습니다.
다차원 배열 vs 가변 배열
이 StackOverflow 문서에서 설명한 바와 같이 다차원 배열에는 함수 호출이 필요하기 때문에 일반적으로 다차원 배열보다 가변 배열을 반복하는 것이 더 효율적입니다.
참고:
-
이는 배열의 배열이며,대신
type[x,y로 선언됩니다.type[x][y] -
ILSpy 또는 유사 툴을 사용하여 다차원 배열에 액세스할 때 생성되는 IL을 검사하면 이를 파악할 수 있습니다.
Unity 5.3 버전에서 프로파일링했을 때, 3차원의 100x100x100 배열을 100번 완벽히 연속 반복했을 경우 다음과 같은 시간이 소요되었으며, 이는 테스트를 10번 이상 수행해서 평균을 계산했습니다.
배열 종류 | 전체 시간 (100회 반복) |
|---|---|
| 1차원 배열 | 660ms |
| 가변 배열 | 730ms |
| 다차원 배열 | 3,470ms |
추가 함수 호출 비용은 다차원 배열 vs 1차원 배열을 액세스하는 비용 차이에서 확인할 수 있으며, 간결하지 않은 메모리 구조를 반복하는 비용은 가변 배열 vs 일차원 배열의 비용 차이에서 확인할 수 있습니다.
위에서 설명했듯이, 추가 함수 호출 비용은 간결하지 않은 메모리 구조를 사용하여 든 비용보다 훨씬 더 많이 듭니다.
성능에 민감한 작업의 경우 1차원 배열을 사용하는 것이 좋습니다. 여러 차원이 있는 배열이 필요한 다른 모든 경우에는 가변 배열을 사용합니다. 다차원 배열은 사용하면 안 됩니다.
파티클 시스템 풀링
파티클 시스템을 풀링할 때는 파티클 시스템이 3500바이트 이상의 메모리를 사용한다는 점을 알고 있어야 합니다. 파티클 시스템에서 활성화된 모듈 수에 따라 메모리 사용량이 증가합니다. 파티클 시스템이 비활성화되면 이 메모리는 해제되지 않고 파티클 시스템이 파괴될 때만 해제됩니다.
Unity 5.3부터는 대부분의 파티클 시스템 설정을 런타임 시 조작할 수 있습니다. 여러 가지 다양한 파티클 효과를 풀링해야 하는 프로젝트의 경우, 파티클 시스템의 설정 파라미터를 데이터 캐리어 클래스나 구조체로 추출하는 것이 더 효율적일 수 있습니다.
파티클 효과가 필요한 경우 '일반' 파티클 효과의 풀이 필요한 파티클 효과 오브젝트를 제공할 수 있습니다. 그런 다음 설정 데이터를 오브젝트에 적용하여 원하는 그래픽 효과를 구현할 수 있습니다.
이것은 주어진 씬에서 사용되는 파티클 시스템의 가능한 모든 배리언트와 설정을 풀링하는 것보다 훨씬 더 메모리 효율적이지만 상당한 엔지니어링 노력이 필요합니다.
업데이트 관리자
Unity는 내부적으로 , , 같은 콜백에 관련된 오브젝트 리스트를 추적합니다. 리스트가 일정한 시간에 업데이트되도록 하기 위해 이러한 리스트는 침투적 방식으로 연결된 리스트로 유지됩니다. MonoBehaviour는 활성화 또는 비활성화 시 각각 이러한 리스트에 추가되거나 제거됩니다.
UpdateFixedUpdateLateUpdate필요한 MonoBehaviour에 적절한 콜백을 추가하는 것이 편리하지만 콜백 수가 증가함에 따라 비효율성이 증가합니다. 네이티브 코드에서 관리되는 코드 콜백을 호출하는 데 드는 오버헤드는 작지만 상당합니다. 그러면 많은 수의 프레임당 메서드를 호출할 때 프레임 시간이 저하되고, 많은 수의 MonoBehaviour가 포함된 프리팹을 인스턴스화할 때 인스턴스화 시간이 저하됩니다. (참고: 인스턴스화 비용은 프리팹의 각 컴포넌트에 Awake 및 OnEnable 콜백을 호츨하는 성능 오버헤드로 인해 발생합니다.)
프레임당 콜백이 있는 MonoBehaviour의 수가 수백 또는 수천으로 증가하면 이러한 콜백을 제거하고 대신 MonoBehaviour(또는 표준 C# 오브젝트)를 전역 관리자 싱글톤에 연결하는 것이 유용합니다. 그 후 글로벌 관리자 싱글톤은 관심 있는 오브젝트에 , , 기타 콜백을 배포할 수 있습니다. 이렇게 하면 코드가 콜백이 작동하지 않을 때 콜백의 구독을 현명하게 취소할 수 있으므로 각 프레임마다 호출해야 하는 함수의 수가 줄어든다는 추가적인 이점이 있습니다.
UpdateLateUpdate가장 많이 절약할 수 있는 방법은 보통 거의 실행하지 않는 콜백을 제거하는 것입니다. 다음과 같은 유사 코드를 살펴보십시오.
void Update() { if(!someVeryRareCondition) { return; }// … some operation …}
위와 유사한 업데이트 콜백이 있는 MonoBehaviour의 수가 많은 경우 업데이트 콜백을 실행하는 데 소요되는 시간 중 상당한 시간이 MonoBehaviour 실행을 위한 네이티브 및 관리되는 코드 도메인 간에 전환하는 데 소요되며, 이 도메인은 즉시 종료됩니다. 대신 이러한 클래스가 이 true인 동안 전역 업데이트 관리자에만 구독하고 그 후 구독이 취소되면 코드 도메인 전환과 드문 상태 평가에 대한 시간을 절약할 수 있습니다.
someVeryRareCondition업데이트 관리자에서 C# 델리게이트 사용
일반 C# 델리게이트를 사용하여 이러한 콜백을 구현하는 것은 유용합니다. 하지만 C#의 델리게이트 구현은 낮은 구독률 및 구독 취소율과 적은 수의 콜백에 최적화되어 있습니다. C# 델리게이트는 콜백이 추가되거나 제거될 때마다 콜백 리스트의 전체 깊은 복사를 수행합니다. 콜백 리스트가 많거나 단일 프레임 동안 콜백이 많이 구독/구독 취소되면 내부 메서드에서 성능 스파이크가 발생합니다.
Delegate.Combine추가/제거가 빈번히 일어날 경우, 델리게이트 대신 빠른 삽입/제거를 위해 만들어진 데이터 구조를 사용하는 것을 고려해 보십시오.
스레드 컨트롤 로딩
Unity를 사용하면 개발자가 데이터를 로드하는 데 사용되는 배경 스레드의 우선 순위를 제어할 수 있습니다. 이는 에셋 번들을 백그라운드의 디스크로 스트리밍하려고 할 때 특히 중요합니다.
메인 스레드와 그래픽스 스레드의 우선 순위는 모두 입니다. 우선 순위가 높은 스레드는 메인/그래픽스 스레드를 우선시하고 프레임 속도 끊김을 일으키지만 우선 순위가 낮은 스레드는 그렇지 않습니다. 스레드의 우선 순위가 메인 스레드와 동일한 경우 CPU는 동일한 시간을 스레드에 제공하려고 시도합니다. 이는 일반적으로 여러 백그라운드 스레드가 에셋 번들 압축 풀기와 같은 큰 작업을 수행하는 경우 프레임 속도 저하를 초래합니다.
ThreadPriority.Normal현재 세 곳에서 우선 순위를 조정할 수 있습니다.
먼저 및 같은 에셋 로딩 호출의 기본 우선 순위는 Application.backgroundLoadingPriority 설정에서 가져옵니다. 문서화된 바와 같이 이 호출은 에셋 로딩이 프레임 시간에 미치는 영향을 제한하기 위해 메인 스레드가 에셋을 연동하는 데 소요하는 시간을 제한합니다. (참고: 대부분의 Unity 에셋 유형은 메인 스레드에 '통합'되어야 합니다. 연동 중에 에셋 초기화가 완료되고 특정 스레드 세이프 작업이 수행됩니다. 여기에는 Awake 콜백과 같은 스크립팅 콜백 호출이 포함됩니다. 자세한 내용은 '리소스 관리' 가이드를 참조하십시오.)
Resources.LoadAsyncAssetBundle.LoadAssetAsync둘째, 각 비동기 에셋 로딩 작업과 각 UnityWebRequest 요청은 작업을 모니터링하고 관리하기 위해 오브젝트를 반환합니다. 이 오브젝트는 개별 작업의 우선 순위를 조정하는 데 사용할 수 있는 우선 순위 프로퍼티를 노출합니다.
AsyncOperationAsyncOperation마지막으로, 에 대한 호출에서 반환된 것과 같은 WWW 오브젝트는 threadPriority 프로퍼티를 노출합니다. WWW 오브젝트는 기본값으로 설정을 자동으로 사용하지 않습니다. WWW 오브젝트는 항상 기본값이 입니다.
WWW.LoadFromCacheOrDownloadApplication.backgroundLoadingPriorityThreadPriority.Normal데이터 압축 해제 및 로드에 사용되는 엔진 내부 시스템은 API마다 다르다는 점을 유의하십시오. 와 는 로딩 스레드를 담당하고 자체 속도 제한을 수행하는 Unity의 내부 PreloadManager 시스템에서 작동됩니다. 는 전용 스레드 풀을 사용합니다. 요청이 생성될 때마다 는 완전히 새로운 스레드를 생성합니다.
Resources.LoadAsyncAssetBundle.LoadAssetAsyncUnityWebRequestWWW다른 모든 로딩 메커니즘에는 빌트인 대기열 시스템이 있지만 WWW에는 없습니다. 매우 많은 수의 압축된 에셋 번들에 대한 호출은 동등한 수의 스레드를 생성하며, 이 스레드는 CPU 시간을 놓고 메인 스레드와 경쟁합니다. 이렇게 하면 프레임 속도 저하가 발생할 수 있습니다.
WWW.LoadFromCacheOrDownload따라서 WWW를 사용하여 에셋 번들을 로드하고 압축 해제할 때, 생성되는 각 WWW 오브젝트의 에 적절한 값을 설정하는 것이 가장 좋습니다.
threadPriority매스 오브젝트 이동 & CullingGroups
트랜스폼 조작 섹션에서 언급한 바와 같이 큰 트랜스폼 계층 구조의 이동은 변경 메시지의 전파로 인해 상대적으로 많은 CPU 비용을 소모합니다. 하지만 실제 개발 환경에서는 계층 구조를 적당한 수의 게임 오브젝트로 축소할 수 없는 경우가 많습니다.
동시에 사용자가 눈치채지 못할 동작을 제거하면서 게임 월드에 대한 신뢰성을 유지할 수 있을 정도의 동작만 실행하는 것은 좋은 개발 방향입니다. 예를 들어, 캐릭터 수가 많은 씬에서 화면 위에 있는 캐릭터의 메시 스키닝과 애니메이션 중심의 트랜스폼 이동만 실행하는 것이 항상 더 적합합니다. 화면 밖에 있는 캐릭터에 대한 시뮬레이션의 순수 시각적 요소를 계산하는 데 CPU 시간을 낭비할 필요는 없습니다.
Unity 5.1에서 처음 소개된 API로 이런 문제를 깔끔하게 해결할 수 있습니다. 컬링 그룹
씬에서 대규모 게임 오브젝트 그룹을 직접 조작하는 대신, 컬링 그룹 내의 BoundingSphere 그룹의 Vector3 파라미터를 조작하도록 시스템을 변경합니다. 각 BoundingSphere는 단일 게임 논리적 엔티티의 월드 공간 위치에 대한 권한 저장소 역할을 하며, 엔티티가 컬링 그룹의 메인 카메라 절두체 근처나 안으로 이동할 때 콜백을 수신합니다. 그런 다음 이러한 콜백을 사용하여 엔티티가 보이는 동안에만 실행되어야 하는 동작을 제어하는 코드 또는 컴포넌트(예: 애니메이터)를 활성화 또는 비활성화할 수 있습니다.
메서드 호출 오버헤드 감소
C#의 문자열 라이브러리는 단순한 라이브러리 코드에 메서드 호출을 추가하는 비용에 대한 훌륭한 사례입니다. 빌트인 문자열 API 및 섹션에서 원치 않는 로케일 강제가 억제된 경우에도 직접 코딩된 교체가 빌트인 메서드보다 10-100배 더 빠르다는 것을 언급했습니다.
String.StartsWithString.EndsWith성능 차이의 주요 원인은 단순히 자주 실행되는 내부 루프에 메서드 호출을 추가하는 비용입니다. 호출되는 각 메서드는 메모리에 메서드의 주소를 찾아 스택에 다른 프레임을 푸시해야 합니다. 두 작업 모두 무료는 아니지만 대부분의 코드에서는 무시할 수 있을 정도로 작습니다.
하지만 빠르게 반복하는 루프에서 작은 메서드를 실행할 때 부가적인 메서드 호출을 도입하면서 추가된 오버헤드는 중요하며 심지어 우세할 수 있습니다.
다음의 두 가지 단순 메서드를 고려하십시오.
예제1:
int Accum { get; set; }Accum = 0;for(int i = 0; i < myList.Count; i++) { Accum += myList[i];}
예제2:
int accum = 0;int len = myList.Count;for(int i = 0; i < len; i++) { accum += myList[i];}
두 메서드 모두 C# 일반 에 있는 모든 정수의 합을 계산합니다. 첫 번째 예시는 데이터 값을 보유하기 위해 자동으로 생성된 프로퍼티를 사용한다는 점에서 조금 더 '현대적인 C#'입니다.
List<int>겉으로는 두 개의 코드가 동일해 보이지만, 메서드 호출에 대해 코드를 분석할 경우 차이가 극명합니다.
예제1:
int Accum { get; set; }Accum = 0;for(int i = 0; i < myList.Count; // call to List::getCount i++) { Accum // call to set_Accum+= // call to get_AccummyList[i]; // call to List::get_Value}
루프를 실행할 때마다 메서드 호출이 네 번 있습니다.
- 는
myList.Count프로퍼티에서Count메서드를 호출합니다.get - 프로퍼티의
Accum및get메서드를 호출해야 합니다.set - 더하기 연산에 전달될 수 있도록 의 현재 값을 가져오기 위해
Accum을 호출합니다.get - 더하기 연산 결과를 에 할당하기 위해
Accum를 호출합니다.set - [] 연산자는 이 리스트의 특정 인덱스에 있는 값을 가져오기 위해 리스트의 get_Value 메서드를 호출합니다.
예제2:
int accum = 0;int len = myList.Count;for(int i = 0; i < len; i++) { accum += myList[i]; // call to List::get_Value}
두 번째 예시에서, 호출은 남아 있지만 그 밖의 다른 메서드는 모두 제거됐거나, 더 이상 루프를 반복할 때마다 실행되지 않습니다.
get_Value-
이제이 프로퍼티가 아닌 기본형 값이기 때문에, 값을 설정하거나 가져오기 위해 메서드를 호출할 필요가 없습니다.
accum -
루프가 실행되는 동안는 달라지지 않을 것으로 예상되기 때문에, 해당 루프의 조건문 밖으로 액세스가 이동되어 더 이상 각 루프 반복 작업(iteration)이 시작될 때 실행되지 않습니다.
myList.Count
두 가지 버전의 소요 시간은 특정 코드의 메서드 호출 오버헤드를 75% 제거하는 것의 진정한 이점을 보여 줍니다. 최신 데스크톱 컴퓨터에서 100,000번 실행하는 경우:
- 예제 1은 실행에 324ms 걸립니다.
- 예제 2는 실행에 128ms 걸립니다.
여기서 가장 큰 문제는 Unity가 거의 메서드 인라이닝을 수행하지 않는다는 점입니다. IL2CPP에서도 많은 메서드가 현재 올바르게 인라인되지 않습니다. 이는 특히 프로퍼티의 경우 더욱 그렇습니다. 또한, 가상 및 인터페이스 메서드는 전혀 인라인될 수 없습니다.
따라서 C# 소스에서 선언된 메서드 호출은 최종 바이너리 애플리케이션에서 결국 메서드 호출을 생산하게 될 가능성이 매우 높습니다.
단순 프로퍼티
Unity는 개발자의 편의를 위해 데이터 유형에 대해 여러 개의 '간단한' 상수를 제공합니다. 그러나 위의 내용을 고려하여 이러한 상수는 일반적으로 상수 값을 반환하는 프로퍼티로 구현됩니다.
Vector3.zero의 프로퍼티 실제 코드는 다음과 같습니다.
get { return new Vector3(0,0,0); }
Quaternion.identity도 매우 유사합니다.
get { return new Quaternion(0,0,0,1); }
이러한 프로퍼티에 액세스하는 비용은 프로퍼티 주변의 실제 코드와 비교했을 때는 보통 작은 수준이지만, 프레임당 수천 번(또는 그 이상) 실행될 경우에는 작은 차이가 발생할 수 있습니다.
단순한 기본형의 경우, 값을 대신 사용합니다. 값은 컴파일할 때 인라인화되며, 변수에 대한 참조는 이 값으로 대체됩니다.
constConstconst참고: 변수에 대한 모든 레퍼런스가 해당 값으로 대체되기 때문에 긴 문자열이나 다른 큰 데이터 유형의 를 선언하는 것은 좋지 않습니다. 이렇게 하면 최종 명령어 코드에 중복된 모든 데이터로 인해 최종 바이너리의 크기가 불필요하게 늘어납니다.
constconstconststatic readonlystatic readonly단순 메서드
단순한 메서드는 더 복잡합니다. 기능을 한 번 선언하고 다른 곳에서 재사용하는 것이 매우 유용합니다. 그러나 자주 실행되는 내부 루프에서는 올바른 코딩 사례에서 벗어나 특정 코드를 '수동으로 인라인'해야 할 수 있습니다.
일부 메서드는 완전히 제거할 수 있습니다. , , 을 고려하십시오. 이는 매우 간단한 작업을 수행하며 간단한 할당 문으로 대체할 수 있습니다.
Quaternion.SetTransform.TranslateVector3.Scale더 복잡한 메서드의 경우, 성능이 좋은 코드를 유지하는 데 드는 장기적 비용에 대비해 수동 인라이닝에 대한 프로파일링 내용을 잘 비교해 보십시오.