이거는 수진위키 훔쳐봐야지

 

C# 이란

옛날옛날에 어셈블리로 코딩을 하던 때가 있었어요

그런데 쓰기 힘들어서 C언어가 생겼어요.

C언어가 나오면서 자연어와 비슷하게 코딩이 가능해졌어요.

그러나 절차지향의 한계점으로 객체지향 부분이 업그레이드 되어 C++이 생겼어요.

와 C++나오니 일반화도 되고 객체지향도 되고 너무 좋았어요.

그러나 C++의 큰 단점이 있었어요.

나는 여러가지 플랫폼을 대응하고 싶은데 (윈도우, 맥, 리눅스도..)

이럴 때 마다 매 번 빌드가 필요하고 소스코드 수정도 필요했어요.

윈도우즈가 제공하는 함수를 리눅스에서는 사용 못하고 불편했어요.

코드 하나로 다양한 플랫폼을 대응하고 싶었어요.

그래서 JAVA라는 언어가 생겼어요 (one source multy use)

소스코드 하나로 다양한 플랫폼을 대응할 수 있게 되었어요.

물론 그만큼 희생되는 부분이 있었기에 성능은 아쉬웠어요.

아무튼 소스코드 하나로 멀티 플랫폼은 메리트가 있어요.

어느날 마이크로소프트가 JAVA를 보고 새로운 언어를 만들자 생각했어요.

그래서 C#이라는 완전 객체 지향 프로그래밍 언어라는 것이 탄생했어요.

완전 객체 지향 프로그래밍은 Class가 무조건 필요하다는 특징이 있어요.

