다음을 통해 공유


가비지 수집기 기본 및 성능 힌트

 

리코 마리아니
Microsoft Corporation

2003년 4월

요약: .NET 가비지 수집기는 메모리를 잘 사용하고 장기적인 조각화 문제가 없는 고속 할당 서비스를 제공합니다. 이 문서에서는 가비지 수집기가 작동하는 방식을 설명한 다음, 가비지 수집 환경에서 발생할 수 있는 성능 문제 중 일부에 대해 설명합니다. (인쇄된 10페이지)

적용 대상:
   Microsoft® .NET Framework

콘텐츠

소개
간소화된 모델
가비지 수집
성능
종료
결론

소개

가비지 수집기를 잘 활용하는 방법과 가비지 수집 환경에서 실행할 때 발생할 수 있는 성능 문제를 이해하려면 가비지 수집기의 작동 방식과 이러한 내부 작업이 실행 중인 프로그램에 미치는 영향의 기본 사항을 이해하는 것이 중요합니다.

이 문서는 두 부분으로 나뉩니다. 먼저 간소화된 모델을 사용하여 CLR(공용 언어 런타임)의 가비지 수집기의 특성에 대해 설명한 다음 해당 구조의 성능에 미치는 영향에 대해 설명합니다.

간소화된 모델

설명을 위해 관리되는 힙의 다음과 같은 간소화된 모델을 고려합니다. 실제로 구현되는 것은 아닙니다 .

그림 1. 관리되는 힙의 간소화된 모델

이 간소화된 모델에 대한 규칙은 다음과 같습니다.

  • 모든 가비지 수집 가능한 개체는 하나의 연속된 주소 공간에서 할당됩니다.
  • 힙은 힙의 작은 부분만 확인하여 대부분의 가비지 제거가 가능하도록 세대 (나중에 자세히 설명)로 나뉩니다.
  • 한 세대 내의 개체는 모두 거의 같은 나이입니다.
  • 숫자가 높은 세대는 이전 개체가 있는 힙의 영역을 나타냅니다. 이러한 개체는 안정될 가능성이 훨씬 더 높습니다.
  • 가장 오래된 개체는 가장 낮은 주소에 있고 새 개체는 주소 증가 시 만들어집니다. (위의 그림 1에서 주소가 감소하고 있습니다.)
  • 새 개체에 대한 할당 포인터는 사용된(할당됨)과 사용되지 않은(사용 가능한) 메모리 영역 간의 경계를 표시합니다.
  • 데드 개체를 제거하고 라이브 개체를 힙의 낮은 주소 끝쪽으로 밀어 주기적으로 힙을 압축합니다. 이렇게 하면 새 개체가 만들어지는 다이어그램 아래쪽의 사용되지 않는 영역이 확장됩니다.
  • 메모리에 있는 개체의 순서는 좋은 지역성을 위해 만들어진 순서로 유지됩니다.
  • 힙의 개체 간에는 간격이 없습니다.
  • 사용 가능한 공간 중 일부만 커밋됩니다. 필요한 경우 ** 예약 된 주소 범위의 운영 체제에서 더 많은 메모리를 획득합니다.

가비지 수집

가장 쉽게 이해할 수 있는 컬렉션은 가비지 수집을 완전히 압축하는 것이므로 먼저 설명하겠습니다.

전체 컬렉션

전체 컬렉션에서 프로그램 실행을 중지하고 GC 힙에 대한 모든 루트 를 찾아야 합니다. 이러한 루트는 다양한 형태로 제공되지만, 특히 힙을 가리키는 스택 및 전역 변수입니다. 루트부터 모든 개체를 방문하고 방문한 모든 개체에 포함된 모든 개체 포인터를 따라가면서 개체를 표시합니다. 이러한 방식으로 수집기는 연결할 수 있는 모든 개체 또는 라이브 개체를 찾습니다. 다른 개체, 연결할 수 없는 개체는 이제 비난됩니다.

그림 2. GC 힙에 뿌리

