왜 std :: move std :: shared_ptr을 사용합니까?


148

Clang 소스 코드를 살펴본 결과이 스 니펫을 발견했습니다.

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = std::move(Value);
}

왜 내가 원하는 것 ?std::movestd::shared_ptr

공유 리소스에 대한 소유권을 이전 할 수 있습니까?

왜 대신 대신이 작업을 수행하지 않습니까?

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = Value;
}

답변:


137

다른 답변이 충분히 강조하지 않은 것은 속도 의 포인트라고 생각합니다 .

std::shared_ptr참조 카운트는 atomic 입니다. 기준 카운트를 늘리거나 줄이려면 원자 적 증가 또는 감소 가 필요합니다 . 이것은 비원 자적 증가 / 감소 보다 백 배 느리지 만 , 동일한 카운터를 증가시키고 감소 시키면 정확한 수로 감겨 프로세스에 많은 시간과 자원을 낭비한다는 것은 말할 것도 없습니다.

shared_ptr복사 하는 대신 이동하여 원자 참조 카운트를 "훔치고" 다른 것을 무효화합니다 shared_ptr. "스틸 링"참조 횟수는 원자 적이 지 않으며 복사하는 것 shared_ptr(및 원자 참조 증분 또는 감소를 일으키는)을 복사하는 것보다 백 배 빠릅니다 .

이 기술은 최적화를 위해 순수하게 사용됩니다. 복사 (제안한대로)는 기능면에서도 훌륭합니다.


5
실제로 100 배 더 빠릅니까? 이에 대한 벤치 마크가 있습니까?
— xaviersjs

1
@xaviersjs 값이 범위를 벗어나면 할당에 원자 단위 증가와 원자 단위 감소가 필요합니다. 원자 연산에는 수백 번의 클럭 사이클이 걸릴 수 있습니다. 예, 실제로는 훨씬 느립니다.
— Adisak

2
@Adisak는 페치 및 추가 작업 ( en.wikipedia.org/wiki/Fetch-and-add )이 기본 증분보다 수백 사이클 이상 걸릴 수 있다고 들었습니다 . 그것에 대한 참조가 있습니까?
— xaviersjs

2
@xaviersjs : stackoverflow.com/a/16132551/4238087 레지스터 작업이 몇 사이클이면서 원자에 대한 100 사이클 (100-300) 사이클이 법안에 맞습니다. 메트릭은 2013 년이지만, 여전히 다중 소켓 NUMA 시스템의 경우에 해당됩니다.
— russianfool

1
때로는 코드에 스레딩이 없다고 생각하지만 ... 어떤 도서관이 와서 u를 위해 그것을 파괴합니다. const 참조와 std :: move ...를 사용하는 것이 더 좋습니다. 포인터 참조 카운트에 의존하는 것보다 명확하고 분명한 경우 ....
— Erik Aronesty

123

사용 move하면 주식 수를 늘리고 바로 줄이지 않습니다. 그러면 사용 횟수에 대한 고가의 원자 작업이 절약 될 수 있습니다.


1
조기 최적화가 아닙니까?
— YSC

11
@YSC 누군가가 실제로 그것을 시험해 본다면 그렇지 않습니다.
— OrangeDog

19
@YSC 코드를 읽거나 유지하기가 어려워지면 조기 최적화가 나쁘다. 이것은 적어도 IMO를 수행하지 않습니다.
— Angew는 더 이상 SO를 자랑스럽게 생각하지 않습니다.

17
과연. 이것은 조기 최적화가 아닙니다. 대신이 함수를 작성하는 것이 현명한 방법입니다.
— 궤도에서 가벼움 레이스

60

이동 조작 (이동 생성자와 같은) std::shared_ptr은 기본적으로 "스틸 링 포인터" (소스에서 대상으로,보다 정확하게 말하면 전체 상태 제어 블록은 참조 카운트 정보를 포함하여 소스에서 대상으로 "도난")이므로 저렴 합니다. .

대신 원자 참조 카운트 증가 (즉 , 정수 데이터 멤버뿐만 아니라 Windows 호출)의 복사 작업 은 포인터 / 상태를 훔치는 것 보다 비용 이 많이 듭니다 .std::shared_ptr++RefCountRefCountInterlockedIncrement

따라서이 경우의 참조 횟수 역학을 자세히 분석하십시오.

// shared_ptr<CompilerInvocation> sp;
compilerInstance.setInvocation(sp);

