경일게임아카데미 멀티 디바이스 메타버스 플랫폼 개발자 양성과정 특강 20220812 2022/04/04~2022/12/14

Jinho Lee·2022년 8월 12일
0

2022.08.12 경일 메타버스 19주차 5일 특강 수업내용. CLR C#과 Unity C#의 차이

정리

.NET

  • CLR과 클래스 라이브러리 세트를 의미함

  • CLR은 C# 코드를 컴파일한 결과물인 IL 코드를 다시 해당 플랫폼에 맞는 코드로 변환하여 하나의 소스코드로 여러 플랫폼을 지원함

  • .NET에는 공용 타입 시스템이 있다.

    • 모든 타입은 값 타입과 참조 타입으로 분류

    • 모든 타입은 System.Object 타입을 상속 받음

    • 이를 명확히 인지하고, 박싱과 언박싱을 피하도록 코드를 작성해야 함.

가비지 컬렉션

  • 메모리를 수동으로 관리하는 것은 여러 문제점이 있음

    • 메모리 누수 / 이중 해제 / 섣부른 해제
  • 메모리를 자동으로 관리하는 기술

  • 가비지로 가정하는 방법

    • 추적 : 도달 가능성으로 가비지 판단

    • 참조 카운팅 : 참조 횟수로 가비지 판단

  • 표준 .NET은 세대별 가비지 컬렉션을 사용

    • 메모리 할당은 0세대에서만 일어남

    • 메모리 해제 시, 가비지가 아닌 메모리는 윗 세대로 승격됨.

  • 가비지 컬렉션이 수행되는 동안에는 프로그램이 멈추기 때문에 이에 유의하여 코드를 작성해야 함.

.NET : Unity

  • Unity의 스크립트 백엔드(스크립트 환경)는 Mono, IL2CPP 두 개가 있음.
    => 여기서 모든 차이가 발생

  • 세대별로 관리하지 않으며, 메모리 압축 또한 없기 때문에 메모리 단편화가 쉽게 일어나고, 힙도 쉽게 확장된다. 그래서 최적화 이슈가 발생할 수 있다.

들어가기

  1. 어셈블리
    기계어에 일대일 대응
    쓰기 불편함

  2. C
    쓰기 좋음
    객체 지향적이지 못함 - 클래스 없음

  3. C++
    C with Class
    클래스를 넣어 객체 지향적
    다른 플랫폼을 지원할 때마다 코드 수정 + 새로 빌드

  4. Java
    원 소스 멀티 플랫폼
    컴파일러 다음, 플랫폼마다 코드를 다르게 빌드해주는 중간 프로그램 존재 - 자바 버추얼 머신 (JVM; Java Virtual Machine)
    성능이 좋지 못함

  5. C#
    완전 객체 지향적
    Java와 비슷 (Java 바탕)
    다양한 기능 - 강건성, 유지보수를 위한 기능

C#

  • 마이크로소프트에서 개발한 완전 객체 지향 프로그래밍

  • 강건성과 유지보수를 위한 다양한 기능 제공

    • 가비지 컬렉션

    • 함수형 프로그래밍을 위한 람다식

    • 비동기 프로그래밍

.NET 아키텍처

  • 어떤 플랫폼이든 언어를 동작시킬 수 있도록 하는 프로그램, 사양

    • 공용 언어 인프라(CLI; Common Language Infrastructure)
  • 이 사양(CLI)에 맞춰 마이크로소프트가 구현한 중간 프로그램

    • 공용 언어 런타임(CLR; Common Language Runtime)
  • .NET은 위의 중간 프로그램과 클래스 라이브러리 세트를 일컫는다.

  • .NET 위에서 동작하는 언어

    • C#

    • F#

    • Visual Basic

빌드

  • C#은 컴파일을 하면 CLI 사양을 준수하는 중간 언어(IL; Intermediate Language)로 컴파일 된다.

  • 그리고 이러한 IL 코드와 프로그램에 사용되는 리소스(아이콘, 마우스 커서, 메뉴 등)가 함께 패키징 되어 어셈블리(Assembly)가 된다.
    ⇒ 여기서는 C / C++를 컴파일하면 생성되는 어셈블리 언어와 다른 개념이다.

    • 어셈블리(Assembly) :
      서로 함께 사용되어 논리적 기능 단위를 형성하도록 빌드되는 타입 및 리소스의 컬렉션
  • 어셈블리는 실행 파일(.exe) 또는 동적 연결 라이브러리(.dll)의 형태를 가지며, .NET 기반 애플리케이션에 대한 배포, 버전 제어, 재사용, 활성화 범위 및 보안 권한의 기본 단위를 형성한다.

  • C# 프로그램을 실행하면 어셈블리가 CLR에 로드 되는데, CLR은 IL 코드를 플랫폼에 따라 JIT(Just-In-Time) 컴파일 혹은 AOT(Ahead-Of-Time) 컴파일을 수행하여 네이티브 명령어로 변환한다.

    • JIT(Just-In-Time) 컴파일 :
      프로그램 실행 중에 그때그때 컴파일을 하는 것

    • AOT(Ahead-Of-Time) 컴파일 :
      프로그램 실행 전 미리 컴파일을 진행하는 것

