Programing

C #에서 참조 계산 + 가비지 수집이없는 이유는 무엇입니까?

lottogame 2021. 1. 6. 07:38
반응형

C #에서 참조 계산 + 가비지 수집이없는 이유는 무엇입니까?


저는 C ++ 배경 출신이며 약 1 년 동안 C #으로 작업했습니다. 다른 많은 사람들과 마찬가지로 결정 론적 리소스 관리가 언어에 내장되지 않은 이유에 대해 당황합니다. 결정 론적 소멸자 대신 처리 패턴이 있습니다. 사람들 은 자신의 코드를 통해 IDisposable 암을 퍼뜨리는 것이 그만한 가치가 있는지 궁금해 하기 시작합니다 .

내 C ++ 편향 뇌에서 결정적 소멸자와 함께 참조 계산 스마트 포인터를 사용하는 것은 IDisposable을 구현하고 dispose를 호출하여 비 메모리 리소스를 정리해야하는 가비지 수집기에서 중요한 단계 인 것 같습니다. 솔직히, 나는별로 똑똑하지 않다 ... 그래서 나는 이것이 왜 상황이 그런지 더 잘 이해하려는 욕망에서 이걸 묻고있다.

C #이 다음과 같이 수정되면 어떻게됩니까?

개체는 참조 횟수입니다. 개체의 참조 횟수가 0이되면 리소스 정리 메서드가 개체에서 결정적으로 호출 된 다음 개체가 가비지 수집 대상으로 표시됩니다. 가비지 수집은 메모리가 회수되는 미래의 비 결정적 시간에 발생합니다. 이 시나리오에서는 IDisposable을 구현하거나 Dispose를 호출하는 것을 기억할 필요가 없습니다. 해제 할 비 메모리 리소스가있는 경우 리소스 정리 기능을 구현하기 만하면됩니다.

  • 그게 왜 나쁜 생각입니까?
  • 그것이 가비지 수집기의 목적을 무너 뜨릴까요?
  • 그런 것을 구현하는 것이 가능할까요?

편집 : 지금까지의 의견에서 이것은 나쁜 생각입니다.

  1. GC는 참조 계수없이 더 빠릅니다.
  2. 개체 그래프의주기 처리 문제

첫 번째는 타당하다고 생각하지만 두 번째는 약한 참조를 사용하여 처리하기 쉽습니다.

따라서 속도 최적화가 다음과 같은 단점보다 중요합니다.

  1. 비 메모리 리소스를 적시에 해제 할 수 없습니다.
  2. 비 메모리 리소스를 너무 빨리 해제 할 수 있음

리소스 정리 메커니즘이 결정적이고 언어에 내장 된 경우 이러한 가능성을 제거 할 수 있습니다.


Brad Abrams는 .Net 프레임 워크 개발 과정에서 작성한 Brian Harry의 이메일을 게시 했습니다 . 초기 우선 순위 중 하나가 참조 계수를 사용하는 VB6과 의미 론적 동등성을 유지하는 것이었지만 참조 계수가 사용되지 않은 많은 이유를 자세히 설명합니다. 일부 유형의 참조는 계산되고 다른 유형은 계산되지 않음 ( IRefCounted!), 특정 인스턴스 참조가 계산되고 이러한 솔루션이 허용되지 않는 이유를 조사합니다.

[자원 관리 및 결정 론적 마무리 문제]는 매우 민감한 주제이기 때문에 가능한 한 정확하고 완전하게 설명하려고합니다. 우편물의 길이에 대해 사과드립니다. 이 메일의 처음 90 %는 문제가 정말 어렵다는 것을 납득시키려는 것입니다. 마지막 부분에서는 우리가하려는 일에 대해 이야기 할 것입니다. 그러나 우리가 왜 이러한 옵션을 검토하는지 이해하려면 첫 번째 부분이 필요합니다.

...

우리는 처음 에 솔루션이 자동 참조 계산 (프로그래머가 잊을 수없는) 의 형태를 취할 것이라는 가정과 함께 사이클을 자동으로 감지하고 처리하기위한 몇 가지 다른 요소를 사용 한다는 가정으로 시작했습니다 . ... 우리는 궁극적으로 이것이 일반적인 경우에 효과가 없을 것이라고 결론지었습니다.

