Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (kevin13@chol.net)
홈페이지
첨부 파일

C# 7.2의 특징 - GC 및 메모리 복사 방지를 위한 struct 타입 개선

지난 글들을 통해 C# 7.2의 struct 관련 문법을 소개했는데요.

C# 7.2 (1) - readonly 구조체
; http://www.sysnet.pe.kr/2/0/11524

C# 7.2 (2) - 메서드의 매개 변수에 in 변경자 추가
; http://www.sysnet.pe.kr/2/0/11525

기타 - Microsoft Build 2018 - The future of C# 동영상 내용 정리
; http://www.sysnet.pe.kr/2/0/11536

위에서 소개한 "기타 - Microsoft Build 2018 - The future of C# 동영상 내용 정리" 글에서도 나왔지만 C# 7.2의 최대 특징은 GC와 메모리 복사를 줄이는 것에 있습니다. 이를 위해 크게 개선된 점이 바로 "구조체"(와 구조체로 정의된 "Span<T>")입니다.

그렇다면, 성능이 어느 정도 체감될까요?

예를 들어, 기존에 다음과 같이 작성된 Game Loop가 있다고 가정해 보겠습니다.

using System;
using System.Threading;

// .NET Core 2.1 + x64 + Release에서 테스트
namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread t = new Thread(gcCheck);
            t.IsBackground = true;
            t.Start();

            GameLoop();
        }

        private static void gcCheck()
        {
            long oldValue = 0;

            while (true)
            {
                int gcCount = GC.CollectionCount(0) + GC.CollectionCount(1) + GC.CollectionCount(2);

                long frame = _frame;
                double fps = (frame - oldValue) / 60.0;
                oldValue = frame;
                Console.WriteLine(gcCount + ": " + fps);

                Thread.Sleep(1000);
            }
        }

        static int _frame;

        private static void GameLoop()
        {
            while (true)
            {
                int x = Environment.TickCount % 30;
                int y = Environment.TickCount % 35;
                int z = Environment.TickCount % 60;

                Vector player = new Vector(x, y, z);

                x = Environment.TickCount % 30;
                y = Environment.TickCount % 35;
                z = Environment.TickCount % 60;

                Vector speed = new Vector(x, y, z);

                Vector newPos = player + speed;
                _frame++;
            }
        }
    }

    class Vector
    {
        double _x;
        double _y;
        double _z;

        public double X { get { return _x; } }
        public double Y { get { return _y; } }
        public double Z { get { return _z; } }

        public Vector(double x, double y, double z)
        {
            _x = x;
            _y = y;
            _z = z;
        }

        public static Vector operator + (Vector v1, Vector v2)
        {
            return new Vector(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
        }

        public override string ToString()
        {
            return $"({_x},{_y},{_z})";
        }
    }
}

이때 화면에 출력되는 GC 수와 fps를 보면 다음과 같습니다. (소수점 2자리 이하는 절삭했습니다.)

0: 0
501: 219024.1
1350: 370783.25
2320: 423876.9
3052: 319816.06
3959: 396515.31
4905: 413009.1
5799: 390606.53
6795: 435278.36
7713: 400970.31
8664: 415150.06
9612: 414202.71
...[생략]...

게임에서 GC 발생으로 인한 성능 저하는 가장 피하고 싶은 것 중의 하나일 텐데요, 이를 위해 Vector를 struct로 만들 수 있습니다.

struct Vector
{
    //...[생략]...
}

이렇게 바꾸고 실행하면 GC가 발생하지 않으므로 성능이 2배 가까이 올라가는 것을 볼 수 있습니다.

0: 0
0: 832359.6
0: 887992.65
0: 894983.23
0: 893596.2
0: 847630.03
0: 884984.05
0: 897776.4
0: 883163.91
0: 911639.2
0: 899867.93
...[생략]...

더 개선할 수 있을까요? struct 인스턴스를 인자로 전달 시 값 복사가 발생하는 부하를 없애기 위해 in 예약어를 operator +에 적용해 보겠습니다.

struct Vector
{
    //...[생략]...