공용 타입 시스템 (CTS; Common Type System)

  • .NET은 여러 .NET 언어를 지원하기 위해
    공용 타입 시스템(CTS; Common Type System)을 지원

  • .NET의 모든 형식은
    값 타입(Value Type) 혹은 참조 타입(Reference Type)으로 구분

  • 모든 타입은 기본 타입 System.Object에서 파생

값 타입

  • C#에서 값 타입은 구조체 / 열거형 / 그 외 기본 제공 타입으로 구성된다.

  • 값 타입은 아래와 같은 특징을 갖는다.

    • 구조체를 제외한 모든 타입은 System.ValueType에서 파생된다.

    • 스택 메모리에 직접 값이 포함된다. 다시 말해 복사가 일어난다.

    • 상속이 불가능하다.

    • 구조체 멤버 중에 참조 타입이 있다면 메모리 주소가 복사된다.

      • 얕은 복사(Shallow Copy)라고 한다.

참조 타입

  • C#에서 참조 타입은 클래스 / 대리자 / 배열 / 인터페이스가 있다.

  • 참조 타입은 아래와 같은 특징이 있다.

    • 힙 메모리인스턴스가 할당된다.

    • 참조 타입의 변수는 인스턴스의 주소에 대한 참조를 가진다.

    • Null을 할당할 수 있다.

박싱과 언박싱

  • 박싱(Boxing) :
    값 타입
    object 타입 또는 값 타입에서 구현된 (구조체의) 임의의 인터페이스 타입으로 변환하는 프로세스

    • object 타입의 새로운 인스턴스를 생성해 값을 힙으로 복사한다.

    • 암시적으로 이루어진다.

  • 언박싱(Unboxing) :
    박싱된 인스턴스에서 값 타입을 추출하는 프로세스

    • 힙에서 스택으로 값이 복사된다.

    • 명시적으로 이루어진다.

  • 주의 :
    박싱과 언박싱많은 계산 과정을 필요로 한다.

    2022. 08. 12 박싱&언박싱 예시 코드

    • 예시 코드의 결과는 다음과 같다.

    • 박싱은 약 30배, 언박싱은 약 1.1배의 차이가 있다.
      언박싱의 경우 int가 아닌 구조체라면 더 많은 시간이 필요할 것이다.

    • 따라서 구현에서 박싱과 언박싱은 일어나지 않도록 주의하자.

      • 제네릭 컬렉션(일반화 컬렉션)만 사용하는 이유 :
        박싱, 언박싱을 피하여 병목 현상을 피할 수 있다.

참고자료

가비지 컬렉션

  • C++에서는 메모리를 프로그래머가 관리한다.
    즉, 동적 할당된 메모리를 전부 사용했다면 시스템에 돌려줘야 한다.

    • C++11부터 스마트 포인터 라이브러리가 추가되어 모던 C++에서는 명시적 해제보다는 스마트 포인터 사용을 권장하고 있다.
  • 이 과정에서 여러가지 실수가 많이 발생하는 데 아래와 같다.

    • 메모리 누수(Memory Leak)

      • 메모리 사용이 끝났음에도 불구하고 해제하지 않은 것
    • 이중 해제(Double Free)

      • 이미 해제가 된 메모리임에도 불구하고 또 해제하는 것

        • 이미 해제가 된 메모리를 가리키는 포인터를 댕글링 포인터(Dangling Pointer)라 한다.
    • 섣부른 해제(Premature Free)

      • 아직 사용이 끝나지 않았음에도 불구하고 해제하는 것
  • 이런 불편함을 해결하기 위해 나온 기술이
    자동 메모리 관리(Automatic Memory Management) 기술인
    가비지 컬렉션(Garbage Collection)이다.