연결할 수 없는 개체가 식별되면 나중에 사용할 수 있도록 해당 공간을 회수하려고 합니다. 이 시점에서 수집기의 목표는 라이브 개체를 위로 밀고 낭비된 공간을 제거하는 것입니다. 실행이 중지되면 수집기에서 해당 개체를 모두 이동하고 모든 포인터를 수정하여 모든 항목이 새 위치에 제대로 연결되도록 하는 것이 안전합니다. 남은 개체는 다음 세대 번호(세대의 경계가 업데이트됨)로 승격되고 실행을 다시 시작할 수 있습니다.

부분 컬렉션

불행히도 전체 가비지 수집은 매번 수행하기에는 너무 비싸기 때문에 이제 컬렉션에 세대를 두는 것이 어떻게 도움이 되는지 논의하는 것이 적절합니다.

먼저 우리가 매우 운이 좋은 가상의 경우를 생각해 봅시다. 최근 전체 컬렉션이 있고 힙이 잘 압축되었다고 가정해 보겠습니다. 프로그램 실행이 다시 시작되고 일부 할당이 발생합니다. 실제로 많은 할당이 발생하고 충분한 할당 후에 메모리 관리 시스템에서 수집할 시간을 결정합니다.

이제 운이 좋은 곳이 있습니다. 마지막 컬렉션 이후 항상 이전 개체에 대해 전혀 쓰지 않았고 새로 할당된 0세대(gen0)만 개체에 기록되었다고 가정합니다. 이런 일이 발생하면 가비지 수집 프로세스를 크게 간소화할 수 있기 때문에 좋은 상황에 처하게 될 것입니다.

일반적인 전체 수집 대신 모든 이전 개체(1세대,2세대) 가 여전히 살아 있거나 적어도 충분히 살아 있어 해당 개체를 살펴볼 가치가 없다고 가정할 수 있습니다. 또한 그 중 어느 것도 기록되지 않았기 때문에 (우리가 얼마나 운이 좋은지 기억하십시오?) 이전 개체에서 최신 개체에 대한 포인터가 없습니다. 따라서 우리가 할 수 있는 일은 평소와 같은 모든 뿌리를 보는 것이며, 뿌리가 오래된 개체를 가리키면 그 뿌리를 무시합니다. 다른 루트(0세대를 가리키는 루트)의 경우 모든 포인터를 따라 평소와 같이 진행합니다. 이전 개체로 돌아가는 내부 포인터를 찾을 때마다 무시됩니다.

이 프로세스가 완료되면 이전 세대의 개체를 방문하지 않고0 세대의 모든 라이브 개체를 방문하게 됩니다. 그런 다음0 세대 개체는 평소와 같이 비난받을 수 있으며, 이전 개체는 방해받지 않고 메모리 영역만 위로 밀어 밉합니다.

우리는 죽은 공간의 대부분이 변동의 큰 거래가 젊은 개체에있을 가능성이 있다는 것을 알고 있기 때문에 지금 이것은 우리에게 정말 좋은 상황이다. 많은 클래스는 반환 값, 임시 문자열 및 열거자 및 무엇과 같은 다양한 기타 유틸리티 클래스에 대한 임시 개체를 만듭니다. 0세대만 보면 거의 개체만 확인하여 대부분의 데드 공간을 쉽게 되돌릴 수 있습니다.

아쉽게도, 적어도 일부 오래된 개체는 새 개체를 가리키도록 변경해야 하기 때문에 이 접근 방식을 사용할 만큼 운이 좋지 않습니다. 이 경우 무시만으로는 충분하지 않습니다.

쓰기 장벽으로 세대 작동

위의 알고리즘이 실제로 작동하도록 하려면 수정된 이전 개체를 알아야 합니다. 더티 개체의 위치를 기억하기 위해 카드 테이블이라는 데이터 구조를 사용하고 관리 코드 컴파일러가 소위 쓰기 장벽을 생성하는 이 데이터 구조를 유지 관리합니다. 이 두 가지 개념은 생성 기반 가비지 수집의 성공의 중심입니다.

