2014-11-04 3 views
4

PIMPL 관용구는 종종 가상 함수도 포함하는 객체의 공용 API에 사용됩니다. 여기서 힙 할당은 종종 다형성 객체를 할당하는 데 사용되며이 객체는 unique_ptr 또는 이와 비슷한 형태로 저장됩니다. 이것의 유명한 예가 Qt API입니다. Qt API에서는 대부분의 객체 (특히 QWidgets 등)가 힙에 할당되고 QObject 부모/자식 관계에 의해 추적됩니다. 따라서 우리는 두 개의 할당에 대해 지불합니다. 객체 자체가 2*sizeof(void*)이고 PIMPL을 보유하고 v_table 포인터를 보유하고 개인 데이터를 한 번 지불합니다.PIMPL 할당을 병합하기 위해 새로운 덮어 쓰기 연산자

내 질문에 와서 : make_shared에 의해 적용된 최적화와 마찬가지로 두 할당이 병합 될 수 있는지 궁금합니다. 그렇다면 malloc의 구현이 잠재적으로 워드 크기 할당 요청을 처리 할 때 상당히 우수하므로이 최적화가 그만한 가치가 있는지 궁금합니다. 반면에 긍정적 인 캐시 효과는 상당히 두드러 질 수 있습니다. 즉, 개인 데이터가 공개 객체 바로 옆에 할당됩니다. 사람이 예상하는대로 출력은


#include <memory> 
#include <cstring> 
#include <vector> 
#include <iostream> 

using namespace std; 

#ifdef NDEBUG 
#define debug(x) 
#else 
#define debug(x) x 
#endif 

class MyInterface 
{ 
public: 
    virtual ~MyInterface() = default; 

    virtual int i() const = 0; 
}; 

class MyObjOpt : public MyInterface 
{ 
public: 
    MyObjOpt(int i); 
    virtual ~MyObjOpt(); 

    int i() const override; 

    static void *operator new(size_t size); 
    static void operator delete(void *ptr); 
private: 
    struct Private; 
    Private* d; 
}; 

struct MyObjOpt::Private 
{ 
    Private(int i) 
    : i(i) 
    { 
    debug(cout << " Private " << i << '\n';) 
    } 
    ~Private() 
    { 
    debug(cout << " ~Private " << i << '\n';) 
    } 
    int i; 
}; 

MyObjOpt::MyObjOpt(int i) 
{ 
    debug(cout << " MyObjOpt " << i << "\n";) 
    if (reinterpret_cast<void*>(d) == reinterpret_cast<void*>(this + 1)) { 
    new (d) Private(i); 
    } else { 
    d = new Private(i); 
    } 
}; 

MyObjOpt::~MyObjOpt() 
{ 
    debug(cout << " ~MyObjOpt " << d->i << '\n';) 
    if (reinterpret_cast<void*>(d) != reinterpret_cast<void*>(this + 1)) { 
    delete d; 
    } 
} 

int MyObjOpt::i() const 
{ 
    return d->i; 
} 

void* MyObjOpt::operator new(size_t /*size*/) 
{ 
    void *ret = malloc(sizeof(MyObjOpt) + sizeof(MyObjOpt::Private)); 
    auto obj = reinterpret_cast<MyObjOpt*>(ret); 
    obj->d = reinterpret_cast<Private*>(obj + 1); 
    return ret; 
} 

void MyObjOpt::operator delete(void *ptr) 
{ 
    auto obj = reinterpret_cast<MyObjOpt*>(ptr); 
    obj->d->~Private(); 
    free(ptr); 
} 

class MyObj : public MyInterface 
{ 
public: 
    MyObj(int i); 
    ~MyObj(); 

    int i() const override; 

private: 
    struct Private; 
    unique_ptr<Private> d; 
}; 

struct MyObj::Private 
{ 
    Private(int i) 
    : i(i) 
    { 
    debug(cout << " Private " << i << '\n';) 
    } 
    ~Private() 
    { 
    debug(cout << " ~Private " << i << '\n';) 
    } 
    int i; 
}; 

MyObj::MyObj(int i) 
    : d(new Private(i)) 
{ 
    debug(cout << " MyObj " << i << "\n";) 
}; 