...

요약하자면:

  • 프로그래머가 이러한 복잡한 데이터 구조 문제를 이해하고 추적하고 설계하도록 강요하지 않고 순환 문제해결하는 것이 매우 중요하다고 생각합니다 .
  • 우리는 고성능 (속도와 작업 세트 모두) 시스템을 갖고 있는지 확인하고 싶고 분석 결과 시스템의 모든 단일 개체에 대해 참조 계수를 사용 하면이 목표를 달성 할 수 없다는 것을 알 수 있습니다.
  • 컴포지션 및 캐스팅 문제를 포함한 다양한 이유로 참조 계산이 필요한 객체 만 갖는 간단한 투명한 솔루션없습니다 .
  • 우리는 단일 언어 / 컨텍스트에 대해 결정 론적 마무리를 제공하는 솔루션을 선택하지 않기로 결정했습니다. 다른 언어와의 상호 운용금지 하고 언어 별 버전을 생성하여 클래스 라이브러리의 분기를 유발 하기 때문 입니다.

가비지 수집기는 정의하는 모든 클래스 / 유형에 대해 Dispose 메서드를 작성할 필요가 없습니다 . 정리를 위해 명시 적으로 수행해야하는 경우에만 하나를 정의합니다. 네이티브 리소스를 명시 적으로 할당 한 경우 대부분의 경우 GC는 객체에 대해 new ()와 같은 작업을 수행하더라도 메모리를 회수합니다.

GC는 참조 카운팅을 수행합니다. 그러나 컬렉션을 수행 할 때마다 '도달 할 수있는'( Ref Count > 0) 객체를 찾는 방식으로 다른 방식으로 수행합니다. 단지 정수 카운터 방식으로 수행하지 않습니다. . 연결할 수없는 개체가 수집됩니다 ( ). 이렇게하면 런타임에서 개체가 할당되거나 해제 될 때마다 테이블을 정리 / 업데이트 할 필요가 없습니다.Ref Count = 0

C ++ (결정적)와 C # (비 결정적)의 유일한 주요 차이점은 개체가 정리되는시기입니다. 개체가 C #에서 수집되는 정확한 순간을 예측할 수 없습니다.

Umpteenth 플러그 : GC 작동 방식에 정말 관심이있는 경우 C #통해 CLR 의 GC에 대한 Jeffrey Richter의 스탠드 업 장을 읽는 것이 좋습니다 .


참조 계산은 C #에서 시도되었습니다. Rotor (소스를 사용할 수있는 CLR의 참조 구현)를 출시 한 사람들은 세대 별 GC와 비교하기 위해 참조 계산 기반 GC를 수행했다고 생각합니다. 결과는 놀랍습니다. "stock"GC는 훨씬 빨랐고 재미도 없었습니다. 정확히 어디서 들었는지 기억이 나지 않습니다. Hanselmuntes 팟 캐스트 중 하나 인 것 같습니다. C ++를보고 싶다면 기본적으로 C #과의 성능 비교에서 압도당하는 경우-google Raymond Chen의 중국어 사전 앱. 그는 C ++ 버전을 만들었고 Rico Mariani는 C # 버전을 만들었습니다. 마침내 C # 버전을이기려면 Raymond 6 반복이 필요하다고 생각하지만, 그때까지 그는 C ++의 모든 멋진 객체 지향성을 삭제하고 win32 API 수준으로 내려 가야했습니다. 모든 것이 성능 해킹으로 바뀌 었습니다.


C ++ 스타일 스마트 포인터 참조 계산과 참조 계산 가비지 수집에는 차이가 있습니다. 나는 또한 내 블로그 에서 차이점에 대해 이야기 했지만 다음은 간단한 요약입니다.

