让我先说一下,我所知道的是我要提出的是一个致命的罪,并且即使考虑它,我也可能会在编程地狱中燃烧。
那就是说,我仍然有兴趣知道为什么这不起作用。
情况是:我有一个我在任何地方使用的引用计数智能指针类。它目前看起来像这样(注意:不完整/简化的伪代码):
class IRefCountable
{
public:
IRefCountable() : _refCount(0) {}
virtual ~IRefCountable() {}
void Ref() {_refCount++;}
bool Unref() {return (--_refCount==0);}
private:
unsigned int _refCount;
};
class Ref
{
public:
Ref(IRefCountable * ptr, bool isObjectOnHeap) : _ptr(ptr), _isObjectOnHeap(isObjectOnHeap)
{
_ptr->Ref();
}
~Ref()
{
if ((_ptr->Unref())&&(_isObjectOnHeap)) delete _ptr;
}
private:
IRefCountable * _ptr;
bool _isObjectOnHeap;
};
今天我注意到sizeof(Ref)= 16。但是,如果我删除布尔成员变量_isObjectOnHeap,则sizeof(Ref)减少到8.这意味着对于我的程序中的每个Ref,有7.875个浪费的RAM字节......并且我的程序中有很多很多Refs
嗯,这似乎浪费了一些RAM。但我真的需要那些额外的信息(好吧,幽默我,并为了我真正做的讨论而假设)。我注意到,由于IRefCountable是一个非POD类,它(可能)总是被分配在一个字对齐的内存地址上。因此,(_ptr)的最低有效位应始终为零。这让我想知道...有什么理由说我不能将我的一点布尔数据或指针的最低有效位,并因此将sizeof(Ref)减少一半而不牺牲任何功能?当然,在取消引用指针之前,我必须小心取出那个位,这会使指针解引用效率降低,但这可能是因为Refs现在更小,因此更多可以立即适应处理器的缓存,依此类推。
这是否合理?还是我为自己创造了一个受伤的世界?如果是后者,那么伤害到底是怎么回事? (请注意,这是需要在所有合理的现代桌面环境中正确运行的代码,但它不需要在嵌入式计算机或超级计算机或任何异国情况下运行)
答案 0 :(得分:3)
这里的问题是它完全依赖于机器。它不是人们经常在C或C ++代码中看到的东西,但它在组装中肯定已经多次完成。旧Lisp解释器几乎总是使用此技巧在低位中存储类型信息。 (我在C代码中看过int,但是在为特定目标平台实现的项目中。)
就个人而言,如果我试图编写可移植代码,我可能不会这样做。事实是它几乎肯定会在“所有合理的现代桌面环境”上工作。 (当然,它会适用于我能想到的每一个。)
很大程度上取决于代码的性质。如果你维持它,没有其他人将不得不处理“受伤的世界”,那么它可能没问题。您将不得不为以后可能需要支持的任何奇怪架构添加ifdef。另一方面,如果你将它作为“便携式”代码发布到世界上,那将引起关注。
另一种处理此问题的方法是编写智能指针的两个版本,一个用于可以使用它的机器,另一个用于不适用的机器。这样,只要您维护这两个版本,更改配置文件以使用16字节版本就没什么大不了的。
不言而喻,您必须避免编写任何其他假定sizeof(Ref)
为8而不是16的代码。如果您使用单元测试,请使用两个版本运行它们。
答案 1 :(得分:3)
如果你只想使用标准设施而不依赖于任何实现,那么使用C ++ 0x就有表达对齐的方法(这里是我回答的recent question)。还有std::uintptr_t
可靠地获得足够大的无符号整数类型来保存指针。现在保证的一件事是从指针类型到std::[u]intptr_t
并返回到相同类型的转换产生原始指针。
我想你可以争辩说,如果你能找回原来的std::intptr_t
(带掩蔽),那么你可以得到原始指针。我不知道这种推理会有多扎实。
[编辑:想一想,当转换为整数类型时,无法保证对齐的指针采用任何特定的形式,例如有一些未设置的一个。这可能太过分了]
答案 2 :(得分:1)
任何原因?除非最近标准中的内容发生了变化,否则指针的值表示是实现定义的。当然某些实现可能会引发相同的技巧,为其自身目的定义这些未使用的低位。有些实现甚至可能使用字指针而不是字节指针,因此不是两个相邻字位于“地址”0x8640和0x8642,它们将位于“地址”0x4320和0x4321。
围绕这个问题的一个棘手的方法是使Ref成为一个(事实上的)抽象类,所有实例实际上都是RefOnHeap和RefNotOnHeap的实例。如果周围有很多Refs,那么用于存储三个类而不是一个类的代码和元数据的额外空间将由节省空间来弥补每个Ref的一半大小。(将无法工作太好了,如果没有虚方法,编译器可以省略vtable指针,并且引入虚方法会将4或8字节添加回类中。
答案 3 :(得分:1)
是的,如果指针对齐到大于 1 的 2 的幂,则在最低有效位中存储数据总是可靠。这意味着它适用于除 {{1} 之外的所有内容}/char*
或指向包含所有 bool*
/char
成员的结构的指针。在 C++11 中,您可以通过使用 alignof
或 std::alignment_of
bool
即使对齐为 1,您也可以使用 alignas
或旧 C++ 中的其他扩展(如 static_assert(alignof(Ref) > 1);
static_assert(alignof(IRefCountable) > 1);
// This check for power of 2 is likely redundant
static_assert((alignof(Ref) & (alignof(Ref) - 1)) == 0);
// Now IRefCountable* is always aligned, so its least significant bit can be used freely
)将其更改为更高的值。动态分配的内存已经与__declspec(align)
对齐,或者您可以使用aligned_alloc
进行更高级别的对齐
答案 4 :(得分:0)
即使这种方法有效,也会始终存在不确定性的感觉,因为最终你正在玩内部架构,这可能是可移植的,也可能是不可移植的。
另一方面为了解决这个问题,如果你想避免 bool
变量,我会建议一个简单的构造函数,
Ref(IRefCountable * ptr) : _ptr(ptr)
{
if(ptr != 0)
_ptr->Ref();
}
从代码中我发现只有当对象在堆上时才需要引用计数。对于自动对象,您只需将0
传递给class Ref
,并在构造函数/析构函数中进行适当的空检查。
答案 5 :(得分:0)
您是否考虑过课外存储?
根据您是否(或不)担心多线程并控制new / delete / malloc / free的实现,可能值得一试。
关键是,不是增加本地计数器(对象的本地),而是维持“计数器”地图地址 - >计数会傲慢地忽略在分配区域之外传递的地址(例如堆栈)。
这可能看起来很愚蠢(MT中存在争用的余地),但它也只是以只读方式播放,因为对象不是为了计数而被“修改”。
当然,我不知道你可能希望通过它实现的性能:p