MyObj::~MyObj() 
{ 
    debug(cout << " ~MyObj " << d->i << "\n";) 
} 

int MyObj::i() const 
{ 
    return d->i; 
} 

int main(int argc, char** argv) 
{ 
    if (argc == 1) { 
    { 
     cout << "Heap usage:\n"; 
     auto heap1 = unique_ptr<MyObjOpt>(new MyObjOpt(1)); 
     auto heap2 = unique_ptr<MyObjOpt>(new MyObjOpt(2)); 
    } 
    { 
     cout << "Stack usage:\n"; 
     MyObjOpt stack1(-1); 
     MyObjOpt stack2(-2); 
    } 
    } else { 
    const int NUM_ITEMS = 100000; 
    vector<unique_ptr<MyInterface>> items; 
    items.reserve(NUM_ITEMS); 
    if (!strcmp(argv[1], "fast")) { 
     for (int i = 0; i < NUM_ITEMS; ++i) { 
     items.emplace_back(new MyObjOpt(i)); 
     } 
    } else { 
     for (int i = 0; i < NUM_ITEMS; ++i) { 
     items.emplace_back(new MyObj(i)); 
     } 
    } 
    int sum = 0; 
    for (const auto& item : items) { 
     sum += item->i(); 
    } 
    return sum > 0; 
    } 
    return 0; 
} 

gcc -std=c++11 -g로 컴파일 :

나는 다음 코드를 사용하여 주위를 연주

Heap usage: 
    MyObjOpt 1 
    Private 1 
    MyObjOpt 2 
    Private 2 
    ~MyObjOpt 2 
    ~Private 2 
    ~MyObjOpt 1 
    ~Private 1 
Stack usage: 
    MyObjOpt -1 
    Private -1 
    MyObjOpt -2 
    Private -2 
    ~MyObjOpt -2 
    ~Private -2 
    ~MyObjOpt -1 
    ~Private -1 

을하지만 당신은 Valgrind의 그것을 실행할 때 ' 다음을 참조하십시오 :

Stack usage: 
    MyObjOpt -1 
==21217== Conditional jump or move depends on uninitialised value(s) 
==21217== at 0x400DC0: MyObjOpt::MyObjOpt(int) (pimpl.cpp:54) 
==21217== by 0x401200: main (pimpl.cpp:142) 
==21217== 
    Private -1 
    MyObjOpt -2 
==21217== Conditional jump or move depends on uninitialised value(s) 
==21217== at 0x400DC0: MyObjOpt::MyObjOpt(int) (pimpl.cpp:54) 
==21217== by 0x401211: main (pimpl.cpp:143) 
==21217== 
    Private -2 

이것은 스택 할당 객체와 dptr을 더 이상 할당 할 필요가없는 힙 할당 객체를 구별하기위한 검사입니다. 이 문제를 해결하는 방법에 대한 아이디어가 있습니까? 내가 보는 유일한 방법은 추악한 공장 방법을 소개하는 것입니다.

전체 con/destructor를 호출하는 것을 포함하여 객체를 할당 취소하는 프로세스가 있는지 궁금합니다.

gcc -std=c++11 -O2 -g -DNDEBUG로 컴파일 내가 얻을 : 그럼, 하나는 단순히 우리가 가치가 있는지를 살펴 보자 ... 지금


새로운 그것으로 할 수 오버로드 된 연산자에서 다른 생성자를 호출 할 수 다음과 같은 결과 :

$ perf stat -r 10 ./pimpl fast 

Performance counter stats for './pimpl fast' (10 runs): 

     9.004201  task-clock (msec)   # 0.956 CPUs utilized   (+- 3.61%) 
      1  context-switches   # 0.111 K/sec     (+- 14.91%) 
      0  cpu-migrations   # 0.022 K/sec     (+- 66.67%) 
     1,071  page-faults    # 0.119 M/sec     (+- 0.05%) 
    19,455,553  cycles     # 2.161 GHz      (+- 5.81%) [45.21%] 
    31,478,797  instructions    # 1.62 insns per cycle   (+- 5.41%) [84.34%] 
    8,121,492  branches     # 901.967 M/sec     (+- 2.38%) 
     8,059  branch-misses    # 0.10% of all branches   (+- 2.35%) [66.75%] 

    0.009422989 seconds time elapsed           (+- 3.46%) 

