왜 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.