【问题标题】:Can storing unrelated data in the least-significant-bit of a pointer work reliably?在指针的最低有效位中存储不相关的数据可以可靠地工作吗?
【发布时间】:2011-09-13 16:02:11
【问题描述】:

让我先说一下,我知道我将要提出的建议是一种致命的罪过,即使考虑到它,我也可能会在编程地狱中燃烧。

也就是说,我仍然想知道是否有任何原因导致这不起作用。

情况是:我有一个引用计数智能指针类,我到处都在使用它。它目前看起来像这样(注意:不完整/简化的伪代码):

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。但是,如果我删除布尔成员变量 _isObjectOnHeapsizeof(Ref) 会减少到 8。这意味着对于我的程序中的每个 Ref,有 7.875 个浪费的 RAM 字节......并且有很多很多 @ 987654327@ 在我的程序中。

嗯,这似乎是在浪费一些 RAM。但我真的需要额外的信息(好吧,让我幽默一下,为了讨论,我假设我真的这样做了)。而且我注意到,由于IRefCountable 是一个非 POD 类,它(可能)总是被分配在一个字对齐的内存地址上。因此,(_ptr) 的最低有效位应始终为零。

这让我想知道......有什么理由我不能将我的一位布尔数据 OR 到指针的最低有效位中,从而在不牺牲任何功能的情况下将 sizeof(Ref) 减少一半?当然,在取消引用指针之前,我必须小心地取消该位,这会降低指针取消引用的效率,但这可能会通过 Refs 现在更小,因此更多的事实来弥补可以立即放入处理器的缓存中,依此类推。

这是合理的做法吗?还是我在为一个受伤的世界做好准备?如果是后者,我究竟会受到怎样的伤害? (请注意,这是需要在所有相当现代的桌面环境中正确运行的代码,但它不需要在嵌入式机器或超级计算机或任何类似的奇异计算机中运行)

【问题讨论】:

  • Boost 中使用了该技巧 - 请参阅 the boost::multi_index 文档的末尾。
  • >> 浪费了 7.875 字节的 RAM。什么?你有模拟字节吗?
  • 好吧,7 个字节的填充,加上最后一个字节中的 7 位未使用,因为布尔值只需要一位。有点像普通家庭有 2.5 个孩子 :)
  • 是否允许回答我们可以跳过bool变量的方式?
  • 在 x86 上,您可能最好使用高位,因为在大多数 ABI 上,该空间是为内核保留的(并且由于在 x86 上,您可以轻松地获得未对齐的指针,尽管正如您所说的那样非 POD 分配器永远不会做一个)。无论您使用什么位,您都应该确保在您的构造函数中检查该位在进入时被清除。

标签: c++ bit-manipulation smart-pointers


【解决方案1】:

这里的问题是它完全依赖于机器。这在 C 或 C++ 代码中并不常见,但在汇编中肯定已经做过很多次了。旧的 Lisp 解释器几乎总是使用这个技巧将类型信息存储在低位中。 (我在 C 代码中看到过 int,但在为特定目标平台实现的项目中。)

就个人而言,如果我尝试编写可移植代码,我可能不会这样做。事实是它几乎肯定会在“所有相当现代的桌面环境”上工作。 (当然,它适用于我能想到的每一个。)

很大程度上取决于代码的性质。如果您正在维护它,并且没有其他人将不得不处理“受伤的世界”,那么它可能没问题。您必须为以后可能需要支持的任何奇怪架构添加 ifdef。另一方面,如果您将其作为“可移植”代码发布给全世界,那将引起关注。

另一种处理方法是编写两个版本的智能指针,一个用于可以在其上工作的机器,一个用于不能在其上工作的机器。这样一来,只要您维护这两个版本,将配置文件更改为使用 16 字节版本就没什么大不了的。

不用说,您必须避免编写任何其他假定 sizeof(Ref) 为 8 而不是 16 的代码。如果您正在使用单元测试,请在两个版本中运行它们。