$ perf stat -r 10 ./pimpl slow 

Performance counter stats for './pimpl slow' (10 runs): 

    17.674142  task-clock (msec)   # 0.974 CPUs utilized   (+- 2.32%) 
      2  context-switches   # 0.113 K/sec     (+- 10.54%) 
      1  cpu-migrations   # 0.028 K/sec     (+- 53.75%) 
     1,850  page-faults    # 0.105 M/sec     (+- 0.02%) 
    43,142,007  cycles     # 2.441 GHz      (+- 1.13%) [54.62%] 
    68,780,331  instructions    # 1.59 insns per cycle   (+- 0.50%) [82.62%] 
    16,369,560  branches     # 926.187 M/sec     (+- 1.65%) [83.06%] 
     19,774  branch-misses    # 0.12% of all branches   (+- 5.66%) [66.07%] 

    0.018142227 seconds time elapsed           (+- 2.26%) 

이 마이크로 벤치 마크는 꽤 생각 났으며 약 2 배의 빠른 속도 향상이라고 생각합니다. 그럼에도 불구하고 병합 된 할당은 사실상 캐시 친화적 일 수 있습니다. 두 할당이 dptr이 어딘가에 있다는 것보다 훨씬 효율적입니다.

$ perf stat -r 10 -e cache-misses ./pimpl slow 

Performance counter stats for './pimpl slow' (10 runs): 

     37,947  cache-misses             (+- 2.38%) 

    0.018457998 seconds time elapsed           (+- 2.30%) 

$ perf stat -r 10 -e cache-misses ./pimpl fast 

Performance counter stats for './pimpl fast' (10 runs): 

     9,698  cache-misses             (+- 4.46%) 

    0.009171249 seconds time elapsed           (+- 2.91%) 

댓글 :

사실, 우리는 심지어이 볼 수 있습니까? 스택 할당 사례에서 초기화되지 않은 메모리 읽기를 제거 할 수있는 방법이 있습니까?

+0

'MyObjOpt :: MyObjOpt (int)'에서 초기화되지 않은 멤버 'd'의 값을 사용하고 있습니다. 그게 UB 야. –

+1

당신은이 모든 문제를 해결할 수 있습니다. 더 많은 것들이 pimpl 기계를 대체하여 추상적 인 기본 클래스와 그들을 가리키는 (스마트) 포인터로 대체 할 수 있습니다. –

+0

@ Cheersandhth.-Alf : 예. 저는 unitialized 멤버를 사용하고 있습니다. valgrind가 저에게 말하고있는 것입니다. 물론 그것은 UB입니다. 그래서 그것을 고치는 법을 묻습니다 ... – milianw

답변

1

필자는 윈도우 스레드 정보 블록을 사용하여 외부 객체가 스택 또는 힙에 있는지 빠르게 확인하고, alloca과 같은 비트를 사용하여 새롭고 수동으로 dtor 호출을 할 수있는 모험을했습니다. pimpls를 만들고 파괴하십시오.

이 차라리 감소 된 MEM 지역 및 간접하여 액세스 비용보다 생성과 파괴를 pimpl 더 많은 관련 핫스팟을 처리했지만, 그것은 아주 빨랐다. 이것은 무료 스토어 오버 헤드를 완전히 제거 했으므로 약 400 클럭 사이클에서 13 사이클까지 스택에 값싼 핌플이있는 객체를 만드는 시간을 단축 시켰습니다. 이것은 90 년대 전의 시대였습니다. 오늘날의 마일리지는 다를 수 있습니다.

그리고 그 이후 후회했습니다.

내가 너무 영리 해져서 코드를 유지하기가 너무 어렵게 만들었고, 모든 객체가 시스템에 가입하는 것이 사소하고 재사용 가능하도록 일반화 된 메커니즘으로도 이해하기가 어렵다. . 그것은 단지 언어 디자인과 싸우고 있으며, 높은 수준의 객체 구조와 가장 낮은 수준의 어셈블리 유형 해킹의 균형을 맞추기를 원합니다.