카드 테이블은 다양한 방법으로 구현할 수 있지만 가장 쉬운 방법은 비트 배열입니다. 카드 테이블의 각 비트는 힙의 메모리 범위를 나타냅니다. 128바이트라고 가정해 보겠습니다. 프로그램이 개체를 일부 주소로 쓸 때마다 쓰기 장벽 코드는 128 바이트 청크가 작성된 것을 계산한 다음 카드 테이블에서 해당 비트를 설정해야 합니다.

이 메커니즘을 사용하면 이제 컬렉션 알고리즘을 다시 검토할 수 있습니다. 0세대 가비지 수집을 수행하는 경우 위에서 설명한 대로 알고리즘을 사용하여 이전 세대에 대한 포인터를 무시할 수 있지만, 일단 완료되면 카드 테이블에서 수정된 것으로 표시된 청크에 있는 모든 개체의 모든 개체 포인터도 찾아야 합니다. 우리는 뿌리처럼 대해야 합니다. 이러한 포인터도 고려하면0 세대 개체만 올바르게 수집합니다.

이 방법은 카드 테이블이 항상 가득 차 있으면 전혀 도움이 되지 않지만 실제로는 이전 세대의 포인터 중 비교적 적은 수의 포인터가 실제로 수정되므로 이 접근 방식에서 상당한 비용을 절감할 수 있습니다.

성능

이제 작동 방식에 대한 기본 모델이 있으므로 문제가 발생할 수 있는 몇 가지 사항을 고려해 보겠습니다. 이를 통해 수집가에서 최고의 성능을 얻기 위해 피해야 할 것들을 잘 알 수 있습니다.

너무 많은 할당

이것은 정말 잘못 될 수 있는 가장 기본적인 것. 가비지 수집기를 사용하여 새 메모리를 할당하는 것은 매우 빠릅니다. 위의 그림 2에서 볼 수 있듯이 일반적으로 할당 포인터를 이동하여 "할당된" 쪽에서 새 개체에 대한 공간을 만드는 것만 있으면 됩니다. 이보다 훨씬 빠르지는 않습니다. 그러나 조만간 가비지 수집이 일어나야 하며, 모든 것이 동일하기 때문에 더 빨리 일어나는 것이 좋습니다. 따라서 새 개체를 만들 때는 개체를 만드는 것이 빠르더라도 실제로 필요하고 적절한지 확인하려고 합니다.

이는 명백한 조언처럼 들릴 수 있지만 실제로는 작성하는 작은 코드 줄이 많은 할당을 트리거할 수 있다는 사실을 잊어버리기 쉽습니다. 예를 들어 어떤 종류의 비교 함수를 작성한다고 가정하고 개체에 키워드 필드가 있고 지정된 순서대로 키워드에 대/소문자를 구분하지 않도록 하려는 경우를 가정합니다. 이제 이 경우 첫 번째 키워드(keyword) 매우 짧을 수 있으므로 전체 키워드 문자열을 비교할 수 없습니다. String.Split을 사용하여 키워드(keyword) 문자열을 조각으로 나눈 다음 일반적인 대/소문자를 구분하지 않는 비교를 사용하여 각 조각을 비교하는 것이 좋습니다. 좋은 오른쪽 소리?

글쎄, 그것은 그런 일을 밝혀로 그렇게 좋은 생각이 아니다. String.Split은 문자열 배열을 만듭니다. 즉, 원래 키워드 문자열에 있는 모든 키워드(keyword) 대해 하나의 새 문자열 개체와 배열에 대해 하나 이상의 개체를 의미합니다. 저런! 정렬의 컨텍스트에서 이 작업을 수행하는 경우 많은 비교가 수행되고 두 줄 비교 함수가 이제 매우 많은 수의 임시 개체를 만듭니다. 갑자기 가비지 수집기가 사용자를 대신하여 매우 열심히 일할 것이고, 영리한 수집 체계에도 불구하고 클린 쓰레기가 많이 있습니다. 할당이 전혀 필요하지 않은 비교 함수를 작성하는 것이 좋습니다.

