C ++의 일반 포인터와 비교하여 스마트 포인터의 오버 헤드는 얼마입니까?


101

C ++ 11의 일반 포인터에 비해 스마트 포인터의 오버 헤드는 얼마입니까? 즉, 스마트 포인터를 사용하면 코드가 느려질까요? 그렇다면 얼마나 느려질까요?

특히 C ++ 11 std::shared_ptrstd::unique_ptr.

분명히 스택 아래로 밀려 난 물건은 더 커질 것입니다 (적어도 나는 그렇게 생각합니다). 내 성능에 영향을 미치나요?

예를 들어 일반 포인터 대신 함수에서 스마트 포인터를 반환합니다.

std::shared_ptr<const Value> getValue();
// versus
const Value *getValue();

또는 예를 들어 내 함수 중 하나가 일반 포인터 대신 스마트 포인터를 매개 변수로 허용하는 경우 :

void setValue(std::shared_ptr<const Value> val);
// versus
void setValue(const Value *val);

8
알 수있는 유일한 방법은 코드를 벤치마킹하는 것입니다.
실레 Starynkevitch

당신은 무엇을 의미합니까? std::unique_ptr또는 std::shared_ptr?
스테판

10
대답은 42입니다. (또 다른 말로, 코드를 프로파일 링하고 일반적인 작업 부하에 대해 하드웨어를 이해해야합니다.)
Nim

애플리케이션이 중요하려면 스마트 포인터를 최대한 사용해야합니다.
user2672165

간단한 setter 함수에서 shared_ptr을 사용하는 비용은 끔찍하며 다중 100 % 오버 헤드를 추가합니다.
Lothar

답변:


176

std::unique_ptr 사소하지 않은 삭제자를 제공하는 경우에만 메모리 오버 헤드가 있습니다.

std::shared_ptr 매우 작지만 항상 참조 카운터에 대한 메모리 오버 헤드가 있습니다.

std::unique_ptr 생성자 동안 (제공된 삭제자를 복사하고 / 또는 포인터를 null로 초기화해야하는 경우) 및 소멸자 동안 (소유 된 개체를 제거하기 위해)에만 시간 오버 헤드가 있습니다.

std::shared_ptr생성자 (참조 카운터 생성), 소멸자 (참조 카운터 감소 및 가능하면 객체 파괴) 및 할당 연산자 (참조 카운터 증가)에 시간 오버 헤드가 있습니다. 의 스레드 안전성 보장으로 인해 std::shared_ptr이러한 증가 / 감소는 원자 적이므로 약간의 오버 헤드가 추가됩니다.

이들 중 어느 것도 역 참조 (소유 된 객체에 대한 참조를 얻는 데)에 시간 오버 헤드가없는 반면,이 작업은 포인터에 대해 가장 일반적인 것으로 보입니다.

요약하면 약간의 오버 헤드가 있지만 스마트 포인터를 지속적으로 생성하고 파괴하지 않는 한 코드가 느려지지 않아야합니다.


11
unique_ptr소멸자에 오버 헤드가 없습니다. 원시 포인터를 사용할 때와 똑같습니다.
R. Martinho Fernandes 2014

6
@ R.MartinhoFernandes는 원시 포인터 자체와 비교하여 원시 포인터 소멸자가 아무것도하지 않기 때문에 소멸자에 시간 오버 헤드가 있습니다. 원시 포인터가 사용되는 방식과 비교할 때 확실히 오버 헤드가 없습니다.
lisyarus

3
shared_ptr 구성 / 파괴 / 할당 비용의 일부가 스레드 안전성 때문이라는 점에 주목할 가치가 있습니다.
Joe

1
또한의 기본 생성자는 std::unique_ptr어떻습니까? 를 생성 std::unique_ptr<int>하면 내부 int*nullptr원하는지 여부에 따라 초기화됩니다 .
Martin Drozdik

1
@MartinDrozdik 대부분의 상황에서 나중에 null인지 확인하기 위해 원시 포인터도 null로 초기화합니다. 그럼에도 불구하고 이것을 답변에 추가했습니다. 감사합니다.
lisyarus

26

모든 코드 성능과 마찬가지로 하드 정보를 얻을 수있는 유일한 방법은 기계 코드 를 측정 및 / 또는 검사하는 것 입니다.

