Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일

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

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

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

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

기타 - Microsoft Build 2018 - The future of C# 동영상 내용 정리
; https://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:techsharer@outlook.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분
본문 예제 수정했습니다. ^^
정성태

... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
11632정성태7/28/20186295Graphics: 14. C# - Unity에서 캐릭터가 바라보는 방향을 기준으로 카메라의 위치 이동 및 회전하는 방법
11631정성태7/27/20184817Graphics: 13. Unity로 실습하는 Shader (9) - 투명 배경이 있는 텍스처 입히기
11630정성태2/11/20202697개발 환경 구성: 391. (GitHub 등과 직접 연동해) 소스 코드 디버깅을 쉽게 해 주는 SourceLink [2]
11629정성태7/26/20182106.NET Framework: 789. C# 컴파일 옵션 - Check for arithmetic overflow/underflow [1]
11628정성태7/26/20183521Graphics: 12. Unity로 실습하는 Shader (8) - 다중 패스(Multi-Pass Shader)
11627정성태7/25/20181743개발 환경 구성: 390. C# - 컴파일러 옵션 OSS signing / Public Signing
11626정성태7/25/20182311오류 유형: 471. .C++ 함수를 const로 바꾼 경우 C2440 컴파일 오류가 발생한다면?
11625정성태7/24/20181804Math: 49. GeoGebra 기하 (25) - 타원의 중심점 찾기파일 다운로드1
11624정성태7/24/20182125개발 환경 구성: 389. C# - 재현 가능한 빌드(reproducible builds) == Deterministic builds [4]
11623정성태7/24/20182604Math: 48. C# - 가우시안 함수의 이산형(discrete) 커널 값 생성파일 다운로드1
11622정성태7/23/20183040개발 환경 구성: 388. Windows 환경에서 Octave 패키지 설치하는 방법
11621정성태7/23/20182312VC++: 127. 멤버 함수에 대한 포인터를 외부에서 호출하는 방법파일 다운로드1
11620정성태8/3/20183608Graphics: 11. Unity로 실습하는 Shader (7) - Blur (평균값, 가우스, 중간값) 필터파일 다운로드1
11619정성태7/21/20182552Graphics: 10. Unity로 실습하는 Shader (6) - Mosaic Shading
11618정성태7/20/20182458개발 환경 구성: 387. 삼성 오디세이(Odyssey) 노트북의 운영체제를 새로 설치하는 방법
11617정성태7/20/20182276Team Foundation Server: 50. TFS 소스 코드 관리 기능 (5) - "Rollback", "Rollback Entire Changeset"
11616정성태7/17/20182213Graphics: 9. Unity Shader - 전역 변수의 초기화
11615정성태7/17/20182983.NET Framework: 788. RawInput을 이용한 키보드/마우스 입력 모니터링파일 다운로드1
11614정성태7/20/20184083Graphics: 8. Unity Shader - Texture의 UV 좌표에 대응하는 Pixel 좌표
11613정성태7/17/20182689Graphics: 7. Unity로 실습하는 Shader (5) - Flat Shading
11612정성태7/16/20182082Windows: 148. Windows - Raw Input의 Top level collection 의미
11611정성태8/3/20182938Graphics: 6. Unity로 실습하는 Shader (4) - 퐁 셰이딩(phong shading)
11610정성태8/3/20182045Graphics: 5. Unity로 실습하는 Shader (3) - 고로 셰이딩(gouraud shading) + 퐁 모델(Phong model) + Texture
11609정성태8/3/20182800Graphics: 4. Unity로 실습하는 Shader (2) - 고로 셰이딩(gouraud shading) + 퐁 모델(Phong model)
11608정성태7/17/20184709Graphics: 3. Unity로 실습하는 Shader (1) - 컬러 반전 및 상하/좌우 뒤집기
11607정성태8/30/20184660Graphics: 2. Unity로 실습하는 Shader
... 16  17  18  19  20  21  22  23  [24]  25  26  27  28  29  30  ...