    public static Vector operator + (in Vector v1, in Vector v2)
    {
        return new Vector(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
    }
}

그런데 실제로 해보면 딱히 그다지 나아진 성능을 볼 수 없고 오히려 떨어졌습니다.

0: 0
0: 729735.63
0: 767630.78
0: 748675.51
0: 758570.78
0: 734026.31
0: 733838.71
0: 756316.41
0: 768155.45
0: 761856.18
0: 779781.7
0: 759016.11
0: 769092.55

이유는, v1.X, v2.X 등에서의 get 접근자로 인해 방어 복사본이 생성되었기 때문입니다. 이 부하를 없애기 위해 readonly를 적용해 볼까요?

readonly struct Vector
{
    readonly double _x;
    readonly double _y;
    readonly double _z;

    //...[생략]...
}

다시 성능 테스트를 해보면,... 그래도 거의 달라진 점 없이 struct만 적용했을 때를 따라잡지 못합니다.

0: 0
0: 736243.83
0: 775353.63
0: 753936.2
0: 758081.36
0: 775756.01
0: 707858.68
0: 780843.81
0: 749853.05
0: 774323.53
0: 771705.58
0: 710503.7

혹시 모르니, struct에 포함된 필드를 (기존 3개에서) 6개로 늘려보겠습니다.

struct Vector
{
    double _x;
    double _y;
    double _z;

    double _x2;
    double _y2;
    double _z2;

    // ...[생략]...
        
