스택은 단순히 데이터를 넣고 뺄 수 있는 구조이며, System.ValueType을 상속하는 기본 데이터형(int, long, bool 등)만 저장할 수 있다. 힙은 예약된 주소 공간을 뜻한다. 최초에 프로세스가 초기화될 때 시스템은 프로세스 주고 공간 내에 하나의 힙을 생성한다. 이 힙을 프로세스의 기본 힙이라 하며, 기본 할당 크기는 1M로 정해져있다. 닷넷에서는 가비지 컬렉터가 자원을 관리하기 때문에 힙에 대한 함수를 직접 제공하지 않지만 Win32 API GetProcessHeap() 사용하여 현재 프로세스의 힙을 가져올 수 있다.
using System.Runtime.InteropServices;
public unsafe class Memory
{
static int ph = GetProcessHeap();
[DllImport("kernel32")]
static extern int GetProcessHeap();
}
응용 프로그램이 다양한 API를 호출하는 메소드를 가지기 때문에 기본 힙에 대한 액세스는 순차화(serialize)된다. 즉, 시스템은 한 번에 하나의 쓰레드만 기본 힙으로부터 메모리를 할당받거나 반환하도록 보장한다. 두 개의 쓰레드가 동시에 기본 힙으로부터 메모리 블록을 할당하려고 하면, 하나의 쓰레드만 메모리 블록을 할당받을 수 있고, 다른 쓰레드는 첫번째 쓰레드의 블록이 할당될 때 까지 기다려야한다. 첫번째 쓰레드의 블록이 할당되면 두번째 쓰레드가 힙을 할당받을 수 있도록 허용한다. 이와 같이 한 번에 하나의 힙에 액세스하는 것은 성능을 저하시킨다. 어셈블리 또는 DLL은 자신의 힙을 갖지 않는다. 또한 기본 힙은 프로세스가 생성되기 전에 생성되고, 프로세스가 종료될 때 자동으로 파괴된다.
힙의 구조
힙은 여러가지 힙 할당자에 의해 관리된다. 프로세스별 기본 힙 할당자, 응용 프로그램 전용 힙 할당자, C 런타임 힙 할당자, CLR 힙 할당자로 구성된다. 이러한 힙 할당자(8 바이트에서 1024 바이트까지 128개의 할당자로 구성)는 Win32 힙 할당자에서 관리하고, Win32 힙 할당자는 NT 런타힘 힙 할당자에서 관리하며, 여기서 가상 메모리를 할당한다. 힙을 할당 할 때 사용 가능한 블록을 찾으며, 사용 가능한 블록을 발견할 수 없을 때 가상 메모리를 예약하고 힙을 확장한다. 마찬가지로 임시 메모리의 사용이 끝나면 힙을 반환한다. 프로세스 힙은 기본적으로 병합을 수행한다. 즉, 인접한 힙이 비어있는 경우에 힙을 하나로 합쳐서 내부 단편화를 막는다.
닷넷 런타임은 큰 블록을 할당하여 자체 메모리 관리를 수행한다. 닷넷은 하나의 큰 연속된 공간을 위해 64M를 예약하며,공간이 부족한 경우에 3M 단위로 블록을 확장한다. 힙은 128개의 할당자중에 127개의 할당자를 사용하고, 첫번째 할당자는 다른 용도로 사용한다. 이들 할당자는 이중 링크드 리스트로 관리되며, 첫번째는 top chunk 사용을 위해 예약되었다.
힙의 성능 저하
힙의 수행 속도를 떨어뜨리는 경우는 다음과 같다.
- 힙을 해제하는 경우에는 해제 자체보다는 해제된 힙을 병합할 때 오버헤드가 발생하며, 병합하는 동안 인접 항목을 찾아내어 더 큰 블록을 만들고 해제한다. 이러한 찾기가 발생하는 동안에 힙에 대한 임의의 액세스가 발생하고, 힙이 할당되는 경우에 캐시 누락이 발생한다.
- 많은 메모리를 사용하는 DLL을 여러 개의 쓰레드에서 실행하거나 다중 프로세서 시스템에서 실행하면 실행이 느려진다. 힙 손상은 닷넷이 직접 관리하지만 프로그래머가 Win32 API를 사용하거나 컴파일을 할 때 발생하는 경우가 있다. 힙 손상은 이미 해제된 블록을 다시 해제하는 경우, 해제한 블록을 사용하려는 경우, 블록 경계를 벗어나거나 다른 스레드에 의해서 블록을 덮어쓴 경우에 발생한다.
- 힙 성능 개선 윈도우 2000은 NT에 비해서 힙에 대한 성능을 개선시켰고, 이러한 것은 CLR에도 반영되었다. 예를 들어서, 프로세스 기본 힙에 대한 잠금을 최소화 하도록 알고리즘을 개선했으며, 할당 캐시(Alloc-Cache)를 사용하는 대신에 128개의 할당자에서 이용할 수 있는 목록을 별도로 관리하는 Lookaside 목록을 사용한다.
- Boxing과 Unboxing을 최소화 박싱과 언박싱은 스택과 힙 사이를 데이터가 오가는 것이기 때문에 많은 오버헤드가 발생한다. 만약 정수형 데이터에 대해서 박싱이 발생한다면 정수형 데이터에 대한 래퍼 클래스를 작성하거나 인터페이스를 사용한다. 인터페이스가 참조 유형이므로 값 형식의 데이터에 대해서도 인터페이스 참조만을 갖는다는 것을 이용한 것이다. 잦은 할당을 최소화하자.
- 내부적으로는 메모리 할당과 해제가 발생한다. 예를 들어서 System.String에 문자열을 설정하고, 다시 문자열을 추가하는 경우에 내부적으로 블록이 반복적으로 할당되고 해제되는 과정이 수행된다.(System.String은 한 번 설정된 문자열을 변경할 수 없다) 따라서 문자열 연결 작업을 최소화하거나 StringBuilder 클래스를 사용하는 것이 수행 성능에 도움이 된다.
- 스레드 고유 힙 생성프로세스의 기본 힙은 여러 스레드가 공유해야하므로 경쟁이 발생하며, 수행 성능이 저하된다. 쓰레드가 빠른 읽기와 쓰기를 필요로 하고, 적은 공간의 메모리 블록을 필요로 하는 경우에 쓰레드 고유 힙을 생성할 수 있다.
출처 : https://m.hanbit.co.kr/network/category/category_view.html?cms_code=CMS2117503016
'프로그래밍 언어 > C#' 카테고리의 다른 글
C# 인터페이스 (interface) (0) | 2022.07.28 |
---|---|
C# 클래스 접근 제한자 (Access Modifier) (0) | 2022.07.27 |
C# 구조체 (struct) 클래스 (class) 차이 (0) | 2022.07.26 |
C# 클래스 타입 업/다운 캐스팅 (Up-DownCasting) (0) | 2022.07.22 |
C# 읽기 전용 (readonly) (0) | 2022.07.21 |