(그러나 최신 C#은 또 아니래요)

애니웨이

C#은 메모리를 자동으로 정리해주는 가비지 컬렉터 등등

C++에서는 볼 수 없었던 기능들이 생겼어요.

이런 C#을 이해하기 위해서는 .NET 아키텍처를 알아야돼요.

프로그램은 하나의 소스코드로 멀티 플랫폼이 사실 불가능해요

그래서 중간단계인 미들웨어 프로그램을 만들었어요.

실제로는 운영체제 위에서 미들웨어 프로그램이 작동하고 이것이 소스코드를 작동하는 것이에요.

이랬던 것이

이렇게 바뀌었어요.

JAVA는 JVM (자바 버츄얼 머신), C#은 .NET은 이라는 것이 있어요.

 

그러면 빌드를 하면 어떤 일이 발생하나요?

C#은 컴파일하면 CLI 사양을 준수하는 중간언어(IL)로 컴파일이 되어요.

그리고 IL코드와 프로그램에 사용되는 리소스가 함께 패키징 되는 어셈블리가 되어요

여기서 어셈블리는 우리가 아는게 아니라 의회 같은것이에요.

 

이렇게 이루어졌구나 라고만 알아두세요.

 

어셈블리는 서로 함께 사용되어 논리적 기능 단위를 형성하도록 빌드되는 타입 및 리소스의 컬랙션을 의미해요

즉 프로그래밍이라는 거죠. 어떤 한 기능을 제공한다는 이야기에요.

확장자는 exe 또는 dll 이에요.

여담이지만 확장자는 큰 의미는 없어요. 여러분만의 파일 확장자를 만들 수 도 있으니까요. 많이 쓰니까 공통적으로 쓰는게 생긴거에요.

애니웨이

dll에 대한 정적라이브러리, 동적라이브러리 이건 나중에 면접준비할 때 알아두세요.

 

IL이라는 것으로 컴파일된다.

빌드되어 나오는 것은 어셈블리 라는 것이에요.

 

어셈블리가 CLR에 로드 되는데.. CLR은 운영체제에 따라서 JIT 혹은 AOT 로 변환해요.

AOT : 실행 전에 컴파일 되는 거에요.

JIT : 프로그램 실행 중에 컴파일 되는 거에요.

 

 

공용 타입 시스템 (CTS)

 

다음으로 공용 타임 시스템이라는 것을 알아야해요.

.NET에는 C# F# 다양하게 돌아가요.

규격만 준수하면 여러분도 만들 수 있어요.

 

.NET의 모든 형식은 값 타입 아니면 참조 타입으로 구분되요.

모든 타입은 기본 타입이 시스템 이름 공간에 있는 오브젝트를 상속받게 되어요.

이것을 그림으로 보면 

값 타입

C#에서 구조체 / 열거형 / 그 외에 기본 제공 타입으로 구성되어요.

값 타입은 아래와 같은 특징이 있어요.

- 구조체를 제외한 모든 타입은 ValueType에서 파생되어요.

- 값 타입은 무조건 스택에만 존재해요. 그래서 복사가 이루어 진대요.

- 상속이 불가능해요.

C++에서는 Struct라는 키워드가 키워드 차이일 뿐 Class같이 사용했는데 C#은 구조체는 구조체에요.

- 구조체 멤버 중에 참조 타입이 있다면 메모리 주소가 복사되어요. (얕은 복사가 이루어진대요)

 

참조 타입

클래스 / 대리자 / 배열 / 인터페이스 가 있어요.

참조 타입 변수를 쓰면 인스턴스의 주소를 참조하게 되어요.

C++의 레퍼런스를 생각하면 되어요.

참조 타입에는 Null을 할당할 수 있어요.

 

 

박싱과 언박싱

박싱은 값 타입을 오브젝트 타입 또는 값 타입에서 구현된 임의의 인터페이스 타입으로 변환하는 프로세스,

언박싱은 박싱된 인스턴스에서 값 타입을 추출하는 프로세스래요.

 

예를 들어

int i = 10; // Value Type
object o = i // Reference Type => Boxing이 되었어요. (암시적으로 변환되어요)

int a = (int)o // Value Type => Unboxing이 되었어요. , 10 (명시적으로 변환되어요)


interface IFlyable {void Fly()}

struct Foo : IFlyable
{
	public override void Fly() {} // 가능해요
}

Foo f = new Foo(); // Value Type
// 여기서 new는 새로운 객체를 만든거에요

IFlyable f2 = f; // Referecne Type => Boxing이 되었어요.

우리가 그래서 왜 알아야 돼냐~

 

 

노우진!!!!

 

 

박싱 언박싱은 스택에서 힙으로 힙에서 스택으로 복사가 이루어져요

 

시스템,컬랙션

컬랙션에 있는 애들은 오브젝트 타입이에요

모든 타입이 오브젝트에서 파생 되었기 때문에 다 업캐스팅 될 수 있겠죠?

그래서 오브젝트 타입이 될 수 있어요.

 

시스템,컬랙션재내릭

으어어어 머지

약 30배 정도 차이가 났어요.

그래서 많이 힘들어요.

그러니 무조건 피해야돼요.

 

이런 것들은 외워야 힘들어하지 않을 수 있어요.

 

 

 

가비지 컬렉션

C++에서는 메모리 관리를 수동적으로 다뤘어요.

그런데 문제는 수동으로 관리하는데 실수가 발생했어요.

 

첫번째 메모리 누수(릭), 다 썼는데 해제하지 않았어요.

두번째 이중 해제로 댕글링 포인터라는것이 있었어요

세번째 섣부른 해제로 아직 사용이 끝나지 않았는데 해제했어요.

 

이런 문제가 자주 발생해서 자동으로 관리해주는

가비지 컬랙션이 탄생하게 되었어요.

 

가비지 컬랙션은 더 이상 사용하지 않는 메모리를 재사용하는 것이에요.

 

문제는 더 이상 사용하지 않는 메모리인지 판단하는 것이 불가능했어요.

이건 앞으로도 불가능하지 않을까 싶대요.

 

그래서 가정 한대요.

첫 번째가 추적 방식

여기서는 도달 가능성이라는 방식을 쓴대요.

그래서 도달할 수 있다면 사용중이라 판단하고, 아니면 가비지라고 가정한대요.

그래서 여기서는 루트(Root)라는 것이 필요하대요.

여기는 스택... 이건 추후에 볼게요.

root에서 타고타고 내려가서 접근할 수 있는지 추적하는 것이에요.

여기서 검은 색은 도달할 수 있는 것, 흰 색은 가비지로 가정되는 것이에요.

 

두 번째는 참조 카운팅이에요.

내부적으로 참조 하고 있는 횟수를 가지고 있어요.

이 것이 0이면 더 이상 사용하고 있지 않다고 판단해서 가비지로 가정하는 것이에요.

순환 참조를 주의해야 하는데요.

사용하지 않는 메모리들이 서로가 서로를 가리키고 있어서 참조 횟수가 0이 되지 않아 가비지가 되지 않는 것이에요.

그래서 가정이라는 말을 쓰는 것이에요.

이를 방지하기 위해 약한 참조라는 개념을 사용해요. 

 

C#은 세대별 가비지 컬랙션을 사용해요.

엄밀히는 CLR이 지원하는 것이에요.

 

세대

세대라는 것에 대한 이해가 필요합니다.

가비지 컬랙터가 관리하는 메모리를 매니지드 힙이라고 이름을 정했어요.

그냥 그렇게 부르는 거래요.

이 힙을 0,1,2 의 3개 세대로 구분하고 낮은 숫자가 신세대래요.

 

1. 가비지 컬랙터가 검사하는데 이걸 나눠서 검사해서 효율을 높이는 것이래요.

 

2. 연구해보니까 최근에 만들어진 객체일 수록 임시 데이터일 가능성이 높았대요.

   그래서 최근에 만들어진 객체부터 검사하는 것이에요.

 

3. 그리고 메모리 할당은 0세대에서만 이루어져요.

  최근에 만들어진 객체끼리 서로 연관되어 있는 경향이 있어서 캐싱 측면에서 좋아요

 

 

메모리 할당

힙에 메모리가 올라가는 것은 참조 타입이에요

C#에서 참조타입은 0세대에 할당되어요.

각 객체들은 메모리가 허락하는 한 인접한 곳에 위치해요.

매니지드 힙은 메모리를 미리 시스템으로부터 할당 받아 놓기 때문에 빨라요.

(메모리 풀링이 효율적인 이유랑 비슷해요)

 

C++에서 동적 할당은

운영 체제한테 나 이정도 필요해요 라고 요청하면

운영 체제가 MMU한테 요청하고 할당을 받는 방식인데

딱 들어봐도 오래 걸릴 것 같아요.

 

C#에서 동적 할당은

이미 MMU를 거쳐서 메모리 할당 받는 것을 가비지 컬랙터가 받아 두었어요.

그래서 빨라요

 

자 근데

예외 사항이 있는데 85kb 이상의 큰 객체는 따로 관리한대요.

85kb 이상은 LOH에 할당된대요. 압축도 안 된대요.

 

메모리 해제

세대별 가비지 컬랙션은 추적방식을 사용해요.

추적방식의 루트에는 스택, CPU 레지스터, 정적 필드 등이 있어요.

 

수집 시기는 가장 적합할 때 알아서 한대요.

적합할 때 조건은 있으나 알기는 어려워요. 애니웨이

 

루트를 이용해서 도달할 수 있거나 없거나 판단해서 가비지 공간을 덮어버려요. 옮겨버린대요.

뭔소리냐, 

요렇게 쓰고 있었어요

근데 얘 이제 안쓴대요. 그럼 가비지죠

그래서 옆에를 복사해서 압축하는거래요.

그럼 주소값이 바뀌겠죠.

그럼 주소값도 바꿔주고 매니지드 힙에서 본 포인터도 바꿔준대요? (봤나?)

 

주의할 점은

라지 오브젝트 힙에 있는 애들은 압축하지 않아요. 압축하기에는 커서 힘들대요.

 

순서도 있대요.

0세대부터 가장 먼저 일어난대요.

가비지 컬랙션이 일어날 때 가비지가 아닌 메모리들은 윗 세대로 승격시켜요.

 

0세대에서 할당이 이루어질 때 공간이 부족하면

1세대에 가비지 컬랙션을 돌려요.

그렇게 1세대 2세대 다 돌았는데도 부족하면 2세대 1세대 0세대 다시 돌아요.

그래도 부족하면 추가적으로 할당을 받아요.

 

주의 사항

가비지 컬랙션은 결코 자원을 적게 소모하는 연산이 아니에요.

멀티스레드 환경인 경우 가비지 컬랙션이 수행되는 동안 다른 스레드가 중단되어요

그저 멈추지 않길 바라며....

 

주의 할 점이 있어요.

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

 

그리고 약한참조 이야기가 나오는데 이건 넘어갈게요.

어려운 개념이기도 하고, 유니티에서 약한참조를 사용하지 말라고 한대요.

 

빈번한 할당을 조심해야돼요.

가비지 컬랙션이 일어나는 조건 중 하나는 공간이 부족할 때래요.

그래서 의미없는 할당이 이루어지면 가비지 컬랙션이 자주 일어나겠죠?

 

너무 큰 객체의 할당을 조심하래요.

85kb 이상의 객체는 LOH에 할당되며 압축이 일어나지 않는대요.

 

관리되지 않는 리소스도 있대요.

가비지 컬랙션은 메모리만 관리해요.

운영체제에서 빌려오고 돌려주는것에는 메모리가 아니라 다른 것도 있대요.

 

 

 

그러면 왜 우리가 여태까지 이런 이야기를 했는가

 

유니티 .NET은 표준 .NET이 아니래요.

.NET이 오픈되면서 새로 만들었나봐요.

 

유니티에서 사용하는 .NET은 Mono와 IL2CPP이래요.

그래서 차이가 발생한대요.

 

유니티에서 사용하는 .NET은

여길 보시면 된대요

애니웨이

 

유니티는 가비지 컬랙션 부분을 잘 봐야 한대요.

유니티에서 사용하는 가비지 컬렉션은 세대별 관리를 하지 않는대요.

세대를 나누지 않고 메모리에 압축 또한 이루어지지 않는대요.

그래서 메모리 단편화가 일어나서 힙이 쉽게 확장되어 최적화 이슈가 발생할 수 있대요.

특히 모바일에서 조심하세요.

이런 부분을 명확하게 알고 쓰지 않으면, 게임이 조금 돌아가다가 메모리가 부족해서 꺼진대요.

 

그리고 여기에서 증분 가비지 컬랙션이라는 방식을 사용한대요.

가비지 컬랙션이 다 끝 날 때까지 오래걸리면 프레임 드랍이 발생할 수 있대요.

그래서 제한 시간을 두고 조금씩 조금씩 한대요.

 

 

 

다시한번 정리하자면

 

.NET

- CRL과 클래스 라이브러리 세트를 의미함

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

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

     > 모든 타입은 값 타입과 참조 타입으로 분류된다.

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

     > 이를 명확히 인지하고, Boxing과 Unboxing을 피하도록 코드를 작성해야 함

 

가비지 컬렉션

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

     > 메모리 누수 / 이중 해제 / 섣부른 해제

- 메모리를 자동으로 관리하는 기술

- 가비지로 가정하는 방법

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

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

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

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

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

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

 

.NET : Unity

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

- 세대별로 관리하지 않으며, 메모리 압축 또한 없기 때문에 메모리 단편화가 쉽게 일어나고, 힙도 쉽게 확장된다. 그래서 최적화 이슈가 발생할 수 있다. (특히 모바일 환경에서 정말정말 중요합니다.)

728x90

+ Recent posts