    public static Vector operator + (Vector v1, Vector v2)
    {
        return new Vector(...[생략]...);
    }
}

위와 같이 하면 복사 부하가 늘어날 것으로 짐작되는데, 이럴 때 일반 struct인 경우 아래의 성능을 보이지만,

0: 0
0: 499835.38
0: 459274.05
0: 503823.2
0: 413757.51
0: 503190.33
0: 494314.35
0: 470810.4
0: 506102.25
0: 489019.13

복사 부하가 없어지는 readonly struct + in 예약어로 바꾸면,

readonly struct Vector
{
    readonly double _x;
    readonly double _y;
    readonly double _z;
    
    readonly double _x2;
    readonly double _y2;
    readonly double _z2;

    // ...[생략]...
        
    public static Vector operator + (in Vector v1, in Vector v2)
    {
        return new Vector(...[생략]...);
    }
}

이번엔 성능이 조금 나아졌습니다.

0: 0
0: 475020.35
0: 597225.11
0: 549815.65
0: 578523.76
0: 603204.06
0: 616510.96
0: 598533.23
0: 567418.03
0: 601100.78

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




현상을 보면, struct의 복사 부하가 낮을 때는 readonly struct + in 예약어 처리가 살짝 느렸습니다. 반면 복사 부하가 늘어나면서 readonly struct + in 예약어 처리가 조금 빨라졌습니다.

이런 차이가 나는 이유는, 인라인 메서드 처리에 있습니다. 작은 수의 인자일 때(예제에서는 3개) .NET Core CLR은 일반 struct의 op_Addition 호출을 인라인으로 대체한 반면, readonly struct + in 메서드의 호출은 인라인 시키지 않았습니다. 이 때문에 in 예약어로 인한 값 복사 오버헤드가 없음에도 불구하고 일반적인 struct 구문이 더 빨랐던 것입니다.

반면 필드가 6개로 늘어나면서 일반 struct의 op_Addition 호출도 인라인되지 않고 함수 호출로 바뀝니다. 이 때문에 값 복사를 하지 않는 readonly struct + in 메서드의 호출이 더 나은 성능을 보이게 되는 것입니다. 여기서 재미있는 것은, 일반 struct의 op_Addition과 readonly struct + in일 때의 op_Addition 메서드의 IL 코드는 Readonly 특성을 제외하고 완전히 같다는 것입니다.

따라서 다음과 같이 옵션을 주면 readonly struct + in의 op_Addition 메서드에 대해서도 인라인 처리가 정상적으로 이뤄집니다.

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector operator +(in Vector v1, in Vector v2)
{
    return new Vector(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

이때의 성능 차이는 일반 struct의 op_Addition과 비슷합니다. 아마도 readonly struct + in 메서드의 기본 인라인 처리는 CoreCLR 버전이 올라가면서 바뀔 수도 있을 것입니다.




그럼 정리해 볼까요? ^^

어찌 보면 readonly struct + in 처리가 성능을 높인 것이라기보다는, 최근의 함수형 프로그래밍 추세에 따라 immutable 타입 정의가 늘어나면서 발생하는 방어본 복사로 인한 성능 저하를 보완하는 형식으로 나왔다고도 볼 수 있습니다. 어쨌든, 결과적으로는 struct의 최대 단점이었던 복사 오버 헤드를 피할 수 있는 방법이 나왔고 그에 따라 스택의 부담이 줄어들면서 이전과는 다르게 보다 더 적극적으로 "값 형식"을 활용할만한 여지가 생긴 것입니다.

이러한 struct 관련 개선과 비-관리 메모리에 대한 Span 래퍼를 극단적으로 이용하게 되면 GC가 거의 발생하지 않는 닷넷 응용 프로그램을 만드는 것이 가능합니다. 이로 인해 게임 루프나 Request/Response 유형의 웹 기반 프레임워크에서는 부하를 최소화시켜 응용 프로그램의 성능을 보다 더 높일 수 있게 되었습니다. 그 좋은 사례로, .NET Core 스스로 이런 부분들을 적용해 성능 개선을 이뤄낸 것입니다.

Performance Improvements in .NET Core 2.1
; https://blogs.msdn.microsoft.com/dotnet/2018/04/18/performance-improvements-in-net-core-2-1/




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]





[최초 등록일: ]
[최종 수정일: 6/14/2018 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:kevin13@chol.com

비밀번호

댓글 쓴 사람
 



2018-06-13 05시33분
[spowner] 본문 소스코드 중, _newPos 관련 오류 발생합니다~
[손님]
2018-06-13 08시17분
struct_perf_compare.zip 파일을 다운로드해서 빌드했는데 오류가 발생한다는 건가요? (어쨌든, 방금도 해봤는데 잘 빌드가 됩니다.)
정성태
2018-06-14 01시28분
[spowner] 아 다운로드 파일이 있는지 확인을 못했습니다. 본문의 소스코드를 복사해 실행했더니 생겼습니다

[손님]
2018-06-14 08시54분
본문 예제 수정했습니다. ^^
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
11658정성태8/18/20185사물인터넷: 31. 커패시터와 RC 회로파일 다운로드3
11657정성태8/18/201810사물인터넷: 30. 릴레이(Relay) 제어파일 다운로드3
11656정성태8/18/201831사물인터넷: 29. 트랜지스터와 병렬로 연결한 LED파일 다운로드1
11655정성태8/18/201826사물인터넷: 28. 저항과 병렬로 연결한 LED파일 다운로드1
11654정성태8/18/201844사물인터넷: 27. 병렬 회로의 저항, 전압 및 전류파일 다운로드1
11653정성태8/18/201828사물인터넷: 26. 입력 전압에 따른 LED의 전압/저항 변화파일 다운로드1
11652정성태8/18/201834사물인터넷: 25. 컬렉터 9V, 베이스에 5V와 3.3V 전압으로 테스트하는 C1815 트랜지스터파일 다운로드1
11651정성태8/18/201846사물인터넷: 24. 9V 전압에서 테스트하는 C1815 트랜지스터파일 다운로드2
11650정성태8/18/201842사물인터넷: 23. 가변저항으로 분압파일 다운로드1
11649정성태8/14/201869사물인터넷: 22. 저항에 따른 전류 테스트파일 다운로드1
11648정성태8/14/201878사물인터넷: 21. 퓨즈를 이용한 회로 보호파일 다운로드3
11647정성태8/8/2018145오류 유형: 476. 음수의 음수는 여전히 음수가 되는 수(절대값이 음수인 수)
11646정성태8/8/201877오류 유형: 475. gacutil.exe 실행 시 "Failure initializing gacutil" 오류 발생
11645정성태8/8/201887오류 유형: 474. 닷넷 COM+ - Failed to load the runtime.
11644정성태8/8/2018171디버깅 기술: 119. windbg - 닷넷 개발자를 위한 MEX Debugging Extension 소개
11643정성태8/12/2018172사물인터넷: 20. 아두이노 레오나르도 R3 호환 보드의 3.3v 핀의 LED 전압/전류 테스트 [1]파일 다운로드1
11642정성태8/3/201899Graphics: 19. Unity - LightMode의 ForwardBase에 따른 _WorldSpaceLightPos0 값 변화
11641정성태8/3/2018135Graphics: 18. Unity로 실습하는 Shader (10) - 빌보드 구현파일 다운로드1
11640정성태8/3/2018117Graphics: 17. Unity - World matrix(unity_ObjectToWorld)로부터 Position, Rotation, Scale 값을 복원하는 방법파일 다운로드1
11639정성태8/2/2018135디버깅 기술: 118. windbg - 덤프 파일로부터 추출한 DLL을 참조하는 방법
11638정성태8/2/2018202오류 유형: 473. windbg - 덤프 파일로부터 추출한 DLL 참조 시 "Resolved file has a bad image, no metadata, or is otherwise inaccessible." 빌드 오류
11637정성태8/1/2018113Graphics: 16. Unity - World matrix(unity_ObjectToWorld)로부터 TRS(이동/회전/크기) 행렬로 복원하는 방법파일 다운로드1
11636정성태8/1/2018114Graphics: 15. 3D 공간에서 두 점이 이루는 각도 구하기파일 다운로드1
11635정성태8/1/2018101오류 유형: 472. C# 컴파일 오류 - Your project is not referencing the ".NETFramework,Version=v3.5" framework.
11634정성태8/1/2018140.NET Framework: 790. .NET Thread 상태가 Cooperative일 때 GC hang 현상 재현 방법파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...