참조 계산 스마트 포인터가 왜 그렇게 인기가 있습니까?


52

보다시피, 스마트 포인터는 많은 실제 C ++ 프로젝트에서 광범위하게 사용됩니다.

어떤 종류의 스마트 포인터가 RAII 및 소유권 이전을 지원하는 데 분명히 유리하지만, "가비지 콜렉션" 의 방법 으로 공유 포인터 를 기본적 으로 사용하는 경향이 있으므로 프로그래머가 할당에 대해 많이 생각할 필요가 없습니다. .

Boehm GC 와 같은 적절한 가비지 수집기를 통합하는 것보다 공유 포인터가 더 인기있는 이유는 무엇 입니까? (또는 실제 GC보다 인기가 높다는 것에 전혀 동의하십니까?)

참조 계산에 비해 기존 GC의 두 가지 장점에 대해 알고 있습니다.

  • 기존의 GC 알고리즘은 참조주기에 문제 가 없습니다 .
  • 참조 횟수는 일반적으로 적절한 GC보다 느립니다 .

참조 카운팅 스마트 포인터를 사용하는 이유는 무엇입니까?


6
나는 이것이 사용하기에 잘못된 기본값이라는 의견을 추가합니다. 대부분의 경우 std::unique_ptr충분하고 런타임 성능 측면에서 원시 포인터보다 오버 헤드가 없습니다. std::shared_ptr어디에서나 사용 하면 소유권 의미론이 모호 해져 자동 리소스 관리 이외의 스마트 포인터의 주요 이점 중 하나를 잃게됩니다. 코드의 의도를 명확하게 이해하는 것입니다.
Matt

2
죄송하지만 여기에 허용되는 답변은 완전히 잘못되었습니다. 레퍼런스 카운팅은 오버 헤드가 더 높고 (마크 비트 대신 카운트가 느리고 런타임 성능이 느려짐) 눈사태가 감소 할 때 무제한 일시 정지 시간이 있으며 Cheney 반 공간과 같이 더 복잡하지 않습니다.
Jon Harrop

답변:


57

가비지 수집에 대한 참조 계산의 몇 가지 장점 :

  1. 오버 헤드가 낮습니다. 가비지 수집기는 가독성이 높을 수 있습니다 (예 : 가비지 수집주기가 진행되는 동안 프로그램이 예상치 못한 시간에 정지 될 수 있음).

  2. 더 예측 가능한 행동. 참조 횟수를 사용하면 객체에 대한 마지막 참조가 사라지면 즉시 객체가 해제됩니다. 반면에 가비지 콜렉션을 사용하면 시스템이 오브젝트에 접근 할 때 오브젝트가 "언제나"해제됩니다. RAM의 경우 일반적으로 데스크톱이나로드가 적은 서버에서는 큰 문제가 아니지만 다른 리소스 (예 : 파일 핸들)의 경우 나중에 잠재적 충돌을 피하기 위해 최대한 빨리 닫혀 야합니다.

  3. 더 간단합니다. 기준 카운팅은 몇 분 안에 설명 될 수 있으며 1 ~ 2 시간 안에 구현 될 수 있습니다. 가비지 수집기, 특히 성능이 우수한 가비지 수집기는 매우 복잡하며 많은 사람들이 이해하지 못합니다.

  4. 표준. C ++에는 STL의 참조 계산 (shared_ptr을 통한) 및 친구가 포함되어 있습니다. 즉, 대부분의 C ++ 프로그래머가 익숙하고 대부분의 C ++ 코드가 작동합니다. 그러나 표준 C ++ 가비지 수집기가 없으므로 하나를 선택해야하고 사용 사례에 잘 작동하기를 희망합니다. 그렇지 않으면 언어가 아니라 수정하는 것이 문제입니다.

참조 카운트의 단점은 사이클을 감지하지 못하는 것이 문제이지만, 지난 10 년 동안 참조 카운트를 사용한 적이 없었습니다. 대부분의 데이터 구조는 자연스럽게 비 주기적이며, 순환 참조가 필요한 상황 (예 : 트리 노드의 부모 포인터)이 발생하면 "역방향"에 weak_ptr 또는 원시 C 포인터를 사용할 수 있습니다. 데이터 구조를 설계 할 때 발생할 수있는 문제를 알고 있다면 문제가되지 않습니다.

성능에 관해서는 참조 계산 성능에 문제가 없었습니다. 가비지 수집 성능, 특히 GC에서 발생할 수있는 임의의 동결 문제 ( "객체를 할당하지 않음")가 "GC를 사용하지 않음"으로 표시 될 수있는 문제가 발생했습니다. .


