무엇을 찾고 계신가요?
Engine & platform

버스트 인스펙터 검색 성능 개선

JONAS REHOLT / UNITY TECHNOLOGIESStudent Software Developer
Jul 11, 2023|7 분
버스트 인스펙터 검색 성능 개선
이 웹페이지는 이해를 돕기 위해 기계 번역으로 제공됩니다. 기계 번역으로 제공되는 콘텐츠에 대한 정확도나 신뢰도는 보장되지 않습니다. 번역된 콘텐츠의 정확도에 관해 의문이 있는 경우 웹페이지의 공식 영어 원문을 참고해 주시기 바랍니다.

저는 버스트 팀에서 일하는 학생입니다. 최근 버스트 인스펙터의 성능 개선을 가능하게 한 최적화 여정을 블로그에 공유하고자 합니다. 이제 버스트 인스펙터 검색 속도가 13배 빨라져 개발자가 프로젝트를 최적화할 때 중요한 코드에 더 빠르게 집중할 수 있습니다.

Unity 프로파일러를 사용하여 프로그램의 성능 병목 현상을 조사하는 방법과 이를 해결하는 방법을 알아보려면 계속 읽어보세요.

버스트 인스펙터 소개

Unity 버스트 컴파일러는 C# 코드를 고도로 최적화된 어셈블리 코드로 변환합니다. 버스트 인스펙터를 사용하면 Unity 에디터에서 바로 해당 어셈블리 코드를 검사할 수 있으므로 간단한 코드 검사를 위해 외부 툴을 사용할 필요가 없습니다.

버스트 인스펙터를 처음 열고 표시할 대상 작업을 선택하면 아래 이미지와 유사한 창이 표시됩니다.

버스트 대상 작업의 컴파일된 어셈블리 코드를 보여주는 버스트 인스펙터 창
버스트 대상 작업의 컴파일된 어셈블리 코드를 보여주는 버스트 인스펙터 창

보시다시피 버스트 인스펙터는 구문 강조 표시, 분기 흐름 화살표 등을 제공합니다.

인스펙터는 선택한 대상 기능을 구현하는 어셈블리로 스크롤을 시도하지만, 어셈블리 보기에서 특정 지침, 주석 등을 검색하는 것도 유용합니다. 이 블로그 게시물의 주제에 대해 알아봅시다.

텍스트 검색 성능 개선

검색을 수행하려면 인스펙터가 원본 어셈블리 출력을 검색하고 이러한 인덱스를 인스펙터 뷰의 위치로 변환해야 합니다. 원래의 검색 기능은 아래 표시된 패턴을 따랐으며 System.String.IndexOf(*)의 구현에 크게 의존했습니다.

while (assemblyCode.IndexOf(key, accIdx) >= 0) {
// ...	
// Do logic for handling search hits
// ...
}

일반적인 검색 히트(총 21,769개의 히트)에 대해 135,582줄의 어셈블리 코드에서 위 검색을 실행한 결과 첫 번째 검색은 약 12초, 후속 검색은 약 5초의 실행 시간이 소요되었습니다. 이는 GUI 이벤트를 기다리는 데 있어 바람직한 대기 시간이 아니기 때문에 무언가 조치를 취해야 했습니다. Unity 프로파일러를 통해 검색을 실행한 결과, 아래와 같이 실행 시간의 37.3%가 IndexOf(*)에 소요된 것으로 나타났습니다.

버스트 인스펙터에서 공통 문자열 검색의 프로파일링 실행
버스트 인스펙터에서 공통 문자열 검색의 프로파일링 실행

합리적인 최적화를 위해서는 사용자 정의 구현을 하거나 알고리즘을 완전히 변경하여 이 함수에 대한 의존도를 해결해야 합니다. 어떤 알고리즘을 사용하든 전체 문자열을 단계적으로 처리해야 합니다. 따라서 일치하는 항목을 찾기 위한 몇 가지 사용자 지정 구현이 필요합니다. 이 점을 감안할 때 원래 알고리즘을 유지하되 사용자 정의 IndexOf 함수를 만드는 것으로 최적화를 시작하는 것이 적절해 보였습니다.