【讨论】:

    【解决方案2】:

    即使这种方法有效,心中总会有一种不确定性的感觉,因为最终你是在使用可能是可移植的也可能不是可移植的内部架构。

    另一方面,为了解决这个问题,如果你想避免bool变量,我会建议一个简单的构造函数,

    Ref(IRefCountable * ptr) : _ptr(ptr) 
    {
      if(ptr != 0) 
        _ptr->Ref();
    }
    

    从代码中,我闻到只有当对象在上时才需要引用计数。对于自动对象,您可以简单地将0 传递给class Ref 并在构造函数/析构函数中进行适当的空检查。

    【讨论】:

    • 将指针设置为 NULL 的问题是,有时我需要将堆栈对象传递给接受 Ref 的函数...
    • @Jeremy Friesner:然后重载该函​​数以获取原始指针。
    • @DeadMG ...并且还重载它调用的所有函数,以及它们调用的所有函数,等等?我想我最终会维护我的代码库的两个独立副本:)
    【解决方案3】:

    如果您只想使用标准工具而不依赖任何实现,那么使用 C++0x 可以表达对齐方式(这是我回答的 recent question)。还有std::uintptr_t 可以可靠地获得一个足够大以容纳指针的无符号整数类型。现在可以保证的一件事是,从指针类型转换为 std::[u]intptr_t 并返回到相同类型会产生原始指针。

    我想你可能会争辩说,如果你能取回原来的std::intptr_t(带屏蔽),那么你就能得到原来的指针。我不知道这个推理有多可靠。

    [编辑:考虑一下,不能保证对齐指针在转换为整数类型时采用任何特定形式,例如一个有一些未设置的位。这里可能有点牵强]

    【讨论】:

    • +1 用于提及一种调用未定义行为的方式,该方式可能具有此处任何方法中最可预测的结果和可检测的故障模式。
    【解决方案4】:

    任何原因?除非最近标准发生了变化,否则指针的值表示是实现定义的。当然,某处的某些实现可能会采用相同的技巧,为自己的目的定义这些否则未使用的低位。更有可能的是,某些实现可能使用字指针而不是字节指针,因此两个相邻的字不是位于“地址”0x8640 和 0x8642,而是位于“地址”0x4320 和 0x4321。

    解决这个问题的一个棘手方法是使 Ref 成为(事实上的)抽象类,并且所有实例实际上都是 RefOnHeap 和 RefNotOnHeap 的实例。如果周围有那么多 Ref,则用于存储三个类而不是一个类的代码和元数据的额外空间将由每个 Ref 大小的一半节省的空间来弥补。(不起作用太好了,如果没有虚方法,编译器可以省略 vtable 指针,引入虚方法会将 4 或 8 个字节添加回类。

    【讨论】:

    • 当然抽象意味着使用虚拟方法,然后不是用于布尔值的“字节”,而是指向 vtable 的完整指针(在传统实现中)......
    • @MatthieuM.: 好点子,我有一段时间没有搞乱 C++ 了,忘记了如果没有虚方法,它可以省略 vtable 指针。
    【解决方案5】:

    您是否考虑过类外存储?

    根据您是否(或是否)担心多线程并控制 new/delete/malloc/free 的实现,可能值得一试。

    重点是,不是增加本地计数器(对象本地),而是维护一个“计数器”映射地址 --> 计数会傲慢地忽略分配区域之外传递的地址(例如堆栈)。

    这可能看起来很傻(在 MT 中存在争用空间),但它也适用于只读,因为该对象不是仅为计数而“修改”的。

    当然,我不知道你可能希望通过这个实现的性能:p

    【讨论】:

      【解决方案6】:

      是的,如果指针与大于 1 的 2 的幂对齐,则在最低有效位中存储数据始终可靠。这意味着它适用于除 char*/ 之外的所有内容bool* 或指向包含所有 char/bool 成员的结构的指针。在 C++11 中,您可以通过使用 alignofstd::alignment_of 强制类的对齐要求来轻松执行此操作

      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
      

      即使对齐为 1,您也可以使用 alignas 或使用旧 C++ 中的其他扩展(如 __declspec(align))将其更改为更高的值。动态分配的内存已经与max_align_t对齐,或者您可以使用aligned_alloc进行更高级别的对齐

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-04-07
        • 1970-01-01
        • 2016-05-28
        • 1970-01-01
        • 1970-01-01
        • 2016-02-06
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多