C ++ 스타일 참조 계산 :

  • 감소에 대한 무제한 비용 : 큰 데이터 구조의 루트가 0으로 감소하면 모든 데이터를 해제하는 데 무한한 비용이 발생합니다.

  • 수동주기 수집 : 주기적 데이터 구조가 메모리 누수를 방지하기 위해 프로그래머는주기의 일부를 약한 스마트 포인터로 대체하여 잠재적 인 구조를 수동으로 중단해야합니다. 이것은 잠재적 인 결함의 또 다른 원인입니다.

참조 계산 가비지 수집

  • 지연된 RC : 스택 및 레지스터 참조에 대해 객체 참조 카운트에 대한 변경 사항이 무시됩니다. 대신 GC가 트리거 될 때 이러한 개체는 루트 집합을 수집하여 유지됩니다. 참조 횟수에 대한 변경은 지연되고 일괄 처리 될 수 있습니다. 그 결과 처리량높아집니다 .

  • 통합 : 쓰기 장벽 을 사용하여 참조 횟수에 대한 변경 사항을 통합 할 수 있습니다. 이렇게하면 자주 변경되는 참조에 대한 RC 성능을 향상시키는 개체 참조 수에 대한 대부분의 변경 사항을 무시할 수 있습니다.

  • 주기 감지 : 완전한 GC 구현을 위해주기 감지기도 사용해야합니다. 그러나 증분 방식으로주기 감지를 수행 할 수 있으며, 이는 다시 제한된 GC 시간을 의미합니다.

기본적으로 Java의 JVM 및 .net CLR 런타임과 같은 런타임을위한 고성능 RC 기반 가비지 수집기를 구현할 수 있습니다.

추적 수집기는 역사적 이유로 부분적으로 사용되고 있다고 생각합니다. 최근 참조 계산의 많은 개선 사항은 JVM과 .net 런타임이 모두 출시 된 이후에 이루어졌습니다. 연구 작업도 생산 프로젝트로 전환하는 데 시간이 걸립니다.

결정 론적 자원 폐기

이것은 거의 별개의 문제입니다. .net 런타임은 아래 예제와 같이 IDisposable 인터페이스를 사용하여이를 가능하게합니다. 나는 또한 Gishu의 대답을 좋아 합니다.


@Skrymsli , 이것이 " using "키워드 의 목적입니다 . 예 :

public abstract class BaseCriticalResource : IDiposable {
    ~ BaseCriticalResource () {
        Dispose(false);
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this); // No need to call finalizer now
    }

    protected virtual void Dispose(bool disposing) { }
}

그런 다음 중요한 리소스가있는 클래스를 추가하려면 :

public class ComFileCritical : BaseCriticalResource {

    private IntPtr nativeResource;

    protected override Dispose(bool disposing) {
        // free native resources if there are any.
        if (nativeResource != IntPtr.Zero) {
            ComCallToFreeUnmangedPointer(nativeResource);
            nativeResource = IntPtr.Zero;
        }
    }
}

그런 다음 사용하는 것은 다음과 같이 간단합니다.

using (ComFileCritical fileResource = new ComFileCritical()) {
    // Some actions on fileResource
}

// fileResource's critical resources freed at this point

IDisposable을 올바르게 구현하는 방법 도 참조하십시오 .


저는 C ++ 배경 출신이며 약 1 년 동안 C #으로 작업했습니다. 다른 많은 사람들과 마찬가지로 결정적 자원 관리가 언어에 내장되지 않은 이유에 대해 당황합니다.

using구문은 "결정적"리소스 관리를 제공하며 C # 언어로 빌드됩니다. "결정적"이란 블록이 실행을 시작한 Dispose후 코드가 호출되기 전에 호출되었음을 의미 using합니다. 이것은 "결정 론적"이라는 단어가 의미하는 바가 아니지만 모든 사람들이 그런 식으로 이런 맥락에서 그것을 남용하는 것처럼 보입니다.

내 C ++ 편향 뇌에서 결정적 소멸자와 함께 참조 계산 스마트 포인터를 사용하는 것은 IDisposable을 구현하고 dispose를 호출하여 비 메모리 리소스를 정리해야하는 가비지 수집기에서 중요한 단계 인 것 같습니다.

가비지 수집기는 구현할 필요가 없습니다 IDisposable. 실제로 GC는이를 완전히 인식하지 못합니다.