16
순진한 참조 계산 구현은 일반적으로 대기 시간을 희생하여 프로덕션 GC (30-40 %)보다 처리량이 훨씬 낮습니다. 리퍼 카운트에 더 적은 비트를 사용하고 탈출 할 때까지 추적 객체를 피하는 등의 최적화로 차이를 막을 수 있습니다. C ++은 주로 make_shared돌아올 때 자연스럽게 지속 됩니다. 그러나 실시간 애플리케이션에서는 지연 시간이 더 큰 문제인 경향이 있지만 처리량이 더 중요하므로 GC 추적이 널리 사용됩니다. 나는 그들에 대해 나쁜 말을 너무 빨리하지 않을 것입니다.
Jon Purdy

3
간단한의 측면에서 : 나는 '단순'애매한 것 필요한 구현의 총량 코드에 대한 예,하지만 간단하게 사용하는 사람이 RC를 사용하는 방법 (이야기 비교 '수행은 개체를 만들 때 그들을 파괴 할 때' ) GC ( '...')를 사용하는 방법에 대해
AakashM

4
"레퍼런스 카운팅을 사용하면 객체에 대한 마지막 레퍼런스가 사라지면 즉시 객체가 해제됩니다." 그것은 일반적인 오해입니다. flyingfrogblog.blogspot.co.uk/2013/10/…
Jon Harrop

4
@ JonHarrop : 그 블로그 포스트는 끔찍하게 잘못되었습니다. 또한 모든 주석, 특히 마지막 주석을 읽어야 합니다 .
중복 제거기

3
@ JonHarrop : 예, 있습니다. 그는 평생이 종말에 도달하는 전체 범위라는 것을 이해하지 못합니다. 그리고 주석에 따르면 때로는 작동하는 F #의 최적화는 변수가 다시 사용되지 않으면 수명이 일찍 끝나는 것입니다. 자연스럽게 자체 위험이 있습니다.
중복 제거기

26

GC에서 우수한 성능을 얻으려면 GC가 메모리에서 객체를 이동할 수 있어야합니다. 메모리 위치와 직접 상호 작용할 수있는 C ++과 같은 언어에서는 거의 불가능합니다. (Microsoft C ++ / CLR은 GC 관리 포인터에 대한 새로운 구문을 도입하므로 사실상 다른 언어이므로 계산에 포함되지 않습니다.)

Boehm GC는 좋은 아이디어이지만 실제로 두 세계에서 최악입니다. 좋은 GC보다 느린 malloc ()이 필요하므로 세대 GC의 성능 향상없이 ​​결정적 할당 / 할당 해제 동작을 잃습니다. . 또한 그것은 필연적으로 보수적이므로 반드시 모든 쓰레기를 모을 필요는 없습니다.

훌륭하고 잘 조정 된 GC는 좋은 일이 될 수 있습니다. 그러나 C ++과 같은 언어에서는 이득이 최소이며 비용은 종종 가치가 없습니다.

그러나 람다와 캡처 의미가 C ++ 커뮤니티를 Lisp 커뮤니티가 처음으로 GC를 발명하게 한 동일한 종류의 할당 및 객체 수명 문제로 C ++ 커뮤니티를 이끌 기 시작하는지 여부는 C ++ 11이 더 대중화됨에 따라 흥미로울 것입니다. 장소.

또한 StackOverflow에서 관련 질문에 대한 답변을 참조하십시오 .


6
Boehm GC에 대해, 나는 C와 C ++ 프로그래머들 사이에서 일반적인 기술에 대한 나쁜 첫인상을 제공함으로써 GC에 대한 전통적인 혐오감을 얼마나 개인적 으로 책임 지는지 가끔 궁금해했다 .
Leushenko 2019

@Leushenko 잘 말했다. Boehm gc를 "적절한"gc라고하는이 질문이 그 예입니다. 느리고 실제로 누출이 보장된다는 사실을 무시합니다. 누군가가 shared_ptr에 파이썬 스타일 사이클 브레이커를 구현했는지 여부를 조사 하면서이 질문을 찾았습니다 .c ++ 구현에 대한 가치있는 목표처럼 들립니다.
user4815162342

4

보다시피, 스마트 포인터는 많은 실제 C ++ 프로젝트에서 광범위하게 사용됩니다.

사실, 객관적으로 대다수의 코드는 현재 가비지 수집기를 추적하여 현대 언어로 작성되었습니다.

