- iOS/Objective-C 元类和类别
- objective-c - -1001 错误,当 NSURLSession 通过 httpproxy 和/etc/hosts
- java - 使用网络类获取 url 地址
- ios - 推送通知中不播放声音
PIMPL 习语通常用于对象的公共(public) API,有时也包含虚函数。在那里,堆分配通常用于分配多态对象,然后将其存储在 unique_ptr
或类似的地方。一个著名的例子是 Qt API,其中大多数对象(尤其是 QWidgets 等)在堆上分配并由 QObject 父/子关系跟踪。因此,我们为两次分配支付费用,一次是对象本身使用 2*sizeof(void*)
来保存 PIMPL 和 v_table
指针,一次是私有(private)数据本身。
现在来回答我的问题:我想知道这两个分配是否可以合并,类似于 make_shared
应用的优化。然后我想知道这种优化是否值得,因为 malloc
的实现可能非常擅长处理字大小的分配请求。另一方面,积极的缓存效果可能非常明显,即让私有(private)数据紧挨着公共(public)对象分配。
我尝试了以下代码:
#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。 关于如何解决这个问题的任何想法?我看到的唯一方法是引入丑陋的工厂方法。
我还想知道是否有任何方法可以覆盖(取消)分配对象的整个过程,包括调用其构造函数/析构函数。然后,可以简单地从重载的 operator new 中调用不同的构造函数并完成它......
现在让我们看看是否值得:
使用 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% )
评论?有没有办法在堆栈分配情况下摆脱对未初始化内存的读取?
最佳答案
很久以前我就开始尝试优化 pimpls,包括使用 Windows 线程信息 block 快速确定外部对象是在堆栈上还是堆上,以及使用有点像 alloca
的东西来放置新的和手动的 dtor 调用来构建和破坏粉刺。
在那里,我处理的热点更多地与 pimpl 的创建和破坏相关,而不是与减少内存局部性和间接性的访问成本相关,但速度非常快。它减少了在堆栈上使用廉价 pimpl 创建对象的时间,从大约 400 个时钟周期减少到 13 个时钟周期,因为它完全消除了免费存储开销。这是很久以前的 90 年代:今天的情况可能有所不同。
从那以后我就后悔了。
那是我觉得自己变得太聪明的时候之一,使代码难以维护、移植和理解,即使使用通用机制使任何对象订阅系统变得微不足道和可重用也是如此。它只是过于反对语言设计,想要平衡高级对象构造与最低级别的汇编类型 hack。
相反,我会建议简单地使您的类足够抽象,以避免提及实现细节以一次性分配子类实例化。示例:
// --------------------------------------------------------
// 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 好处,但不会丢失整个对象的连续内存布局。缺点是间接虚函数调用的抽象成本,但这几乎总是被严重高估。您通常会立即更好地用通常可以忽略不计的抽象成本换取更好的内存/缓存局部性带来的并非总是可以忽略的好处。
如果您需要的不止于此,那时我会建议在一个良好且安全的公共(public)接口(interface)背后进行更多类似 C 的编码,作为一种罕见的通配符,因为它实际上更容易进行低级位/字节内存管理而无需担心关于面向对象的构造妨碍。我仍然建议将此类代码保留在更高级别、安全的 C++ 接口(interface)之后。
至于利用堆栈,在堆栈上创建对象非常快。固定分配器也是如此,它在 O(1) 中分配/取消分配对象而无需搜索(例如:池分配器将内存块视为缓冲区和单链表指针之间的 union - 空闲时为列表节点,空闲时为缓冲区占据)。您可以使用这样的分配器获得类似堆栈的性能,并且您的对象将在内存中靠近空间局部性(特别是如果您的分配和 Release模式与您对堆栈的利用率一致,在这种情况下固定分配的行为就像虚拟堆栈)。
如果您已经计划为此类对象使用堆栈,则可以使固定分配仅使用具有预定大小和无分支分配和释放的单个池,真正与硬件堆栈相媲美(在效率和缺乏防止溢出的安全性,每个线程都需要一个单独的安全性)。如果你走这条路,我建议选择这个无分支分配器(具有单独的功能或重载)作为选择性优化细节。与全自动相比,半自动优化解决方案更容易避免陷入麻烦。
您可以做的另一件事是使用这个新的 std::aligned_storage
类型,而我当时还没有。不过,这需要您预测 header 中 pimpl 的大小,而且我很想让它比实际大一些,以便为更改留出一些空间。如果您开始想这样做,我仍然推荐使用抽象方法,因为您不想开始破坏 ABI 或摆弄 header 以向 pimpl 添加更多内容。
关于c++ - 覆盖 operator new 以合并 PIMPL 分配,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26740195/
#include using namespace std; class C{ private: int value; public: C(){ value = 0;
这个问题已经有答案了: What is the difference between char a[] = ?string?; and char *p = ?string?;? (8 个回答) 已关闭
关闭。此题需要details or clarity 。目前不接受答案。 想要改进这个问题吗?通过 editing this post 添加详细信息并澄清问题. 已关闭 7 年前。 此帖子已于 8 个月
除了调试之外,是否有任何针对 c、c++ 或 c# 的测试工具,其工作原理类似于将独立函数复制粘贴到某个文本框,然后在其他文本框中输入参数? 最佳答案 也许您会考虑单元测试。我推荐你谷歌测试和谷歌模拟
我想在第二台显示器中移动一个窗口 (HWND)。问题是我尝试了很多方法,例如将分辨率加倍或输入负值,但它永远无法将窗口放在我的第二台显示器上。 关于如何在 C/C++/c# 中执行此操作的任何线索 最
我正在寻找 C/C++/C## 中不同类型 DES 的现有实现。我的运行平台是Windows XP/Vista/7。 我正在尝试编写一个 C# 程序,它将使用 DES 算法进行加密和解密。我需要一些实
很难说出这里要问什么。这个问题模棱两可、含糊不清、不完整、过于宽泛或夸夸其谈,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开,visit the help center . 关闭 1
有没有办法强制将另一个 窗口置于顶部? 不是应用程序的窗口,而是另一个已经在系统上运行的窗口。 (Windows, C/C++/C#) 最佳答案 SetWindowPos(that_window_ha
假设您可以在 C/C++ 或 Csharp 之间做出选择,并且您打算在 Windows 和 Linux 服务器上运行同一服务器的多个实例,那么构建套接字服务器应用程序的最明智选择是什么? 最佳答案 如
你们能告诉我它们之间的区别吗? 顺便问一下,有什么叫C++库或C库的吗? 最佳答案 C++ 标准库 和 C 标准库 是 C++ 和 C 标准定义的库,提供给 C++ 和 C 程序使用。那是那些词的共同
下面的测试代码,我将输出信息放在注释中。我使用的是 gcc 4.8.5 和 Centos 7.2。 #include #include class C { public:
很难说出这里问的是什么。这个问题是含糊的、模糊的、不完整的、过于宽泛的或修辞性的,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开它,visit the help center 。 已关
我的客户将使用名为 annoucement 的结构/类与客户通信。我想我会用 C++ 编写服务器。会有很多不同的类继承annoucement。我的问题是通过网络将这些类发送给客户端 我想也许我应该使用
我在 C# 中有以下函数: public Matrix ConcatDescriptors(IList> descriptors) { int cols = descriptors[0].Co
我有一个项目要编写一个函数来对某些数据执行某些操作。我可以用 C/C++ 编写代码,但我不想与雇主共享该函数的代码。相反,我只想让他有权在他自己的代码中调用该函数。是否可以?我想到了这两种方法 - 在
我使用的是编写糟糕的第 3 方 (C/C++) Api。我从托管代码(C++/CLI)中使用它。有时会出现“访问冲突错误”。这使整个应用程序崩溃。我知道我无法处理这些错误[如果指针访问非法内存位置等,
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。 关闭 7 年前。
已关闭。此问题不符合Stack Overflow guidelines 。目前不接受答案。 要求我们推荐或查找工具、库或最喜欢的场外资源的问题对于 Stack Overflow 来说是偏离主题的,因为
我有一些 C 代码,将使用 P/Invoke 从 C# 调用。我正在尝试为这个 C 函数定义一个 C# 等效项。 SomeData* DoSomething(); struct SomeData {
这个问题已经有答案了: Why are these constructs using pre and post-increment undefined behavior? (14 个回答) 已关闭 6
我是一名优秀的程序员,十分优秀!