동작 원리

  • 가비지 컬렉션은 가비지 컬렉터(Gabage Collector)
    더이상 사용하지 않는 메모리(가비지(Gabage))를 재사용
    함으로써 동작된다.

  • 하지만 애석하게도 어떤 객체가 아직 사용되고 있고, 사용되지 않는지(which object is still lived) 정확하게 판별할 수 있는 알고리즘이 없다.

    • 정지 문제(Halting Problem)와 관련이 있다.
  • 그래서 다음과 같은 2가지 방법으로 객체의 사용 유무(생존, Liveness)를 가정한다.

  1. 추적 가비지 컬렉션(Tracing Garbage Collection)

    • 도달 가능성(Reachability)으로 생존을 가정한다.

    • 루트(Root)를 사용하여 해당 메모리까지 도달할 수 있는지 보고, 도달되지 못한 메모리는 가비지로 가정한다.

  1. 참조 카운팅(Reference Couting)

    • 해당 메모리에 참조하는 것이 없을 때 가비지로 가정한다.

      • 바로 뒤에 설명할 순환 참조 때문에, 확정을 못하고 가정하는 것이다.
    • 참조 카운팅 방법은 순환 참조(Circular Reference)를 주의해야 한다.

      • 순환 참조 (Circular Reference) :
        서로 다른 두 메모리가 서로를 참조하는 것
    • 순환 참조를 방지하기 위해 약한 참조(Week Reference)라는 개념을 사용한다.

      • 약한 참조(Week Reference) :
        가비지 판단 기준인 참조 횟수에는 영향을 주지 않고 참조를 하는 것
    • 앞서 말한 스마트 포인터 라이브러리가 이 방식으로 동작한다.

  • 추적 가비지 컬렉션과 참조 카운팅 두 방법은 하이브리드 형식으로 같이 사용될 수 있다.

  • 가비지 컬렉션에도 여러 가지 종류가 있다.

    • 보수적 가비지 컬렉션(Conservative Garbage Collection)

    • 복제 가비지 컬렉션(Copying Garbage Collection)

    • 분산 가비지 컬렉션(Disributed Garbage Collection)

    • 증분 가비지 컬렉션(Incremental Garbage Collection)

    • 등등

  • C#은 가비지 컬렉션을 지원한다.

    • 엄밀히는 CLR이 지원한다.

    • 가비지 컬렉션을 지원하는 언어를
      매니지드 언어(Managed Language)라고 한다.

      • 반대를 언매니지드 언어(Unmanaged Language)라고 한다.

      • 언매니지드 언어는 프로그래머가 거의 모든 작업을 수행하고, 실제 프로그램이 운영체제에 의해 로드되므로
        네이티브 언어(Native Language)라고도 한다.

  • C#에서 사용하는 방식은
    세대별 가비지 수집(Generational Garbage Collection)이다.

세대

  • 세대별 가비지 수집을 이해하기 위해선 먼저 세대(Generation)에 대한 이해가 필요하다.

  • 매니지드 힙(Managed Heap) :
    가비지 컬렉터가 관리하는 메모리

    • 이 힙을 0세대, 1세대 및 2세대의 총 3개 세대로 나눠 관리한다.
  • 세대를 나눈 이유는 다음과 같은 이유로
    메모리를 재사용하기 용이하기 때문이다.

    1. 가비지 컬렉션이 일어날 때 파편화 방지를 위해 메모리를 압축하는 데, 힙 전체를 대상으로 하기보다 일부분에서만 수행하는 게 더 빠르다.

    2. 최근에 만들어진 객체일 수록 수명이 짧고 오래 사용된 객체일 수록 수명이 길어 재사용할 메모리를 빠르게 분류할 수 있다.

    3. 메모리 할당은 0세대에서만 주로 일어나는데 최근에 만들어진 객체끼리 서로 연관되는 경향이 있어 캐싱 측면에서 좋다.

  • 매니지드 힙에는 여러 개의 포인터가 있으며 이를 이용해 세대를 구별한다.

메모리 할당

  • C#에서 모든 참조 타입의 객체매니지드 힙의 0세대에 할당된다.

  • 각 객체들은 주소 공간이 허락하는 한 인접한 곳에 위치하게 된다.

  • 매니지드 힙은 메모리를 미리 시스템으로부터 할당 받아 놓기 때문에(메모리 풀링 (Memory Pooling)) 스택에서 메모리를 할당하는 속도만큼 빠르게 할당할 수 있고, 접근도 빠르게 할 수 있다.

    • 즉, 동적 할당도 정적 할당만큼 빠르다.
  • 단, 85KB 이상의 크기를 가지는 큰 객체는 LOH(Large Object Heap)라는 2세대 메모리에 할당