즉, 간단한 추론은

  • 디버그 빌드에서 약간의 오버 헤드를 예상 할 수 있습니다. 예를 들어 operator->함수 호출로 실행해야 단계에 들어갈 수 있기 때문입니다 (이는 클래스 및 함수를 비디 버그로 표시하는 데 대한 일반적인 지원이 없기 때문입니다).

  • 들어 shared_ptr그 제어 블록의 동적 할당을 포함, 동적 할당이 매우 느린 ++ C의 다른 기본 동작보다 이후 당신은 (사용을 초기 생성에 약간의 오버 헤드를 기대할 수 있습니다 make_shared때 실질적으로 가능한 한 그 오버 헤드를 최소화하기 위해).

  • 또한 shared_ptr예를 들어 shared_ptrby 값을 전달할 때 참조 횟수를 유지하는 데 약간의 오버 헤드가 있지만에 대한 오버 헤드는 없습니다 unique_ptr.

위의 첫 번째 사항을 염두에두고 측정 할 때 디버그 및 릴리스 빌드 모두에 대해 수행하십시오.

국제 C ++ 표준화위원회는 발표했다 성능에 대한 기술 보고서를 하기 전에, 그러나 이것은 2006 년이었다 unique_ptrshared_ptr표준 라이브러리에 추가되었습니다. 그럼에도 불구하고 스마트 포인터는 그 시점에서 오래된 모자 였으므로 보고서는 그것을 고려했습니다. 관련 부분 인용 :

“사소한 스마트 포인터를 통해 값에 액세스하는 것이 일반 포인터를 통해 값에 액세스하는 것보다 훨씬 느리다면 컴파일러는 추상화를 비효율적으로 처리합니다. 과거에는 대부분의 컴파일러에 상당한 추상화 패널티가 있었지만 현재의 여러 컴파일러는 여전히 그렇습니다. 그러나 적어도 2 개의 컴파일러가 1 % 미만의 추상화 패널티와 3 %의 패널티가있는 것으로보고되었으므로 이러한 종류의 오버 헤드를 제거하는 것은 최신 기술 내에 있습니다. "

정보에 입각 한 추측에 따르면, 2014 년 초 현재 가장 인기있는 컴파일러를 통해 "최신 기술 수준"을 달성했습니다.


내 질문에 추가 한 사례에 대한 자세한 내용을 답변에 포함 해 주시겠습니까?
Venemo 2014 년

이것은 10 년 이상 전에 사실 이었을지 모르지만 오늘날 기계 코드 검사는 위의 사람이 제안한 것만 큼 유용하지 않습니다. 명령어가 파이프 라인, 벡터화되는 방식, 그리고 컴파일러 / 프로세서가 궁극적으로 추측을 처리하는 방식에 따라 속도가 달라집니다. 코드 기계어 코드가 적다고해서 반드시 빠른 코드는 아닙니다. 성능을 결정하는 유일한 방법은 프로파일 링하는 것입니다. 이는 프로세서 및 컴파일러별로 변경 될 수 있습니다.
Byron

내가 본 문제는 일단 shared_ptrs가 서버에서 사용되면 shared_ptrs의 사용이 급증하기 시작하고 곧 shared_ptrs가 기본 메모리 관리 기술이된다는 것입니다. 그래서 이제 당신은 1-3 % 추상화 페널티를 반복해서 반복했습니다.
Nathan Doromal 19

디버그 빌드를 벤치마킹하는 것은 완전하고 시간 낭비라고 생각합니다
Paul Childs

26

내 대답은 다른 사람들과 다르며 그들이 코드를 프로파일 링했는지 정말로 궁금합니다.

shared_ptr은 제어 블록에 대한 메모리 할당 (참조 카운터와 모든 약한 참조에 대한 포인터 목록을 유지함) 때문에 생성에 상당한 오버 헤드가 있습니다. 또한 std :: shared_ptr이 항상 2 개의 포인터 튜플 (하나는 객체, 하나는 제어 블록)이기 때문에 엄청난 메모리 오버 헤드가 있습니다.

shared_pointer를 값 매개 변수로 함수에 전달하면 일반 호출보다 10 배 이상 느려지고 스택 해제를 위해 코드 세그먼트에 많은 코드가 생성됩니다. 참조로 전달하면 성능 측면에서 훨씬 더 나빠질 수있는 추가 간접 정보를 얻을 수 있습니다.

따라서 기능이 실제로 소유권 관리에 관여하지 않는 한 이렇게하면 안됩니다. 그렇지 않으면 "shared_ptr.get ()"을 사용하십시오. 정상적인 함수 호출 중에 객체가 죽지 않도록 설계되지 않았습니다.

화가 나서 컴파일러의 추상 구문 트리와 같은 작은 개체 또는 다른 그래프 구조의 작은 노드에서 shared_ptr을 사용하면 엄청난 성능 저하와 엄청난 메모리 증가를 볼 수 있습니다. C ++ 14가 시장에 출시 된 직후 프로그래머가 스마트 포인터를 올바르게 사용하는 방법을 배우기 전에 재 작성된 파서 시스템을 보았습니다. 재 작성은 이전 코드보다 훨씬 느 렸습니다.

