2021년 2월 17일 수요일

[Unity] GC Alloc를 발생시키지 않는 C# 코딩

 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 )

몇번이나 호출하는 경우 인수 배열이나 목록을 전달하고 결과를받는 함수를 사용한다.

0 comments:

댓글 쓰기