값을 전달한 sp다음 메소드 내에서 사본 을 가져 오면 다음을 수행 CompilerInstance::setInvocation할 수 있습니다.

  1. 메소드를 입력 할 때 shared_ptr매개 변수는 복사 구성됩니다. ref count atomic incremental .
  2. 메소드 본문 내에서 매개 변수를 데이터 멤버에 복사 합니다 shared_ptr. ref count atomic incremental .
  3. 메소드를 종료하면 shared_ptr매개 변수가 삭제됩니다 (ref count atomic decrement) .

총 3 개의 원자 연산에 대해 2 개의 원자 증분과 1 개의 원자 감소가 있습니다.

대신 Clang의 코드에서 올바르게 수행 된 것처럼 shared_ptr값으로 매개 변수 를 전달한 다음 std::move메서드 내부에 매개 변수 를 전달하면

  1. 메소드를 입력 할 때 shared_ptr매개 변수는 복사 구성됩니다. ref count atomic incremental .
  2. 메소드 본문 내에서 데이터 멤버에 std::move대한 shared_ptr매개 변수 : 참조 횟수는 변경 되지 않습니다 ! 당신은 포인터 / 상태를 훔치고 있습니다 : 비싼 원자 참조 횟수 작업이 필요하지 않습니다.
  3. 메소드를 종료하면 shared_ptr매개 변수가 삭제됩니다. 그러나 2 단계에서 이동 했으므로 shared_ptr매개 변수가 더 이상 아무것도 가리 키지 않으므로 폐기 할 것이 없습니다. 이 경우에도 원자 감소는 발생하지 않습니다.

결론 :이 경우 하나의 심판 카운트 원자 증분, 즉 하나의 원자 연산 을 얻습니다 .
보시다시피, 이것은 복사 사례에 대해 두 개의 원자 단위 증가와 하나의 원자 감소 (총 세 개의 원자 연산) 보다 훨씬 낫 습니다 .


1
또한 주목할 가치가 있습니다. 왜 const 참조로 전달하고 전체 std :: move 항목을 피하지 않습니까? 값별 전달을 사용하면 원시 포인터를 직접 전달할 수 있으며 하나의 shared_ptr 만 작성됩니다.
— 조셉 아일랜드

@JosephIreland const 참조를 이동할 수 없기 때문에
— Bruno Ferreira

2
@JosephIreland 당신이 그것을 호출하면 증가compilerInstance.setInvocation(std::move(sp)); 가 없기 때문에 . 필요한 오버로드를 추가하여 동일한 동작을 얻을 수 있지만 필요하지 않은 경우 중복되는 이유는 무엇입니까? shared_ptr<>&&
— ratchet freak

2
@BrunoFerreira 나는 내 자신의 질문에 대답하고있었습니다. 참조이므로 이동하지 않아도됩니다. 복사하기 만하면됩니다. 두 개가 아닌 여전히 하나의 사본 만 있습니다. 그들이하지 않는 이유는 예를 들어 from setInvocation(new CompilerInvocation)또는 ratchet이 언급 한 것처럼 새로 생성 된 shared_ptr을 불필요하게 복사하기 때문입니다 setInvocation(std::move(sp)). 첫 번째 의견이 불분명 한 경우 죄송합니다. 글쓰기를 마치기 전에 실수로 실수로 게시 한 후 그대로두기로 결정했습니다.
— Joseph Ireland

22

를 복사하면 shared_ptr내부 상태 객체 포인터를 복사하고 참조 횟수를 변경합니다. 그것을 이동하는 것은 포인터를 내부 참조 카운터와 소유 한 객체로 바꾸는 것만 포함하므로 더 빠릅니다.


16

이 상황에서 std :: move를 사용하는 데는 두 가지 이유가 있습니다. 대부분의 응답은 속도 문제를 해결했지만 코드의 의도를보다 명확하게 보여주는 중요한 문제는 무시했습니다.

std :: shared_ptr의 경우 std :: move는 포인트의 소유권 이전을 분명하게 나타내며 간단한 복사 작업은 추가 소유자를 추가합니다. 물론, 원래 소유자가 그 후에 소유권을 포기하면 (예 : std :: shared_ptr을 파기 할 수있게하여) 소유권 이전이 완료된 것입니다.

std :: move로 소유권을 이전하면 무슨 일이 일어나고 있는지 분명합니다. 일반 사본을 사용하는 경우 원래 소유자가 즉시 소유권을 양도한다는 것을 확인할 때까지 의도 한 작업이 이전인지 확실하지 않습니다. 보너스로, 소유권의 원자 이전은 소유자 수가 1 증가한 임시 상태를 피할 수 있기 때문에보다 효율적인 구현이 가능합니다.


정확히 내가 찾고있는 것. 다른 답변 이이 중요한 의미 론적 차이를 무시하는 방법에 놀랐습니다. 스마트 포인터는 모두 소유권에 관한 것입니다.
— qweruiop

당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.