3.34초가 소요된 LongTextArea.GetFragNrFromBlockIdx() 는 색이 지정되지 않은 어셈블리 코드를 검색하는 데서 비롯된 것입니다. 검색을 수행하는 데 사용됩니다. 현재 버스트 인스펙터는 어셈블리 코드를 렌더링을 위해 포맷된 상태로 한 번, 포맷되지 않은 상태로 한 번, 두 번 저장합니다.

사용자 지정 함수를 작성하면 현재 검색 히트마다 호출이 하나씩 추가되므로 호출 횟수를 줄일 수 있는 좋은 효과도 있습니다.

IndexOf(*) 의 소스 코드를 보면 강력한 일반 구현에 필요한 많은 안전 점검을 확인할 수 있습니다. 하지만 저희의 경우에는 이러한 확인 사항이 대부분 사실이라고 안전하게 가정할 수 있습니다. 성능을 최대한 끌어올리려면 바운드 체크 같은 것을 피하기 위해 C와 유사한 함수를 만들어야 합니다.

아래 의사 코드에 따라 함수를 작성할 수 있으며, 여기서 IsKeyMatch(*)는 단순히 키가 일치하는지 여부를 확인합니다.

List<int> Search(string assemblyCode, string key, int accIdx) {
     var hits = new List<int>();
     for (i = accIdx; i < assemblyCode.len - key.len; i++) {
	     if (IsKeyMatch(assemblyCode, key, i)) {
		     hits.add(i);
	     	     i += key.len-1;
          }
     }
     return hits;
}

그러나 C#은 관리형 언어이므로 이 C와 유사한 함수는 가비지 컬렉터가 메모리 주소를 재배치하지 않도록 사용된 관리형 객체를 고정해야 합니다. 다음은 상용구 코드입니다:

unsafe {
	fixed (char* source = assemblyCode) {
		fixed (char* needle = key) {
			CustomIndexOf(source, key)
		}
	}
}

이러한 것들을 종합하면 원래의 동안 루프를 인덱스 파인더에 대한 단일 호출과 검색 히트를 처리하는 로직으로 분리할 수 있습니다:

matches = FindAllMathces(text, key)
foreach match {
	...
	Do logic for handling search hits
	...
}

어떤 이득이 있었나요? 이전의 작은 예시를 사용하면 이 코드 변경으로 초기 호출에서는 6.6배, 후속 호출에서는 13.2배 속도가 향상됩니다(이전/새로운 호출로 측정). 초기 검색에서 속도가 느려지는 것은 색상 문자열에서 일치하는 항목을 찾지 않기 위해 포맷되지 않은 어셈블리를 로드하는 데 따른 오버헤드 때문입니다.

버스트 인스펙터에서 텍스트 검색의 런타임 측정
버스트 인스펙터에서 텍스트 검색의 런타임 측정

이러한 개선 사항으로 인해 이제 22,000건 미만의 대용량 검색의 경우 초기 검색에는 약 1.8초, 후속 검색에는 약 0.4초가 소요됩니다. 따라서 검색할 때마다 차 한 잔을 마실 시간이 부족하지 않으므로 대규모 어셈블리에서 버스트 인스펙터를 더 유용하게 사용할 수 있습니다.

지금 Burst 1.8.7 패키지를 통해 이러한 성능 개선의 이점을 누릴 수 있습니다.

버스트에 대해 더 자세히 알고 싶으신가요? 버스트 포럼에서 저희와 소통하세요. 현재 진행 중인 참호 기술시리즈의 일환으로 다른 유니티 개발자들의 새로운 기술 블로그도 기대해 주세요.