Too-Large 할당

malloc()와 같은 기존 할당자를 사용하는 경우 프로그래머는 할당 비용이 비교적 높다는 것을 알고 있으므로 malloc()를 최대한 적게 호출하는 코드를 작성하는 경우가 많습니다. 이는 청크에서 할당하는 관행으로 변환되며, 종종 필요할 수 있는 개체를 투기적으로 할당하여 총 할당을 줄일 수 있습니다. 그런 다음 미리 할당된 개체는 일종의 풀에서 수동으로 관리되어 일종의 고속 사용자 지정 할당자를 효과적으로 만듭니다.

관리되는 세계에서 이 사례는 여러 가지 이유로 훨씬 덜 매력적입니다.

첫째, 할당을 수행하는 비용은 매우 낮습니다. 기존 할당자와 마찬가지로 무료 블록을 검색할 수 없습니다. 자유 영역과 할당된 영역 간의 경계만 이동해야 합니다. 할당 비용이 낮다는 것은 풀에 대한 가장 강력한 이유가 존재하지 않는다는 것을 의미합니다.

둘째, 사전 할당을 선택하면 당연히 즉각적인 요구 사항에 필요한 것보다 더 많은 할당을 수행하게 되며, 이로 인해 불필요한 추가 가비지 수집이 강제로 발생할 수 있습니다.

마지막으로, 가비지 수집기는 수동으로 재활용하는 개체의 공간을 회수할 수 없습니다. 전역 관점에서 현재 사용되지 않는 개체를 포함한 모든 개체가 여전히 라이브 상태이기 때문입니다. 사용 준비가 완료되었지만 사용 중인 개체를 손에 들고 있지 않은 상태로 유지하면 많은 메모리가 낭비될 수 있습니다.

이것은 사전 할당이 항상 나쁜 생각이라고 말하는 것이 아닙니다. instance 위해 특정 개체를 강제로 함께 할당하도록 할 수도 있지만 관리되지 않는 코드보다 일반적인 전략으로 덜 매력적일 수 있습니다.

포인터가 너무 많습니다.

포인터의 큰 메시인 데이터 구조를 만드는 경우 두 가지 문제가 발생합니다. 첫째, 많은 개체 쓰기(아래 그림 3 참조)가 있으며, 둘째, 데이터 구조를 수집할 때 가비지 수집기가 모든 포인터를 따르도록 하고 필요한 경우 모든 포인터를 변경합니다. 데이터 구조가 수명이 길고 많이 변경되지 않는 경우 수집기는 전체 컬렉션이 발생할 때(2 세대 수준에서) 모든 포인터만 방문하면 됩니다. 그러나 트랜잭션 처리의 일부로 이러한 구조를 일시적인 방식으로 만들면 비용을 훨씬 더 자주 지불하게 됩니다.

그림 3. 포인터가 많은 데이터 구조

포인터가 많은 데이터 구조에는 가비지 수집 시간과 관련이 없는 다른 문제도 있을 수 있습니다. 앞에서 설명한 것처럼 개체가 만들어질 때 할당 순서대로 연속적으로 할당됩니다. instance 파일에서 정보를 복원하여 크고 복잡할 수 있는 데이터 구조를 만드는 경우에 적합합니다. 서로 다른 데이터 형식이 있더라도 모든 개체가 메모리에서 함께 닫히게 되므로 프로세서가 해당 개체에 빠르게 액세스할 수 있습니다. 그러나 시간이 지날수록 데이터 구조가 수정되면 새 개체를 이전 개체에 연결해야 할 수 있습니다. 이러한 새 개체는 훨씬 나중에 만들어지므로 메모리의 원래 개체 근처에 있지 않습니다. 가비지 수집기가 메모리를 압축하더라도 개체가 메모리에서 섞이지 않고 단순히 함께 "슬라이드"되어 낭비된 공간을 제거합니다. 그 결과 장애는 시간이 지남에 따라 너무 나빠서 전체 데이터 구조의 신선한 사본을 만들고, 모두 멋지게 포장하고, 오래된 무질서한 사람이 적절한 과정에서 수집가에 의해 비난받을 수 있습니다.