메모리 해제

  • 세대별 가비지 컬렉션추적 방식을 사용한다.

  • 루트에는 스택 루트(스택 메모리), CPU 레지스터, 정적 필드 등이 있다.

    • 이외에도 GC 핸들이나 Finalize 큐가 있으나, 일반적으로 고려할 부분은 아니다.
  • 수집 시기는 가비지 컬렉터가 자동으로 가장 적합한 때에 컬렉션을 수행한다.

    • 상세한 조건을 알고 싶다면 여기를 확인
  • 매니지드 힙을 검사해 가비지를 찾으면 접근할 수 있는 객체를 압축해 가비지의 공간을 덮어버린다.

    • 압축 :
      단편화된 공간이 없게끔, 다른 데이터를 복사해 덮어넣는다.

    • LOH는 압축되지 않는다.

    • 이 과정에서 참조 변수의 주소값과 각 세대의 시작을 가리키는 포인터 모두 수정한다.

  • 가비지 컬렉션이 일어나는 순서는 다음과 같이 정해져 있다.

    1. 가장 먼저 가비지 컬렉션이 일어나는 세대는 0세대

      • 이 과정에서 가비지가 아닌 메모리는
        윗 세대(숫자가 더 큰 세대)로 승격(Promotion)시킨다.
    2. 만약 0세대에서 가비지 컬렉션을 수행했음에도 불구하고
      새로운 객체를 만들기 위한 메모리 공간이 충분하지 않다면,
      먼저 1, 2세대 순으로 수집을 수행한다.

    3. 그럼에도 또 부족하다면 세대 2, 1, 0의 순서로 수집을 수행한다. 이 경우에도 2세대를 제외하곤 세대 승격은 일어난다.

주의 사항

  • 가비지 컬렉션에 대해서 명확히 이해해야 하는 이유는
    성능과 직결되기 때문이다.

  • 가비지 컬렉션은 자원을 많이 소모하는 연산이다.

  • 멀티스레드 환경인 경우 가비지 컬렉션이 수행되는 동안 다른 스레드가 중단(Suspended)된다.

  • 따라서 아래의 사항을 주의하자.

    1. 참조 카운팅 방식으로 가비지 수집이 일어나지 않는다.

      • 세대별 가비지 컬렉션은 추적 방식이다.
    2. 필요하다면 약한 참조를 사용할 수 있다.

      • 어떤 객체에 도달 가능할 때, 이를 객체에 대한 강한 참조(Strong Reference)를 갖는다고 표현한다.

      • 하지만 객체의 도달 가능성에 영향을 주지 않으면서(객체의 생존에 영향을 주지 않으면서) 해당 객체를 참조하고 싶은 때, 약한 참조(Weak Reference)를 사용할 수 있다.

    3. 빈번한 할당을 조심하자.

      • 가비지 컬렉션이 일어나는 조건 중 하나는 객체를 할당할 충분한 공간이 없을 때다.

        • 즉, 0세대가 가득 찼을 때를 의미한다.
      • 객체를 빈번하게 생성하면 0세대에 여유 공간이 부족해 가비지 컬렉션이 일어날 수 있다.

    4. 너무 큰 객체 할당은 피하도록 하자.

      • 85KB 이상의 크기를 가지는 객체는 LOH에 할당된다.

      • LOH에서는 메모리 압축이 일어나지 않아 내부 단편화가 발생할 수 있다.

    5. 복잡한 참조 관계를 피하자.

      • 가비지 컬렉션 후에 메모리 주소 관리를 어렵게 한다.

      • 특히 오래된 세대에서 새로운 세대에 대한 메모리를 참조하게 될 때 수집을 방지하기 위해 쓰기 장벽(Write Barrier)을 만든다.

        • 이는 많은 성능을 필요로 한다.
    6. 관리되지 않는 리소스도 있다.

      • 운영체제 리소스를 래핑하는 경우 제대로 정리가 되지 않는다.

        • 파일 핸들, 윈도우 핸들, 네트워크 연결 등
      • 이런 경우에는 IDisposableusing 문을 이용할 수 있다. 자세한 것은 여기를 참고하자.

참고자료

Unity C#

  • C# 프로그램은 결국 .NET 위에서 동작한다.

  • Unity C#의 다른 점은 바로 이 .NET에 있다.

스크립팅 백엔드(스크립트 환경)

  • Unity에서 사용하는 .NET은 2가지가 있다.

    • 하나는 Mono로 다른 하나는 IL2CPP이다.

      • Mono :
        크로스 플랫폼을 지원하는 오픈 소스 .NET이다.

      • IL2CPP :
        Unity가 만든 .NET이다.

    • 기본적으로 Unity는 Mono를 사용하지만
      일부 플랫폼으로 빌드할 때 IL2CPP를 사용할 수 있다.

    • 두 플랫폼의 차이는 여기를 참고

가비지 컬렉션

  • .NET 환경이 다르기에 가비지 컬렉션의 방식도 달라진다.

  • Unity(Mono)에서는 Boehm GC를 사용한다.

    • 세대별로 관리되지 않는다.

    • 메모리의 압축 또한 일어나지 않는다.

  • 메모리 압축이 일어나지 않아 메모리 단편화가 일어나 힙이 쉽게 확장되어, 최적화 이슈가 발생할 수 있다.

    • 이로 인한 성능 저하를 막고 싶다면 여기를 확인하라.
  • 가비지 컬렉션에 너무 많은 성능이 소모되는 것을 막기 위해
    수행 제한 시간을 두어 조금씩 수행하는 증분 방식 또한 사용한다.

참고자료

0개의 댓글