【问题标题】:Combining a pointer and an integer in a union在联合中组合指针和整数
【发布时间】:2011-12-12 11:23:49
【问题描述】:

我定义了一个联合如下:

union {
  uintptr_t refcount;
  struct slab_header *page;
} u;

page 指针保证在页面边界上对齐(很可能是 4096),并且永远不会是NULL。这意味着可能的最低地址将是4096

refcount 将在0 .. 4095 内。

在创建封闭结构后,我可以拥有u.refcount = 0u.page = mmap(...)

围绕这个联合的代码将是这样的:

if (u.refcount < 4096) {
  /* work with refcount, possibly increment it */
} else {
  /* work with page, possibly dereference it */
}

这是否总是保证在完全符合 POSIX 的实现上工作? uintptr_tstruct slab_header * 是否有可能具有不同的表示形式,例如,当 u.page == 8192u.refcount &lt; 4096 产生 true 时?

【问题讨论】:

  • 假设两个usion成员兼容的。如果 refcount 为 0 - 4095,那么当 refcount 为 0 时,页指针肯定为 NULL,因为 NULL 几乎总是被实现为 0 或 (void*)0。
  • “这总是保证有效吗?” - 由谁担保?按照标准,没有。通过任何给定的 C 实现自己的文档/ABI,几乎可以肯定是的。大概如果你打电话给mmap,那么 Posix 中的保证就足够了,尽管我没有声称有一个。
  • @Steve:假设一个完全符合 POSIX 的实现?

标签: c pointers integer unions


【解决方案1】:

我不认为它“总是保证有效”,因为:

  1. uintptr_t 是可选的 (7.18.1.4)。
  2. void * 可以转换为 uintptr_t 并返回 (7.18.1.4)。不能保证struct slab_header* 就是这种情况。 void * 具有与指向字符类型的指针相同的表示和对齐要求。指向结构的指针不必具有相同的表示或对齐方式 (6.2.5 27)。即使不是这种情况,也不能保证sizeof(uintptr_t) == sizeof(void *),它显然可以更大,并且仍然满足在同类指针的典型情况下可转换为void * 的要求。
  3. 最后,即使它们具有相同的大小并且可以转换,指针值的表示方式也可能与无符号整数的表示方式有一种奇怪的不同。无符号整数的表示相对受限(6.2.6.2 1),但指针上不存在这样的约束。

因此,我得出的结论是,最好的方法是有一个共同的初始元素来告诉状态。

【讨论】:

  • 7.18.1.4 表示任何有效的void* 值都可以转换为uintptr_t 并返回。它并没有说相反,任何有效的uintptr_t 值都可以转换为void* 并返回。所以确实uintptr_t 可以大于void*,如果void* 的大部分表示范围不是有效指针,它也可以合法地更小。当然,我从来没有见过这么奇怪的东西。
  • @Steve 谢谢,我已经相应地修正了答案。
  • 好的,感谢您的准确回答。根据 C99,它不能保证,因为 uintptr_t 不需要与 void *X * 具有相同的大小或表示。换个说法,是否有一个主流的 POSIX 兼容操作系统可能有sizeof(uintptr_t) != sizeof(struct slab_header *),在主流架构(ARM、x86 等)上运行?
【解决方案2】:

我要回答一个不同的问题——“这是个好主意吗?”我从您的代码中看到的更重要的担忧是别名问题。如果可以编写一段具有以下效果的代码,我不会感到惊讶(事实上,我会温和地期待

  • 写信给u.refcount
  • 写信给u.page
  • 阅读u.refcount
  • 发现你读到的值和你第一次写的一样

你可能会嗤之以鼻,但我已经看到这种情况发生了——如果你不明白其中的危险,那么调试将需要 非常 很长时间。

你可能对这个特殊的工会很安全;我会把它留给对这类事情有更多经验的人(和/或方便的 C 标准副本)来做出判断。但我想强调的是,这是重要需要担心的事情!!!

还有另一个成本。将联合与“魔术”测试结合使用以发现其中存储的内容(尤其是使用系统特定事实的测试)将使您的代码更难阅读、调试和维护。您可以采取措施缓解这一事实,但这始终是个问题。

当然,当有人试图在一台奇怪的机器上使用你的代码时,你的代码根本无法工作。

正确的解决方案可能是以某种方式构建您的代码,以便关心数据布局的唯一代码是几个很小的inlined 访问例程,这样您就可以轻松地交换存储方式。组织您的代码以使用编译时标志来选择哪一个!

现在我已经说了这么多,我真正想问的问题(并且想让你养成思考的习惯):“值得吗?”您在时间、代码可读性、易于编程和可移植性方面做出了牺牲。你得到了什么?

很多人忘记问这个问题,他们编写了复杂的错误代码,而简单、易于编写和阅读的代码同样好。

如果您发现此更改将使耗时的例程的性能提高一倍,那么处理此 hack 可能是值得的。但是,如果您只是因为节省 8 个字节是一个聪明的技巧而对此进行调查,那么您应该仅将其视为一种智力练习。

【讨论】:

    【解决方案3】:

    我认为&lt;stdint.h&gt;和C99标准保证uintptr_tvoid*具有相同的大小,并且可以无损失地铸造。

    针对指针进行页面对齐是一个实现细节。

    【讨论】:

      【解决方案4】:

      该代码应该有任何问题。但是对于任何情况,您都可以在程序的 init 中进行检查:

      if (sizeof(uintptr_t) != sizeof (struct slab_header*)
      {
          print error
      }
      

      但似乎有些不对劲(或不清楚)。当你有一个页面时,你希望“refcount”为 0。当页面为 NULL 时不为零?

      【讨论】:

        【解决方案5】:

        这是否总能保证有效?

        没有。您无法阅读与最后一个书面不同的工会成员。优化器确实考虑到了这一点,所以这不是理论上的问题。

        uintptr_t 和 structslab_header * 有没有可能有不同的表示, 因此,例如,当 u.page == 8192 时,u.refcount

        理论上也是可以的。我想不出它发生的当前实现。

        【讨论】:

        • 这不是真的;你可以。该标准特别允许类型双关。
        • @Artefacto:也就是说,C89 只允许非常有限的类型双关语(包含具有共同初始序列的结构的联合)。 C99 还指出,访问联合类型的成员会使用另一个成员来对存储在那里的任何内容进行双关语。在这个特定的例子中,无论如何它不是严格的别名规则下的法律双关语,所以虽然“你不能读取与最后一个写的不同的工会的成员”这句话一般来说是不正确的,但到目前为止这个工会是正确的就标准而言。
        • @Steve 至于 C89,很公平。但我不确定你的严格别名论点是否成立。 6.5.7 允许联合类型为对象设置别名。我不认为他想使用类型双关语本身(通过结构将整数转换为指针,然后访问对象),所以我没有问题。
        • @Artefacto:也许吧,但乍一看,如果他写了一个指向u.page 的指针,然后像问题中的代码一样读取u.refcount,他正在将指针转换为整数通过结构,期望值 >= 4096。它完全是一种双关语类型,尽管使用严格别名进行优化的 AFAIK 编译器实际上对这种双关语并不严格。
        • @Steve 当然,这是一种双关语,但它与别名有什么关系?他不会通过指针访问任何对象,除非他确定联合实际上包含一个指针,此时他可以使用它来访问该对象。这将导致的唯一问题是,实际上,如果 a 有一个函数接收这种类型的联合,并且该联合具有整数,而不是指针,编译器就不能假定联合没有别名 slab_header 结构。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-11-05
        • 1970-01-01
        • 1970-01-01
        • 2013-09-17
        • 2010-10-30
        • 1970-01-01
        相关资源
        最近更新 更多