너무 많은 뿌리

가비지 수집기는 수집 시 루트에 특별한 처리를 제공해야 합니다. 항상 열거되고 정식으로 고려되어야 합니다. Gen0 컬렉션은 고려해야 할 뿌리의 홍수를 주지 않는 범위까지만 빠를 수 있습니다. 해당 지역 변수 중 많은 개체 포인터가 있는 매우 재귀적인 함수를 만드는 경우 결과는 실제로 비용이 많이 들 수 있습니다. 이 비용은 이러한 모든 루트를 고려해야 할 뿐만 아니라 그 뿌리가 오래 지속되지 않을 수 있는 매우 많은 수의0 세대 개체에서도 발생합니다(아래에서 설명).

너무 많은 개체 쓰기

이전 토론을 다시 한 번 언급하면 관리되는 프로그램이 개체 포인터를 수정할 때마다 쓰기 장벽 코드도 트리거됩니다. 이는 다음 두 가지 이유로 잘못될 수 있습니다.

첫째, 쓰기 장벽의 비용은 처음에 수행하려고 했던 비용과 비슷할 수 있습니다. instance 경우 일부 종류의 열거자 클래스에서 간단한 작업을 수행하는 경우 모든 단계에서 기본 컬렉션의 일부 키 포인터를 열거자로 이동해야 할 수 있습니다. 쓰기 장벽으로 인해 포인터를 복사하는 비용을 효과적으로 두 배로 늘리며 열거자에서 루프당 하나 이상의 작업을 수행해야 할 수 있기 때문에 실제로는 피해야 할 수 있습니다.

둘째, 이전 개체에 실제로 쓰는 경우 쓰기 장벽을 트리거하는 것은 두 배로 나쁩니다. 이전 개체를 수정할 때 다음 가비지 수집이 발생할 때 검사(위에서 설명한) 추가 루트를 효과적으로 만듭니다. 이전 개체를 충분히 수정한 경우 가장 어린 세대만 수집하는 것과 관련된 일반적인 속도 향상을 효과적으로 부정할 수 있습니다.

이러한 두 가지 이유는 물론 어떤 종류의 프로그램에서도 너무 많은 쓰기를 수행하지 않는 일반적인 이유로 보완됩니다. 모든 것이 같으면 프로세서 캐시를 더 경제적으로 사용할 수 있도록 메모리(읽기 또는 쓰기)를 적게 터치하는 것이 좋습니다.

거의 긴 수명 개체가 너무 많습니다.

마지막으로, 아마도 세대 가비지 수집기의 가장 큰 함정은 정확히 일시적이지도 않고 정확히 수명이 긴 개체도 아닌 많은 개체를 만드는 것입니다. 이러한 개체는 여전히 필요하므로 0세대 컬렉션(가장 저렴한 컬렉션) 의해 정리되지 않기 때문에 많은 문제를 일으킬 수 있으며, 여전히 사용 중이기 때문에1 세대 컬렉션에서도 살아남을 수 있지만 곧 죽습니다.

문제는 개체가2 세대 수준에 도달하면 전체 컬렉션만 제거되고 전체 컬렉션은 가비지 수집기가 합리적으로 가능한 한 지연될 만큼 비용이 많이 듭니다. 따라서 많은 "거의 수명이 긴" 개체가 있는 결과는2 세대가 잠재적으로 놀라운 속도로 성장하는 경향이 있다는 것입니다. 그것은 당신이 원하는만큼 빨리 정리되지 않을 수 있습니다, 그것은 청소를 얻을 때 그것은 확실히 당신이 원하는 것보다 그렇게 훨씬 더 비용이 많이 드는 것입니다.