은색 총알이 아니며 원시 포인터도 정의상 나쁘지 않습니다. 나쁜 프로그래머는 나쁘고 나쁜 디자인은 나쁘다. 주의 깊게 디자인하고 명확한 소유권을 염두에두고 디자인하고 주로 서브 시스템 API 경계에서 shared_ptr을 사용하십시오.

자세한 내용을 보려면 Nicolai M. Josuttis의 "C ++에서 공유 포인터의 실제 가격"에 대한 좋은 이야기를 볼 수 있습니다. https://vimeo.com/131189627
쓰기 장벽, 원자성에 대한 구현 세부 사항 및 CPU 아키텍처에 대해 자세히 설명합니다. 잠금 등. 일단 듣고 나면이 기능이 저렴하다고 결코 말하지 않을 것입니다. 더 느린 규모의 증명을 원한다면 처음 48 분을 건너 뛰고 어디서나 공유 포인터를 사용할 때 최대 180 배 더 느리게 실행되는 (-O3로 컴파일 된) 예제 코드를 실행하는 것을 지켜보십시오.


답변 해 주셔서 감사합니다! 어떤 플랫폼에서 프로파일 링했습니까? 일부 데이터로 클레임을 백업 할 수 있습니까?
Venemo

표시 할 번호가 없지만 Nico Josuttis 토크에서 일부를 찾을 수 있습니다. vimeo.com/131189627
Lothar

6
들어 본 적이 std::make_shared()있습니까? 또한, 나는 ... 나쁜 비트 지루한되는 뻔뻔스러운 오용의 데모를 찾을 수
Deduplicator

2
"make_shared"가 할 수있는 모든 것은 하나의 추가 할당으로부터 안전하고 제어 블록이 객체 앞에 할당 된 경우 더 많은 캐시 지역성을 제공하는 것입니다. 포인터를 넘겨도 전혀 도움이되지 않습니다. 이것은 문제의 근원이 아닙니다.
Lothar

14

즉, 스마트 포인터를 사용하면 코드가 느려질까요? 그렇다면 얼마나 느려질까요?

천천히? shared_ptrs를 사용하여 거대한 인덱스를 생성하고 컴퓨터가 구겨지기 시작할 때까지 메모리가 충분하지 않은 경우가 아니면 멀리서 견딜 수없는 힘에 의해 땅에 쓰러지는 노부인처럼 말입니다.

코드를 느리게 만드는 것은 느린 검색, 불필요한 루프 처리, 방대한 데이터 복사본 및 디스크에 대한 많은 쓰기 작업 (예 : 수백 개)입니다.

스마트 포인터의 장점은 모두 관리와 관련이 있습니다. 그러나 오버 헤드가 필요합니까? 이것은 구현에 따라 다릅니다. 3 개의 위상 배열을 반복한다고 가정 해 보겠습니다. 각 위상에는 1024 개의 요소 배열이 있습니다. smart_ptr반복이 완료되면 삭제해야한다는 것을 알게되므로이 프로세스 에 대한을 생성하는 것은 과도 할 수 있습니다. 따라서 사용하지 않으면 추가 메모리를 얻을 수 있습니다 smart_ptr...

하지만 정말 그렇게 하시겠습니까?

단일 메모리 누수로 인해 제품이 제 시간에 오류 지점을 가질 수 있습니다 (프로그램이 매시간 4 메가 ​​바이트를 누수한다고 가정 해 봅시다. 컴퓨터를 중단하는 데 몇 달이 걸리 겠지만, 누수가 있기 때문에 알 수 있습니다) .

"당신의 소프트웨어는 3 개월 동안 보증됩니다. 그러면 저에게 서비스를 요청하십시오."라고 말하는 것과 같습니다.

그래서 결국은 정말 문제입니다 ...이 위험을 감당할 수 있습니까? 수백 개의 다른 개체에 대한 인덱싱을 처리하기 위해 원시 포인터를 사용하는 것은 메모리 제어를 잃을 가치가 있습니다.

대답이 예이면 원시 포인터를 사용하십시오.

고려하고 싶지 않다면 a smart_ptr는 훌륭하고 실행 가능하며 멋진 솔루션입니다.


4
좋아,하지만 Valgrind의는 좋은 ™ 않도록 당신이 그것을 사용하는 당신이 안전해야 가능한 메모리 누수에 대한 검사에서
graywolf

