string 변경이 빈번하다면 stringbuilder 사용하기
c# string은 불변(Immutable)의 속성을 가지고 있다, 즉 한 번 생성되면 값이 바뀔 수 없다. 그렇기 때문에 + 연산자를 사용하여 새로운 문자열을 만들어낼때 임시 객체가 생기게 된다.
한 번의 행동에서 + 연산자를 여러번 쓴다고 해도 임시 문자열은 한번만 생성된다. (string result = “Hello” + userName + “!”;)
그렇지만 새로운 문자열 생성을 위해 for나 while 같은 루프문에서 + 을 사용하면 루프만큼 임시 객체가 생기게 된다.
void ConcatExample(int[] intArray) {
string line = intArray[0].ToString();
for (i = 1; i < intArray.Length; i++) {
line += ", " + intArray[i].ToString();
}
return line;
}
이러한 문제를 해결 하고 방지하기 위해서 자주 변경되는 문자열의 경우 stringbuilder를 사용하여 최적화 할 수 있다.
Collection과 Array의 재사용
컬렉션이나 배열을 사용 할 때 풀링해서 사용하는 방법을 고려하는 것이다.
컬렉션에서는 Clear라는 메서드를 제공하는데, 이 메서드는 컬렉션의 할당된 메모리를 해제하지 않고 내부의 값들만 제거하는 메서드이다.
// 나쁜 예
void Update()
{
List<float> nearestNeighbors = new List<float>();
findDistancesToNearestNeighbors(nearestNeighbors);
nearestNeighbors.Sort();
}
// 좋은 예
List<float> m_NearestNeighbors = new List<float>();
void Update()
{
m_NearestNeighbors.Clear();
findDistancesToNearestNeighbors(NearestNeighbors);
m_NearestNeighbors.Sort();
}
여기서 Update문안에서 계속해서 new로 컬렉션을 재생성하는데 이는 마찬가지로 가비지를 생성하게 된다.
박싱을 생각하면서 코드를 작성하기, 컬렉션 대신 제네릭 컬렉션 사용
엔진을 사용하면서 의도치 않게 메모리를 할당하는 흔한 방법 중 하나 박싱이다. 이는 값타입을 참조 타입처럼 사용하려고 할때 발생 한다.
int x = 1;
object y = new object();
y.Equals(x); // x는 object의 Equals 메소드의 인자로 쓰이기 위해 object로 박싱되었습니다.
또 하나는 제네릭 컬렉션이 아닌 컬렉션을 사용할 때 박싱이 생긴다.
제네릭 컬렉션이 아닌 일반 컬렉션을 사용하면 컬렉션에 값을 넣을때 값타입이든 참조타입이든 object로 캐스팅해서 갑을 넣게 된다. 그러면 이때 가비지가 발생 한다.
이러한 임시 할당으로 인한 문제는 유니티엔진에서 채택한 GC에서는 효율적으로 관리할 수 없기 때문에 피하는게 좋다.
클로저를 줄이기
List<float> listOfNumbers = createListOfRandomNumbers();
int desiredDivisor = getDesiredDivisor();
listOfNumbers.Sort( (x, y) => (int)x.CompareTo((int)(y/desiredDivisor)));
클로저란 로컬 스코프 상위에 존재하는 지역변수를 사용하는 무명메서드 혹은 람다식을 뜻한다.
예시는 위의 코드처럼 Sort의 Compare 함수로 들어간 익명함수에서 알 수 있는데, 익명 메서드 안에서 외부 스코프에 존재하는 desiredDivisor라는 지역변수를 사용하기 때문에 클로저가 된다.
클로저를 실행 할 때 이 클래스의 인스턴스가 생성되고, 힙에 할당되기 때문에 결과적으로 가비지 생성을 하게 된다. 왠만해서는 클로저를 피할수 있다면 피하는 방법이 좋다.
열거형(Enum)을 키(Key)로 사용하는 Dictionary 컬렉션 사용
Dictionary.Add, Dictionary.TryGetValue().. 등 Dictionary의 키를 사용하는 함수들의 경우 내부적으로 Object.GetHashCode(Object)를 호출하는데, enum은 int같은 값 타입이기 때문에 이를 object형으로 감싸는 박싱을 하게 된다.
public class MyEnumComparer : IEqualityComparer<MyEnum> {
public bool Equals(MyEnum x, MyEnum y) {
return x == y;
}
public int GetHashCode(MyEnum x) {
return (int)x;
}
}
이 문제를 해결하기 위해서 IEqualityComparer 인터페이스를 구현한 커스텀 클래스를 만들어서 생성자에 파라미터로 넣어주면 해결 할 수 있다.
Array를 리턴하는 엔진 API 사용시 주의하기
Array를 리턴하는 API 접근시 매 접근마다 배열을 복사로 리턴하게 된다. 그러므로 루프문안에서 해당 API를 사용할 때에는 캐싱해서 사용해야한다.
for(int i = 0; i < mesh.vertices.Length; i++)
{
float x, y, z;
x = mesh.vertices[i].x;
y = mesh.vertices[i].y;
z = mesh.vertices[i].z;
DoSomething(x, y, z);
}
위 코드에서는 매 루프마다 vertices배열을 호출하기 때문에 4번의 배열이 생성된다.(for루프 , x,y,z)
var vertices = mesh.vertices;
for(int i = 0; i < vertices.Length; i++)
{
float x, y, z;
x = vertices[i].x;
y = vertices[i].y;
z = vertices[i].z;
DoSomething(x, y, z);
}
그러나, 루프에 진입하기 전 vertices 배열을 캐싱해놓으면 1번의 할당으로 줄일 수 있다.