어떤 종류의 스마트 포인터가 RAII 및 소유권 이전을 지원하는 데 분명히 도움이 되더라도, "가비지 수집"의 방법으로 공유 포인터를 사용하는 경향이 있으므로 프로그래머가 할당에 대해 많이 생각할 필요가 없습니다. .

사이클에 대해 여전히 걱정할 필요가 있기 때문에 나쁜 생각입니다.

Boehm GC와 같은 적절한 가비지 수집기를 통합하는 것보다 공유 포인터가 더 인기있는 이유는 무엇입니까? (또는 실제 GC보다 인기가 높다는 것에 전혀 동의하십니까?)

오, 와우, 당신의 사고 방식에는 많은 문제가 있습니다.

  1. Boehm의 GC는 어떤 의미에서든 "적절한"GC가 아닙니다. 정말 끔찍합니다. 보수적이므로 누출되어 설계 상 비효율적입니다. 참조 : http://flyingfrogblog.blogspot.co.uk/search/label/boehm

  2. 공유 포인터는 객관적으로 GC만큼 인기가 거의 없습니다. 대다수 개발자가 현재 GC 언어를 사용하고 공유 포인터가 필요하지 않기 때문입니다. C ++에 비해 구직 시장에서 Java와 Javascript를 살펴보십시오.

  3. GC가 접선 문제라고 생각하기 때문에 고려 사항을 C ++로 제한하는 것 같습니다. 선택 편견을 도입 하는 것은 아닙니다 ( 좋은 GC를 얻는 유일한 방법은 처음부터 언어와 VM을 디자인하는 것입니다). 적절한 가비지 수집을 원하는 사람들은 C ++을 고수하지 않습니다.

참조 카운팅 스마트 포인터를 사용하는 이유는 무엇입니까?

C ++로 제한되어 있지만 자동 메모리 관리를 원합니다.


7
음, C ++ 기능에 대해 말하는 c ++ 태그가 붙은 질문 입니다. 분명히, 모든 일반적인 진술은 프로그래밍 전체가 아니라 C ++ 코드 내에서 이야기하고 있습니다 . 그러나 "객관적으로"가비지 콜렉션은 C ++ 세계 외부에서 사용 중일 수 있으며, 이는 현재 당면한 문제와 관련이 없습니다.
Nicol Bolas

2
마지막 줄은 특허 적으로 잘못되었습니다 : 당신은 C ++에 있고 GC를 다루지 않아도되어 리소스 확보가 지연되어 기쁩니다. Apple이 GC를 좋아하지 않는 이유가 있으며 GC 언어에 대한 가장 중요한 지침은 다음과 같습니다.
중복 제거기

3
@JonHarrop : 따라서 GC의 유무에 관계없이 동등한 작은 프로그램을 비교하십시오. 어느쪽에 더 많은 메모리가 필요합니까?
중복 제거기

1
@ 중복 제거기 : 결과를 제공하는 프로그램을 구상 할 수 있습니다. 프로그램이 보육원 (예 : 목록 대기열)에서 살아남을 때까지 힙 할당 메모리를 유지하도록 설계된 경우 참조 카운트는 GC 추적 성능을 능가합니다. 가비지 수집을 추적하면 작은 개체가 많고 수명이 짧지 만 정적으로 잘 알려지지 않은 순수한 기능의 데이터 구조를 사용하는 경우 범위 기반 참조 카운트보다 적은 메모리가 필요합니다.
Jon Harrop

3
@ JonHarrop : C ++을 사용한다면 GC (추적 등)와 RAII를 의미했습니다. 여기에는 참조 계산이 포함되지만 유용한 경우에만 포함됩니다. 또는 Swift 프로그램과 비교할 수 있습니다.
중복 제거기

3

MacOS X 및 iOS 및 Objective-C 또는 Swift를 사용하는 개발자의 경우 참조 계산이 자동으로 처리되므로 인기가 높아지고 Apple에서 더 이상 지원하지 않기 때문에 가비지 수집 사용이 상당히 줄었습니다 ( 가비지 수집은 다음 MacOS X 버전에서 중단되며 가비지 수집은 iOS에서 구현되지 않았습니다). 실제로 가비지 수집을 사용할 수있는 소프트웨어가 많았다는 사실을 진지하게 의심합니다.

가비지 수집을 제거하는 이유 : 가비지 수집기에서 액세스 할 수없는 영역으로 포인터를 "이스케이프"할 수있는 C 스타일 환경에서는 안정적으로 작동하지 않았습니다. 애플은 레퍼런스 카운팅이 더 빠르다고 강력하게 믿고 믿었다. (여기서 상대 속도에 대한 주장을 할 수는 있지만 아무도 Apple을 설득 할 수 없었습니다). 결국 아무도 가비지 수집을 사용하지 않았습니다.

