https://qiita.com/sapphire_al2o3/items/4f517523f50e0113af1f
Unity 코드를 쓸 때 GC Alloc가 발생하는 패턴을 조사했다. GC Alloc이 줄어들 패턴 good했지만, 코드가 알기 어려울 경우도 있으므로 반드시 개서를 할 필요는 없다.
Unity 2018.4에서 동작 확인
문자와 숫자의 연결
bad
int i = 123 ;
string s = "num_" + i ;
good
int i = 123 ;
string s = "num_" + i . ToString ();
string.Concat에 object로 전달되어 박스가되어 버리므로 문자열한다.
4 개의 문자열 연결
bad
string [] num = { "0" , "1" , "2" , "3" , "4" , "5" };
// 96byte
s = num [ 0 ];
s + = num [ 1 ];
s + = num [ 2 ];
s + = num [ 3 ];
good
string [] num = { "0" , "1" , "2" , "3" , "4" , "5" };
// 34byte
string s = num [ 0 ] + num [ 1 ] + num [ 2 ] + num [ 3 ];
연결 할 때마다 새로운 문자열을 생성되는데, 4 개까지 연결라면 Concat (string, string, string, string)이 알려져 번에 연결 할 수있다.
루프에서 문자열 연결
good
var sb = new System . Text . StringBuilder ();
for ( int i = 0 ; i < num . Length ; i ++)
{
sb . Append ( num [ i ]);
}
string s = sb . ToString ();
루프에서 많은 문자열 연결 할 수 있으면 한번씩 문자열이 생성되지 않도록 StringBuilder를 사용한다.
문자열 배열의 연결
good
string [] num = { "0" , "1" , "2" , "3" , "4" , "5" };
// 38byte
string s = string . Join ( "" , num );
문자열 배열을 연결하는 경우 Join을 사용한다.
string.Format
bad
string [] num = new string [ 10 ];
for ( int i = 0 ; i < num . Length ; i ++)
{
// 84byte
num [ i ] = string . Format ( "num_ {0}" , i ) ;
}
good
string [] num = new string [ 10 ];
for ( int i = 0 ; i < num . Length ; i ++)
{
// 64byte
num [ i ] = $ "num_ { i . ToString ()} " ;
}
string.Format보다 문자열 보간 ($ "")를 사용한다. 문자열 보간 쪽이 선명 인수가 문자열 만 있으면 간단한 문자열 연결됩니다 string.Format가 호출되지 않습니다.
StringBuilder에 숫자 추가
var sb = new System . Text . StringBuilder ();
int n = 100 ;
sb . Append ( n );
.NET Framework에서는 정수는 ToString되어 추가된다. ToString하지 않으 문자 단위로 추가 할 필요가있다.
https://gist.github.com/sapphire-al2o3/ba7d6a80836a2e5ee117abb4c3d75132
Enum 캐스트
bad
// 40byte
EnumType e = ( EnumType ) System . Enum . ToObject ( typeof ( EnumType ), i );
// 40byte
i = System . Convert . ToInt32 ( e );
good
// 0byte
EnumType e = ( EnumType ) i ;
// 0byte
i = ( int ) e ;
Enum을 Dictionary의 Key하지 (.NET 3.5)
bad
var d = new Dictionary < EnumType , int > ();
good
var d = new Dictionary < int , int > ();
.NET 4라면 박스하지 않기 때문에 Enum을 Key하고 접근해서 GC Alloc는 발생하지 않지만, 저속이므로 int를 Key로하는 것이 좋다.
코루틴 반환
bad
// 20byte
yield return 0 ;
good
// 0byte
yield return null ;
0을 반환하는 경우, 박스 화하여 GC Alloc가 발생한다.
코루찐에서 foreach
bad
IEnumrator Sum ( int [] array )
{
int s = 0 ;
foreach ( var a in array )
{
s + = a ;
yield return null ;
}
}
int [] array = { 1 , 2 , 3 , 4 , 5 };
StartCoroutine ( Sum ());
good
IEnumrator Sum ( int [] array )
{
int s = 0 ;
for ( int i = 0 ; i < array . Length ; i ++)
{
s + = a [ i ];
yield return null ;
}
}
int [] array = { 1 , 2 , 3 , 4 , 5 };
StartCoroutine ( Sum ());
foreach보다 for 쪽이 임시 변수가 적기 때문에 코루찐의 크기가 작아진다.
List의 크기 지정을
bad
List < int > list = new List < int > ();
// 8.3Kbyte
for ( int i = 0 ; i < 1000 ; i ++)
{
list . Add ( i );
}
good
List < int > list = new List < int > ( 1000 );
// 4.0Kbyte
for ( int i = 0 ; i < 1000 ; i ++)
{
list . Add ( i );
}
List와 Dictionary 미리 추가하는 크기를 알고있는 경우 생성 할 때 Capacity를 지정한다.
배열 정렬
bad
int [] array = new int [ 100 ];
// 10.9Kbyte
for ( int i = 0 ; i < 100 ; i ++)
{
Array . Sort ( array );
}
good
int [] array = new int [ 100 ];
// 112byte
for ( int i = 0 ; i < 100 ; i ++)
{
Array . Sort ( array , ( x , y ) => x - y );
}
비록 int의 배열에도 람다 식을 지정하지 않으면 불필요한 캐스트가 발생 해 버린다. (.NET Core이라고 괜찮아)
Delegate
bad
void Log ( int value ) { Debug . Log ( value ); }
void Squared ( int value , Action < void > callback )
{
int v = value * 2 ;
callback ( v );
}
int [] num = { 0 , 1 , 2 , 3 , 4 };
foreach ( var n in num )
{
Squared ( n , Log );
}
good
void Log ( int value ) { Debug . Log ( value ); }
void Squared ( int value , Action < void > callback )
{
int v = value * 2 ;
callback ( v );
}
int [] num = { 0 , 1 , 2 , 3 , 4 };
var log = Log ;
foreach ( var n in num )
{
Squared ( n , log );
}
여러 번 호출 함수는 캐시한다.
람다 식의 범위
bad
List < int > RemoveList ( List < int > list , int v )
{
if ( list ! = null && list . Count > 0 )
{
return list . FindAll ( x => x ! = v );
}
return null ;
}
good
List < int > RemoveList ( List < int > list , int v )
{
if ( list ! = null && list . Count > 0 )
{
int t = v ;
return list . FindAll ( x => x ! = t );
}
return null ;
}
list가 null의 경우는 아무것도하지 않지만 bad 쪽은 함수를 호출하는 것만으로 GC Alloc가 발생한다.
함수에 들어갈 때 인수를 캡처 한 람다 표현식이 생성되어 버리기 때문에 인수를 내부의 범위에 캐싱 캡처하면 범위에 들어 가지 않는 경우는 람다 식을 생성한다.
IList 루프
bad
IList list = new string [ 10 ];
foreach ( var e in list ) {}
good
IList list = new string [ 10 ];
for ( int i = 0 ; i < list . Count ; i ++) {}
인터페이스를 통해 foreach를 사용하면 박스가 발생하기 때문에 for를 사용한다.
멤버의 순서
bad
// 80byte
class C
{
byte a ;
long b ;
byte c ;
long d ;
byte e ;
long f ;
byte g ;
long h ;
}
good
// 56byte
class C
{
long b ;
long d ;
long f ;
long h ;
byte a ;
byte c ;
byte e ;
byte g ;
}
Unity는 class에도 자동으로 적절한 메모리 레이아웃이되지 않도록 때문에 불필요한 패딩이 들어 가지 않도록 멤버 정의의 순서를 고려한다.
인코딩
bad
string s = "hoge" ;
int n = System . Text . Encoding . GetEncoding ( "UTF-8" ). GetByteCount ( s );
good
string s = "hoge" ;
int n = System . Text . Encoding . UTF8 . GetByteCount ( s );
GetEncoding을 호출하면 매번 클래스가 생성되기 때문에 캐시되는 UTF8을 사용한다.
Unity의 API
Object.name
bad
for ( int i = 0 ; i < 4 ; i ++)
{
Debug . Log ( go . name );
}
name
속성에 액세스 할 때마다 GC Alloc가 발생하기 때문에 캐시한다.
Application *** Path
- Application.persistentDataPath
- Application.temporaryCachePath
등도 방문 할 때마다 문자열이 생성되어 버리므로 캐시한다.
Renderer.materials
bad
for ( int i = 0 ; i < renderer . materials . length ; i ++)
{
}
good
foreach ( var m in renderer . materials )
{
}
Renderer.materials
속성에 배열을 생성 해 돌려 있기 때문에 루프에서 매번 사용하지 않도록한다.
Input.touches
bad
// 80byte
foreach ( var touch in Input . touches )
{
}
good
// 0byte
for ( int i = 0 ; i < Input . touchCount ; i ++)
{
var touch = Input . GetTouch ( i );
}
이쪽도 배열을 생성 해 돌려 주므로 Input.GetTouch
을 사용한다.
Animator.GetParameter
bad
for ( int i = 0 ; i < animator . parameterCount ; i ++)
{
// animators.parameters [i] .name과 거의 같은
Debug . Log ( animator . GetParameter ( i ). name );
}
good
foreach ( var paramter in animator . parameters )
{
Debug . Log ( paremter . name );
}
Animator.parameters
배열을 반환 API이므로 캐시한다.
Animator.GetParameter
치아 Animator.parameters
를 내부에서 호출있을 뿐이므로 사용하지 않는다.
결과를 배열로 반환 API
good
void GetComponentsInChildren ( List < T > results )
void GetComponentsInChildren ( List < T > results )
void GetComponentsInParent ( bool includeInactive , List < T > results )
int Physics . RaycastNonAlloc (... , RaycastHit [] results , ...)
Animator . GetCurrentAnimatorClipInfo ( int layerIndex , List < AnimatorClipInfo > clips )
몇번이나 호출하는 경우 인수 배열이나 목록을 전달하고 결과를받는 함수를 사용한다.