이러한 종류의 개체를 방지하기 위해 최상의 방어선은 다음과 같습니다.

  1. 사용 중인 임시 공간의 양에 주의하여 가능한 한 적은 수의 개체를 할당합니다.
  2. 수명이 긴 개체 크기를 최소한으로 유지합니다.
  3. 스택에 가능한 한 적은 수의 개체 포인터를 유지합니다(루트임).

이러한 작업을 수행하면Gen 0 컬렉션이 매우 효과적일 가능성이 높으며1 세대는 매우 빠르게 성장하지 않습니다. 결과적으로1 세대 컬렉션은 빈도가 낮아질 수 있으며,1 세대 컬렉션을 수행하는 것이 신중해지면 중간 수명 개체는 이미 죽었으며 그 당시 저렴하게 복구할 수 있습니다.

상황이 잘 진행되면 안정 상태 작업 중에 gen2 크기가 전혀 증가하지 않습니다!

종료

이제 간소화된 할당 모델로 몇 가지 topics 살펴보았으므로 한 가지 더 중요한 현상에 대해 논의할 수 있도록 작업을 약간 복잡하게 만들고 싶습니다. 이것이 종료자 및 종료 비용입니다. 간단히 말해서, 종료자는 모든 클래스에 있을 수 있습니다. 가비지 수집기가 해당 개체의 메모리를 회수하기 전에 배달 못한 개체를 호출할 것을 약속하는 선택적 멤버입니다. C#에서는 ~클래스 구문을 사용하여 종료자를 지정합니다.

종료가 컬렉션에 미치는 영향

가비지 수집기가 죽은 개체를 처음 발견했지만 여전히 완료해야 하는 경우 해당 개체의 공간을 회수하려는 시도를 포기해야 합니다. 대신 개체가 종료가 필요한 개체 목록에 추가되고, 또한 수집기는 종료가 완료될 때까지 개체 내의 모든 포인터가 유효한지 확인해야 합니다. 이는 기본적으로 완료가 필요한 모든 개체가 수집기 관점에서 임시 루트 개체와 같다고 말하는 것과 같습니다.

컬렉션이 완료되면 적절하게 명명된 종료 스레드 는 종료가 필요한 개체 목록을 살펴보고 종료자를 호출합니다. 이 작업을 수행하면 개체가 다시 한 번 죽게 되고 정상적인 방식으로 자연스럽게 수집됩니다.

종료 및 성능

이러한 기본적인 마무리 이해로 우리는 이미 몇 가지 매우 중요한 것들을 추론할 수 있습니다.

먼저 종료가 필요한 개체는 그렇지 않은 개체보다 더 오래 살 수 있습니다. 사실, 그들은 훨씬 더 오래 살 수 있습니다. instance2세대에 있는 개체를 완료해야 한다고 가정합니다. 종료는 예약되지만 개체는여전히 2 세대에 있으므로 다음 Gen2 컬렉션이 발생할 때까지 다시 수집되지 않습니다. 2세대 컬렉션은 비용이 많이 들고 따라서 매우 드물게 발생하기를 하기 때문에 실제로 매우 긴 시간이 될 수 있으며, 실제로 상황이 잘 진행된다면 오랜 시간이 될 것입니다. 완료가 필요한 이전 개체는 공간이 회수되기 전에 수백 개의 gen0 컬렉션이 아닌 경우 수십 개의 대기해야 할 수 있습니다.

둘째, 마무리가 필요한 개체는 부수적인 손상을 일으킵니다. 내부 개체 포인터는 유효한 상태를 유지해야 하므로 직접 종료가 필요한 개체는 메모리에 남아 있을 뿐만 아니라 개체가 참조하는 모든 항목도 직접 및 간접적으로 메모리에 유지됩니다. 완료가 필요한 단일 개체에 의해 거대한 개체 트리가 고정된 경우, 방금 설명한 대로 전체 트리가 오랫동안 남아 있을 수 있습니다. 따라서 종료자를 드물게 사용하고 가능한 한 적은 내부 개체 포인터가 있는 개체에 배치하는 것이 중요합니다. 방금 제공한 트리 예제에서는 종료가 필요한 리소스를 별도의 개체로 이동하고 트리의 루트에 해당 개체에 대한 참조를 유지하여 문제를 쉽게 방지할 수 있습니다. 이러한 겸손한 변경으로 하나의 개체(멋진 작은 개체)만 남아 있고 종료 비용이 최소화됩니다.