MacOS X 또는 iOS 개발자가 가장 먼저 배우는 것은 참조주기를 처리하는 방법이므로 실제 개발자에게는 문제가되지 않습니다.


내가 이해하는 방식은 C와 같은 환경이 아니라 결정을 내리는 것이 아니라 GC가 결정적이며 수용 가능한 성능을 얻으려면 훨씬 더 많은 메모리가 필요하다는 것입니다.
중복 제거기

가비지 컬렉터는 (충돌로 이어지는) 난 아직도 사용 된 오브젝트를 파괴 왜 디버깅을 결정 나를 위해 :-)
gnasher729

아, 그래요. 결국 그 이유를 찾았습니까?
중복 제거기

그렇습니다. 이것은 void *를 "컨텍스트"로 전달한 콜백 함수에서 사용자에게 돌려주는 많은 유닉스 함수 중 하나였습니다. void *는 실제로 Objective-C 객체였으며 가비지 수집기는 Unix 호출에서 객체가 숨겨져 있다는 것을 인식하지 못했습니다. 콜백이 호출되고 void *를 Object *에 캐스팅합니다.
gnasher729

2

C ++에서 가비지 수집의 가장 큰 단점은 올바르게 얻을 수 없다는 것입니다.

  • C ++에서 포인터는 자체 벽이있는 커뮤니티에 존재하지 않으며 다른 데이터와 혼합되어 있습니다. 따라서 유효한 포인터로 해석 할 수있는 비트 패턴이있는 다른 데이터와 포인터를 구별 할 수 없습니다.

    결과 : 모든 C ++ 가비지 콜렉터는 수집해야하는 오브젝트를 누출시킵니다.

  • C ++에서는 포인터 산술을 수행하여 포인터를 파생시킬 수 있습니다. 따라서 블록 시작에 대한 포인터를 찾지 못한다고해서 해당 블록을 참조 할 수 없다는 의미는 아닙니다.

    결과 : 모든 C ++ 가비지 콜렉터는 이러한 조정을 고려하여 블록의 끝 바로 다음을 포함하여 블록 내 어디에서나 발생하는 모든 비트 시퀀스를 블록을 참조하는 유효한 포인터로 처리해야합니다.

    참고 : C ++ 가비지 수집기는 다음과 같은 트릭을 사용하여 코드를 처리 할 수 ​​없습니다.

    int* array = new int[7];
    array--;    //undefined behavior, but people may be tempted anyway...
    for(int i = 1; i <= 7; i++) array[i] = i;

    사실 이것은 정의되지 않은 동작을 호출합니다. 그러나 기존 코드 중 일부는 코드보다 영리하며 가비지 수집기에서 예비 할당 해제를 트리거 할 수 있습니다.


2
" 다른 데이터와 혼합되어 있습니다. "다른 데이터와 "혼합"될 정도로 많지는 않습니다. C ++ 유형 시스템을 사용하면 포인터가 무엇이고 무엇이 아닌지를 쉽게 알 수 있습니다. 문제는 포인터가 자주 있다는 것입니다 다른 데이터. 정수에 포인터를 숨기는 것은 많은 C 스타일 API에서 불행히도 일반적인 도구입니다.
Nicol Bolas

1
C ++에서 가비지 수집기를 망치기 위해 정의되지 않은 동작이 필요하지 않습니다. 예를 들어, 파일에 대한 포인터를 직렬화하여 나중에 읽을 수 있습니다. 한편, 프로세스는 주소 공간의 어느 곳에도 포인터를 포함하지 않을 수 있으므로 가비지 수집기가 해당 객체를 수집 한 다음 포인터를 직렬화 해제 할 때 ... 으악.
Bwmat

@Bwmat "짝수"? 그런 파일에 대한 포인터를 작성하는 것은 조금 ... 아주 많이 가져온 것 같습니다. 어쨌든, 동일한 심각한 문제는 객체를 스택하기위한 포인터를 괴롭 힙니다. 코드의 다른 곳에서 포인터를 읽을 때 포인터가 사라질 수 있습니다! 유효하지 않은 포인터 값을 역 직렬화하는 것은 정의되지 않은 동작입니다.
hyde

물론 그런 일을하고 있다면 조심해야합니다. 일반적으로 가비지 컬렉터는 모든 경우 C ++에서 (언어를 변경하지 않고) '올바로'작동 할 수 없습니다.
Bwmat

1
@ gnasher729 : 흠, 아니? 과거 엔드 포인터가 완벽합니까?
중복 제거기
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.