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%)
댓글 :
사실, 우리는 심지어이 볼 수 있습니까? 스택 할당 사례에서 초기화되지 않은 메모리 읽기를 제거 할 수있는 방법이 있습니까?
'MyObjOpt :: MyObjOpt (int)'에서 초기화되지 않은 멤버 'd'의 값을 사용하고 있습니다. 그게 UB 야. –
당신은이 모든 문제를 해결할 수 있습니다. 더 많은 것들이 pimpl 기계를 대체하여 추상적 인 기본 클래스와 그들을 가리키는 (스마트) 포인터로 대체 할 수 있습니다. –
@ Cheersandhth.-Alf : 예. 저는 unitialized 멤버를 사용하고 있습니다. valgrind가 저에게 말하고있는 것입니다. 물론 그것은 UB입니다. 그래서 그것을 고치는 법을 묻습니다 ... – milianw