당신이 당신의 기억을 처리 할 수 있는지 예 @Paladin, smart_ptr큰 팀 정말 유용합니다
Claudiordgz

3
나는 사물의 그것을 단순화 많은 unique_ptr 사용하지만 shared_ptr을 좋아하지 않아, 참조 카운트가되지 매우 효율적 GC이고 하나 완벽하지 그
graywolf

1
@Paladin 모든 것을 캡슐화 할 수 있다면 원시 포인터를 사용하려고합니다. 인수처럼 여기 저기 돌아 다니는 것이 있다면 아마도 smart_ptr을 고려할 것입니다. 내 unique_ptrs의 대부분은 주 또는 실행 방법처럼 큰 구현에 사용되는
Claudiordgz

@Lothar 나는 당신이 당신의 대답에서 말한 것 중 하나를 의역 한 것을 본다 : Thats why you should not do this unless the function is really involved in ownership management... 훌륭한 대답, 감사합니다, upvoted
Claudiordgz dec

0

간단히 살펴보고 []연산자를 gcc -lstdc++ -std=c++14 -O0위해이 결과를 사용하여 컴파일 하고 출력 한 다음 코드에서 설명하는 것처럼 원시 포인터보다 ~ 5 배 느립니다 .

malloc []:     414252610                                                 
unique []  is: 2062494135                                                
uq get []  is: 238801500                                                 
uq.get()[] is: 1505169542
new is:        241049490 

저는 C ++를 배우기 시작하고 있습니다. 저는 이것을 생각하고 있습니다. 당신은 항상 당신이 무엇을하고 있는지 알고 다른 사람들이 당신의 C ++에서 무엇을했는지 알기 위해 더 많은 시간을 할애해야합니다.

편집하다

@Mohan Kumar가 methioned에 따라 자세한 내용을 제공했습니다. gcc 버전은 7.4.0 (Ubuntu 7.4.0-1ubuntu1~14.04~ppa1), 위의 결과 -O0는를 사용할 때 얻었지만 '-O2'플래그를 사용하면 다음과 같이 표시됩니다.

malloc []:     223
unique []  is: 105586217
uq get []  is: 71129461
uq.get()[] is: 69246502
new is:        9683

그런 다음에 이동 clang version 3.9.0, -O0했다 :

malloc []:     409765889
unique []  is: 1351714189
uq get []  is: 256090843
uq.get()[] is: 1026846852
new is:        255421307

-O2 였다:

malloc []:     150
unique []  is: 124
uq get []  is: 83
uq.get()[] is: 83
new is:        54

clang의 결과 -O2는 놀랍습니다.

#include <memory>
#include <iostream>
#include <chrono>
#include <thread>

uint32_t n = 100000000;
void t_m(void){
    auto a  = (char*) malloc(n*sizeof(char));
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}
void t_u(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

void t_u2(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    auto tmp = a.get();
    for(uint32_t i=0; i<n; i++) tmp[i] = 'A';
}
void t_u3(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a.get()[i] = 'A';
}
void t_new(void){
    auto a = new char[n];
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

int main(){
    auto start = std::chrono::high_resolution_clock::now();
    t_m();
    auto end1 = std::chrono::high_resolution_clock::now();
    t_u();
    auto end2 = std::chrono::high_resolution_clock::now();
    t_u2();
    auto end3 = std::chrono::high_resolution_clock::now();
    t_u3();
    auto end4 = std::chrono::high_resolution_clock::now();
    t_new();
    auto end5 = std::chrono::high_resolution_clock::now();
    std::cout << "malloc []:     " <<  (end1 - start).count() << std::endl;
    std::cout << "unique []  is: " << (end2 - end1).count() << std::endl;
    std::cout << "uq get []  is: " << (end3 - end2).count() << std::endl;
    std::cout << "uq.get()[] is: " << (end4 - end3).count() << std::endl;
    std::cout << "new is:        " << (end5 - end4).count() << std::endl;
}

지금 코드를 테스트했는데 고유 포인터를 사용할 때 10 % 만 느립니다.
Mohan Kumar

8
-O0코드를 벤치마킹 하거나 디버그 하지 마십시오 . 출력은 매우 비효율적 입니다. 항상 최소한 사용하십시오 -O2(또는 -O3요즘에는 일부 벡터 -O2
화가

1
시간이 있고 커피 브레이크를 원하면 링크 시간 최적화를 위해 -O4를 사용하면 모든 작은 추상화 함수가 인라인되고 사라집니다.
Lothar

당신은 freemalloc 테스트와 delete[]new (또는 make 변수 astatic)에 대한 unique_ptr호출 delete[]을 포함해야합니다.
RnMss
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.