대신 단순히 한 번에 서브 클래스의 인스턴스 생성을 할당 구현 세부 사항을 언급하지 않도록 충분히 수업 추상적 있도록 건의 할 것입니다. 예 :

// -------------------------------------------------------- 
// In some public header: 
// -------------------------------------------------------- 
class Interface 
{ 
public: 
    virtual ~Interface() {} 
    virtual void foo() = 0; 
}; 
std::unique_ptr<Interface> create_concrete(); 

// -------------------------------------------------------- 
// In some private source file: 
// -------------------------------------------------------- 
// Include all the extra headers you need here 
// to implement the interface. 

class Concrete: public Interface 
{ 
public: 
    // Store all the hidden stuff you want here. 
    virtual void foo() override {...} 
}; 

unique_ptr<Interface> create_concrete() 
{ 
    // Can use a fast, fixed allocator here. 
    return unique_ptr<Interface>(new Concrete); 
} 

당신은 구현 세부 사항을 숨기고 컴파일러 방화벽을 만드는 pimpl 혜택 같은 종류를 얻을 수 있지만, 전체 개체에 대한 연속 메모리 레이아웃을 잃지 않고. 단점은 간접 가상 함수 호출의 추상화 비용이지만 거의 항상 과소 평가됩니다. 더 나은 메모리/캐시 지역성의 항상 보장 할 수없는 이점에 대해 종종 무시할 수있는 추상화 비용을 즉시 더 잘 거래합니다.

이보다 더 많은 정보가 필요하다면 저의 경우와는 달리 C와 비슷한 코딩을 희귀 한 와일드 카드로 사용하는 것이 안전하고 안전한 공개 인터페이스 뒤에 코딩하는 것이 좋습니다. 개체 지향 구조가 걱정되고 있습니다. 여전히 높은 수준의 안전한 C++ 인터페이스를 유지하는 것이 좋습니다. 스택 오브젝트 스택을 이용하여 작성하는 바와

슈퍼 빠르다. 따라서 검색없이 O (1)의 객체를 할당/할당 해제하는 고정 할당자가 있습니다 (예 : 풀 할당자가 메모리 청크를 버퍼와 단일 링크 된 목록 포인터 사이의 합집합으로 처리하는 경우 - 목록 노드가없는 경우, 버퍼 가득차 있는). 이러한 할당자를 사용하면 스택과 같은 성능을 얻을 수 있으며 객체가 공간적으로 가까운 지역에 대해 메모리에 가까이있게됩니다 (특히 할당 및 할당 패턴이 스택의 활용도와 일치 할 경우 고정 할당이 가상 스택).

이러한 개체에 대해 이미 스택을 활용할 계획이라면 고정 할당량에 미리 지정된 크기와 분기없는 할당 및 할당 해제를 가진 단일 풀만 사용할 수 있습니다. 실제로는 효율성 측면에서 하드웨어 스택과 비교됩니다 오버플로에 대한 안전성이 결여되고 각 스레드마다 분리 된 것이 필요함). 이 경로를 사용한다면 선택적 확장 세부 사항으로 사용할 분기없는 할당 자 (별도의 함수 또는 오버로드 포함)를 사용하는 것이 좋습니다. 완전 자동보다 반자동 최적화 솔루션으로 문제가 생기는 것을 피하는 것이 더 쉽습니다.

당신이 할 수있는 또 다른 것은, 내가 다음이 다시이 없었,이 새로운 std::aligned_storage 유형을 사용하는 것입니다. 그래서 머리말에 핌플의 크기를 예상해야합니다. 그리고 실제로는 변화를위한 여지를 남겨 두는 것보다 더 크게 만들려고합니다.나는 당신이 이것을하기 위해 유혹되기 시작한다면, 추상적 인 접근법을 여전히 추천 할 것입니다. 당신이 ABI를 깨기 시작하거나, 머리말을 더듬어서 여드름에 더 많은 것을 추가하기를 원하지 않기 때문입니다.