내용 보기

작성자

관리자 (IP : 106.247.248.10)

날짜

2022-10-04 10:30

제목

[C#] 지금 바로 할 수 있는 C# 성능 개선을 위한 작은 테크닉 11개


1. Capacity 설정

System.Collections.Generic에 정의된 List와 같은 클래스는 생성자를 호출할 때 Capacity를 설정하여 쓸데없는 할당을 줄일 수 있다.

List 등은 내부에 배열을 가지고 있어 Add 등의 조작에 의해 그 배열 사이즈에 들어가지 않는 요소 수가 되었을 때에 새롭게 배열을 확보한다.( 현재의 배열 사이즈 x 2 사이즈의 배열을 확보 )

필요한 요소 수가 명확한 경우는 Capacity를 설정해 쓸데없는 할당을 피하여 퍼포먼스 개선을 전망할 수 있다.

샘플 코드

var list = new List<int>(10);

CapacityBenchmark.cs 의 측정 결과

2. StringBuilder 사용

문자열 연결에는 StringBuilder를 사용하는 것이 더 효율적일 수 있다.

C#의 String은 readonly이므로, 문자열을 연결했을 때에는 새로운 캐릭터 라인만큼의 힙 할당이 실행된다.

따라서 여러 번 문자열을 연결할 때 StringBuilder를 사용하여 성능 향상을 기대할 수 있다.

StringBuilder의 내부 구현을 자세하게 해설된 「A deep dive on StringBuilder」  글이 굉장히 알기 쉬우므로 참고하기 바란다.

샘플 코드

var stringBuilder = new StringBuilder(10); // 이것도 capacity 설정 할 수 있으므로 문자열 사이즈를 알 수 있다면 설정하는 것이 좋다
stringBuilder.Append(
"Hello");
stringBuilder.Append(
", ");
stringBuilder.Append(
"World");
stringBuilder.Append(
"!");

StringBuilder.cs 의 측정 결과

3. struct 사용

작은 데이터 사이즈를 표현할 때는 struct를 사용하는 것으로 힙 할당을 피할 수 있다.

아래의 링크의 「구조체 디자인」으로 Microsoft가 DO, DONT를 명시해 주고 있으므로, 구조체 사용법을 잘 모른다면 보는 것을 추천한다.

StructBenchmark

4. struct를 readnly로 설정

가능한 한 struct를 readonly로 만들어 방어적으로 복사를 피할 수 있다.

readonly로 보관 유지된 구조체의 메소드를 호출했을 때에, 보통의 구조체인 채로는 내부에서 값이 재기록 되지 않는 것을 보증 할 수 없다. 이 때문에, 구조체를 전체적으로 복사하고, 이 복사한 것에 대해서 메소드를 호출하는 것으로, 보관 유지하고 있던 구조체의 값이 변하지 않는 것을 보증하는 구조이다.

그러나, 이 원형 복사에 비용이 발생하기 때문에, readonly로 하는 것으로 이 방어적 복사가 발생하지 않게 된다.

방어적 복사 샘플 코드

public struct Huga {
   
public void Say();
}

public class Hoge {
   
private readonly Huga readonlyHuga;

   
private Huga huga;

   
public void Say() {
       readonlyHuga.Say();
       huga.Say();
   }
}

ReadonlyStructBenchmark.cs

5. IEquatable 구현

등가성 판정을 위한 메소드를 제공하는 IEquatable 를 구현하는 것으로, 특히 구조체라면 퍼포먼스 개선을 기대할 수 있다.

C#의 구조체는 아무것도 오버라이드(override) 하지 않는 상태라면 내부적으로는 리플렉션을 사용해 모든 필드 변수의 등가성을 판정하고 있다.

그래서 상당히 느리다.

IEquatable을 구현하는 것으로, 등가성 판정 메소드를 오버라이드(override) 할 수 있어서 어떤 경우에는 퍼포먼스 개선을 전망할 수 있다.

EquatableForStruct.cs

6. boxing 피하기

boxing(값 타입을 참조 타입으로 랩하는 것)을 피하는 것은 당연할지도 모르지만, 의외의 장소에서 boxing 되기도 한다.

예를 들면 문자열 보완을 사용했을 때 등.

샘플 코드
int number = 10;
string message = $"number: {number}"
// 아래처럼 컴파일 된다
// string.Format("number: {0}", number);
// string.Format(string, object);라고 되어서 인수 number는 box화 된다

이러한 경우는 int로서 건네주는 것이 아니라 string로서 건네주는 것으로 box화를 피할 수 있다.

int number = 10;
string message = $"number: {number.ToString()}";

BoxingBenchmark.cs

7. 람다 식을 로컬 함수로

람다 식은 Action 등의 클래스를 내부적으로 생성하지만 로컬 함수는 단순히 해당 클래스의 static 메서드로 취급된다.

따라서 가능한 경우 로컬 함수를 사용하여 쓸데없는 할당을 피할 수 있으면 성능 향상을 기대할 수 있다.

하지만, 로컬 함수를 한층 더 Action으로서 인수에 건네주는 경우 등은, 오히려 퍼포먼스의 저하에 연결되기 때문에 주의가 필요하다.

LocalFunctionBenchmark.cs

8. IEqualityComparer 전달

Dictionary와 같은 생성자는 IEqualityComparer를 받을 수 있다. 이것을 건네주는 것으로, 내부에서 등가성을 판정할 때의 처리를 커스터마이즈 할 수 있다.

예를 들면 유저 정보를 관리하고 있을 때에 유저 고유의 Id 가 존재하고, 이 비교만으로 유저 정보의 등가성을 판정할 수 있는 경우는 그 처리를 정의한 IEqualityComparer 를 구현해 constructor 에 건네주는 것으로, 쓸데없는 등가 성을 위한 처리를 생략할 수 있어 퍼포먼스의 개선으로 이어지는 경우가 있다.

EqualityComparer.cs

9. Conditional을 사용하여 프로덕션 빌드에 포함하지 않기

개발중의 로그 출력은 중요하지만, 프로덕션 빌드 시에는 로그 출력은 포함하고 싶지 않다고 생각한다. (보안적인 우려나 성능적인 우려 등에 의해)

이 때  #if-#endif로 둘러싸는 것도 방법이지만, Conditional 속성을 사용하는 것으로 코드의 전망을 유지하면서 프로덕션 빌드로부터 제외할 수가 있다.

// DEBUG_MOD가 정의 되어 있을때만 이 메소드는 호출할 수 있다
[
Conditinal("DEBUG_MODE")]
public static void Log(string message) {
   Console.WriteLine(message);
}

10. LINQ 사용하지 않음

LINQ는 어떤 시퀸스에 대한 처리를 쓸 수 있는 기능으로 뛰어나지만 내부적으로 Iterator용 클래스를 생성하거나, IEnumerable에 대한 오버로드 밖에 없어서 T[] 등을 일부러 IEnumerable로서 취급해서 조작하는 경우가 있으므로 보통으로 쓰는 것보다는 아무래도 느리다.

결론으로서 케이스 바이 케이스라고 생각합니다만, 궁극적으로 퍼포먼스를 요구할 필요가 있는 개소에서는 「사용하지 않는다」라고 하는 선택사항도 있을까 생각합니다.

또, LINQ에는 ToArray나 ToList라는 메소드가 준비되어 있어, 이것을 호출하는 것으로 즉시 평가를 행한 결과를 취득할 수 있다.

단 foreach 등의 처리를 할뿐이라면 ToArray나 ToList를 호출할 필요는 없다(LINQ 메소드의 반환 값은 기본적으로 IEnumerable<T>)로 지연평가 or 즉각 평가를 이해한 상태에서 필요한 곳에서만 사용하는 것이 좋다

LinqVsPureBenchmark.cs

11. 2의 제곱이 제수가 되는 잉여 계산의 고속화

「2의 제곱 제수」의 경우의 잉여 계산의 고속화에 대해것이다.

ALU의 잉여 계산의 구조상, 계산 시간이 걸려 버리는 것은 어쩔 수 없지만, 반대로 말하면 잉여 계산을 하지 않고 잉여의 결과를 취득할 수 있는 방법이 있으면 그쪽으로 치환하는 것으로 고속화를 도모할 수 있다고 하는 것이다.

예를 들어, 2의 제곱이 제수인 경우에, (제수-1)을 한 수치로 AND 연산자를 사용한 계산을 하는 것으로 잉여를 구할 수 있다.

ModuleBenchmark.cs

※ 이 계측 결과로부터, 제수가 정수의 경우는 어떠한 최적화가 걸려 있다는 생각이 든다. (하지만 IL을 보았을 뿐이라면 %를 사용했을 경우는 모두 rem 명령을 호출하고 있었으므로, 자세한 것은 쫓고 있지 않다…)

출처1

https://qiita.com/shun-shun123/items/cb6689a9f210e90b9833

출처2

https://docs.google.com/document/d/e/2PACX-1vTY3j4QXhwCm7Ob_ncNRGEg1ECzytaGfp7jFZaziJpwgGLAXA0i7Vk2CPnm7GSW5zxfvdvFprWcEV3U/pub