솔직히, 나는별로 똑똑하지 않다 ... 그래서 나는 이것이 왜 상황이 그런지 더 잘 이해하려는 욕망에서 이걸 묻고있다.

가비지 콜렉션 추적은 프로그래머가 수동 메모리 관리의 부담에서 벗어나 무한 메모리 시스템을 에뮬레이트하는 빠르고 안정적인 방법입니다. 이로 인해 여러 종류의 버그가 제거되었습니다 (달리는 포인터, 너무 빨리 해제 됨, 두 배 해제 됨, 해제하는 것을 잊음).

C #이 다음과 같이 수정되면 어떻게됩니까?

개체는 참조 횟수입니다. 개체의 참조 횟수가 0이되면 리소스 정리 메서드가 개체에 대해 결정적으로 호출됩니다.

두 스레드간에 공유되는 객체를 고려하십시오. 스레드는 참조 횟수를 0으로 줄이기 위해 경쟁합니다. 한 스레드가 경주에서 이기고 다른 스레드가 정리를 담당합니다. 그것은 비 결정적입니다. 참조 카운트가 본질적으로 결정적이라는 믿음은 신화입니다.

Another common myth is that reference counting frees objects at the earliest possible point in the program. It doesn't. Decrements are always deferred, usually to the end of scope. This keeps objects alive for longer than necessary leaving what is called "floating garbage" lying around. Note that, in particular, some tracing garbage collectors can and do recycle objects earlier than scope-based reference counting implementations.

then the object is marked for garbage collection. Garbage collection occurs at some non-deterministic time in the future at which point memory is reclaimed. In this scenario you don't have to implement IDisposable or remember to call Dispose.

You don't have to implement IDisposable for garbage collected objects anyway, so that is a non-benefit.

You just implement the resource cleanup function if you have non-memory resources to release.

Why is that a bad idea?

Naive reference counting is very slow and leaks cycles. For example, Boost's shared_ptr in C++ is up to 10x slower than OCaml's tracing GC. Even naive scope-based reference counting is non-deterministic in the presence of multithreaded programs (which is almost all modern programs).

Would that defeat the purpose of the garbage collector?

Not at all, no. In fact it is a bad idea that was invented in the 1960s and subjected to intense academic study for the next 54 years concluding that reference counting sucks in the general case.

Would it be feasible to implement such a thing?

Absolutely. Early prototype .NET and JVM used reference counting. They also found it sucked and dropped it in favor of tracing GC.

EDIT: From the comments so far, this is a bad idea because

GC is faster without reference counting

Yes. Note that you can make reference counting much faster by deferring counter increments and decrements but that sacrifices the determinism that you crave so very much and it is still slower than tracing GC with today's heap sizes. However, reference counting is asymptotically faster so at some point in the future when heaps get really big maybe we will start using RC in production automated memory management solutions.

problem of dealing with cycles in the object graph

Trial deletion is an algorithm specifically designed to detect and collect cycles in reference counted systems. However, it is slow and non-deterministic.

I think number one is valid, but number two is easy to deal with using weak references.

Calling weak references "easy" is a triumph of hope over reality. They are a nightmare. Not only are they unpredictable and difficult to architect but they pollute APIs.

So does the speed optimization outweigh the cons that you:

may not free a non-memory resource in a timely manner

Doesn't using free non-memory resource in a timely manner?

might free a non-memory resource too soon If your resource cleanup mechanism is deterministic and built-in to the language you can eliminate those possibilities.

The using construct is deterministic and built into the language.

I think the question you really want to ask is why doesn't IDisposable use reference counting. My response is anecdotal: I've been using garbage collected languages for 18 years and I have never needed to resort to reference counting. Consequently, I much prefer simpler APIs that aren't polluted with incidental complexity like weak references.


I know something about garbage collection. Here is a short summary because a full explanation is beyond the bounds of this question.

.NET uses a copying and compacting generational garbage collector. This is more advanced than reference counting and has the benefit of being able to collect objects that refer to themselves either directly, or through a chain.

Reference counting will not collect cycles. Reference counting also has a lower throughput (slower overall) but with the benefit of faster pauses (maximal pauses are smaller) than a tracing collector.