마지막으로 종료가 필요한 개체는 종료자 스레드에 대한 작업을 만듭니다. 종료 프로세스가 복잡한 프로세스인 경우 종료자 스레드 하나만 해당 단계를 수행하는 데 많은 시간을 소비하므로 백로그가 작동하므로 더 많은 개체가 종료를 기다리는 동안 남아 있을 수 있습니다. 따라서 종료자가 가능한 한 적은 작업을 수행하는 것이 매우 중요합니다. 또한 모든 개체 포인터는 종료 중에 유효한 상태로 유지되지만 해당 포인터가 이미 완료된 개체로 이어질 수 있으므로 유용하지 않을 수 있습니다. 일반적으로 포인터가 유효하더라도 종료 코드에서 개체 포인터를 따르는 것을 방지하는 것이 가장 안전합니다. 안전하고 짧은 종료 코드 경로가 가장 좋습니다.

IDisposable 및 Dispose

대부분의 경우 IDisposable 인터페이스를 구현하여 해당 비용을 방지하기 위해 항상 종료해야 하는 개체가 있을 수 있습니다. 이 인터페이스는 프로그래머에게 수명이 잘 알려져 있고 실제로 상당히 발생하는 리소스를 회수하는 대체 방법을 제공합니다. 물론 개체가 단순히 메모리만 사용하므로 종료 또는 삭제가 전혀 필요하지 않은 경우에도 더 좋습니다. 그러나 완료가 필요하고 개체의 명시적 관리가 쉽고 실용적인 경우가 많은 경우 IDisposable 인터페이스를 구현하는 것이 마무리 비용을 방지하거나 최소한 줄일 수 있는 좋은 방법입니다.

C# 구문에서 이 패턴은 매우 유용할 수 있습니다.

class X:  IDisposable
{
   public X(…)
   {
   … initialize resources … 
   }

   ~X()
   {
   … release resources … 
   }

   public void Dispose()
   {
// this is the same as calling ~X()
        Finalize(); 

// no need to finalize later
System.GC.SuppressFinalize(this); 
   }
};

Dispose를 수동으로 호출하면 수집기가 개체를 활성 상태로 유지하고 종료자를 호출할 필요가 없습니다.

결론

.NET 가비지 수집기는 메모리를 잘 사용하고 장기적인 조각화 문제가 없는 고속 할당 서비스를 제공하지만 최적의 성능보다 훨씬 적은 성능을 제공하는 작업을 수행할 수 있습니다.

할당자를 최대한 활용하려면 다음과 같은 방법을 고려해야 합니다.

  • 지정된 데이터 구조에 동시에 사용할 모든 메모리(또는 가능한 한 많이)를 할당합니다.
  • 복잡성이 거의 발생하지 않도록 방지할 수 있는 임시 할당을 제거합니다.
  • 개체 포인터를 쓰는 횟수, 특히 이전 개체에 대한 쓰기를 최소화합니다.
  • 데이터 구조에서 포인터의 밀도를 줄입니다.
  • 종료자를 제한한 다음 "리프" 개체에서만 최대한 많이 사용합니다. 이 작업을 돕기 위해 필요한 경우 개체를 중단합니다.

할당 프로파일러와 같은 도구를 사용하여 주요 데이터 구조를 검토하고 메모리 사용 프로필을 수행하는 정기적인 방법은 메모리 사용량을 효과적으로 유지하고 가비지 수집기가 가장 잘 작동하도록 하는 데 큰 영향을 줍니다.