There's a lot of issues in play here. First of all you need to distinguish between freeing managed memory and clean-up of other resources. The former can be really fast whereas the later may be very slow. In .NET the two are separated, which allows for faster clean-up of managed memory. This also implies, that you should only implement Dispose/Finalizer when you have something beyond managed memory to clean up.

The .NET employs a mark and sweep technique where it traverses the heap looking for roots to objects. Rooted instances survive the garbage collection. Everything else can be cleaned by just reclaiming the memory. The GC has to compact memory every now and then, but apart from that reclaiming memory is a simple pointer operation even when reclaiming multiple instances. Compare this with multiple calls to destructors in C++.


The object implemeting IDisposable must also implement a finalizer called by the GC when the user doesn't explicit call Dispose - see IDisposable.Dispose at MSDN.

The whole point of IDisposable is that the GC is running at some non-deterministic time and you implement IDisposable because you hold a valuable resource and wants to free it at a deterministic time.

So your proposal would change nothing in terms of IDisposable.

Edit:

Sorry. Didn't read your proposal correctly. :-(

Wikipedia has a simple explanation of the shortcomings of References counted GC


Reference count

The costs of using reference counts are twofold: First, every object requires the special reference count field. Typically, this means an extra word of storage must be allocated in each object. Second, every time one reference is assigned to another, the reference counts must be adjusted. This increases significantly the time taken by assignment statements.

Garbage Collection in .NET

C# does not use reference counting of the objects. Instead it maintains a graph of the object references from the stack and navigates from the root to cover up all the referenced objects. All the referenced objects in the graph are compacted in the heap to that a contiguous memory is available for future objects. Memory for all the unreferenced objects who do not need to be finalized is reclaimed. Those that are unreferenced but have finalizers to be executed on them are moved to a separate queue called the f-reachable queue where the garbage collector calls their finalizers in the background.

In addition to the above GC uses the concept of generations for a more efficient garbage collection. It is based on the following concepts 1. It is faster to compact the memory for a portion of the managed heap than for the entire managed heap 2. Newer objects will have shorter lifetimes and older objects will have longer lifetimes 3. Newer objects tend to be related to each other and accessed by the application around the same time

The managed heap is divided into three generations: 0, 1, and 2. The new objects are stored in gen 0. Objects that are not reclaimed by a cycle of GC are promoted to the next gen. So if newer objects which are in gen 0 survive GC cycle 1, then they are promoted to gen 1. Those among these that survive GC cycle 2 are promoted to gen 2. Because the garbage collector supports only three generations, objects in generation 2 that survive a collection remain in generation 2 until they are determined to be unreachable in a future collection.

The garbage collector performs a collection when generation 0 is full and memory for new object needs to be allocated. If a collection of generation 0 does not reclaim enough memory, the garbage collector can perform a collection of generation 1, then generation 0. If this does not reclaim enough memory, the garbage collector can perform a collection of generations 2, 1, and 0.

Thus GC is more efficient than reference count.


Deterministic non-memory resource management is part of the language, however it is not done with destructors.

Your opinion is common among people coming from a C++ background, attempting to use the RAII design pattern. In C++ the only way you can guarrantee that some code will run in the end of a scope, even if an exeption is thrown, is to allocate an object on the stack and put the clean-up code in the destructor.

In other languages (C#, Java, Python, Ruby, Erlang, ...) you can use try-finally (or try-catch-finally) instead to ensure that the clean-up code will always run.

// Initialize some resource.
try {
    // Use the resource.
}
finally {
    // Clean-up.
    // This code will always run, whether there was an exception or not.
}

I C#, you can also use the using construct:

using (Foo foo = new Foo()) {
    // Do something with foo.
}
// foo.Dispose() will be called afterwards, even if there
// was an exception.

Thus, for a C++-programmer, it might help to think about "running clean-up code" and "freeing memory" as two separate things. Put your clean-up code in a finally block and leave to the GC to take care of the memory.

ReferenceURL : https://stackoverflow.com/questions/867114/why-no-reference-counting-garbage-collection-in-c

반응형