【问题标题】:Pointer alignment and aliasing指针对齐和别名
【发布时间】:2016-07-02 13:53:19
【问题描述】:

我看过不少下面的代码(抽象例子):

char* byteBlockPtr;

long* alignedPtr = NULL;

/* ... */

/* aligning pointer by long boundary */
while (!ALIGNED(byteBlockPtr))
{
  byteBlockPtr++;
}

alignedPtr = (long*)byteBlockPtr;

/* ... */

/* do stuff with memory */
alignedPtr++; /* go to next block */

/* ... */

这很容易理解,因为从 char 指针转换为更严格的指针类型(在本例中为 long 指针)需要对齐相同。

同样适用于 void 指针吗?

如果一个人正在编写自己的 memset,是否必须遵循任何一般规则才能不破坏指针的对齐方式?

关于 char 和 void 指针以及其他指针,如果有的话,指针别名和对齐之间有什么联系?例如,如果按照标准将 void 指针隐式转换为任何其他指针类型,这是否意味着也可以保证满足对齐要求?


附:提前对超过 1 个问题表示抱歉,但显然我的知识存在差距,我不知道如何缩小范围。

【问题讨论】:

  • ALIGNED 不是标准的。使用标准方法检查对齐。如果那是一些自定义宏,请显示它。 (旁注:名称选择不当,因为不清楚它检查哪个对齐方式)。
  • @Olaf,就像我说的,这是一个抽象的例子。它只是检查指针是否与长边界对齐。我怀疑它的特定实现对手头的问题至关重要,但如果你不这么认为,我会更新帖子。
  • 您是否阅读过关于此主题的各种帖子:stackoverflow.com/search?q=strict+aliasing
  • AFAIK char *void * 具有相同的内部表示,如果从 void * 转换为无效的 T *,则行为未定义。
  • @melpomene 为什么?仅仅第一次分配它不应该是UB。之后再读是的。你能为此争论吗?请使用标准引用或良好的链接,带参数。

标签: c pointers memory-management strict-aliasing


【解决方案1】:

如果使用标准中列出的特定类型以外的任何类型的指针修改任何类型的对象,C 标准允许钝化实现做任何他们想做的事情,无论编译器是否有理由期望正在修改对象。指针是否正确对齐并不重要。根据基本原理,规则的存在使得给定的代码如下:

float f;
void hey(int *p)
{
  f=1.0f;
  *p=6;
  f+=1.0f;
}

编译器不必悲观地假设p 可能持有f 的地址,因此在指针分配之前写入f 并在之后读取它。在这种情况下,编译器没有理由期望对p 的写入会影响f,因此没有理由期望冗余存储和加载会起到任何作用。

虽然没有证据表明该标准的作者打算让编译器编写者如此迟钝,以至于忽略明显存在别名的情况,但一些编译器编写者,包括那些参与 gcc 的编译器编写者,将缺乏授权解释为表明他们应该忽略明显的别名,因为这样做会促进更“高效”的代码,而不考虑所讨论的代码是否真的有用。

在任何定义了检查指针是否针对给定类型适当对齐的方法的平台上,将指针转换为 char*,除非或直到它适当对齐,否则将其递增,然后将其转换为其他类型将产生指向其他类型的指针。不幸的是,虽然 C11 定义了一种标准方法来确保一种类型的对象以符合另一种对齐要求的方式定位,但它没有定义一种标准方法,通过该方法代码可以利用这种对齐方式而不会遇到别名问题.

如果代码只需要在非钝角编译器上运行,我建议从一种类型转换为另一种类型并作为后一种类型访问应该是可靠的,前提是使用新类型的操作是通过一个被转换的指针完成的 在最后一次使用旧类型访问之后从旧类型到新类型,并且所有使用强制转换指针的操作都在使用旧类型的下一次访问之前完成。大多数使用“分块优化”的代码都适合该模式,并且它是编译器支持的简单模式,无需做出过于悲观的假设(如果代码将指针从 T1* 类型转换为 T2* 然后写入它,假设认为这样的操作可能会影响 T1 类型的对象可能是悲观的,但在大多数情况下它也是正确的)。

不幸的是,因为标准还没有强制编译器识别别名,即使在很明显的情况下,而且 gcc 的作者对没有强制要求的这种识别不感兴趣,所以没有任何方法可以安全地在 gcc 中使用分块优化使用非标准 gcc 特定的扩展或使用 -fno-strict-aliasing 标志。在使用该标志时获得良好的性能将需要学习使用 restrict 限定符,但是使用分块来加速热循环并使用 restrict 来最小化 -fno-strict-aliasing 的性能影响似乎比使用慢速非分块循环。另请注意,gcc 通常会处理正确使用分块优化的代码,有或没有标志,但 gcc 的作者认为,当这些代码在没有标志的情况下编译时的任何正确行为都是“意外的”,并且不反对“修复”[即打破]这样的代码没有警告。

顺便说一句,如果想以完全一致的方式使用分块优化,唯一的方法是(1)使用面向字节的代码并希望优化器以某种方式找出如何用分块版本替换它,或者(2) 使用 memcpy/memmove 从其他存储加载字大小的变量,并希望优化器设法用健全的代码替换它们。例如,如果一个 64 位对齐的指针指向一堆 uint16_t 值并希望计算它们的补码,则可以使用:

void flip_quad16s(uint16_t *p, int num_quads)
{
  uint64_t *pp = (uint64_t*)p;
  union {
    uint64_t dw;
    uint16_t hw[4];
  } u;
  for (int i=0; i<num_quads; i++)
  {
    memcpy(u.hw, pp, 8);
    u.dw = ~u.dw;
    /* Note that if p actually identifies something which has no declared
       type but will be used as uint16_t, we must make sure that memcpy
       uses that as a source type */
    memcpy(pp++, u.hw, 8);
  }
}

当然,这需要编译器假定 p 可能有别名 任何类型的任何东西,甚至可能会阻止完美的优化编译器 从实现与非钝角编译器一样好的结果 使用采用 uint16_t 的代码实现,将其转换为 uint64_t,然后 与之合作,例如

void flip_quad16s(uint16_t *p, int num_quads)
{
  uint64_t *pp = (uint64_t*)p;
  for (int i=0; i<num_quads; i++)
    pp[i] = ~pp[i];
}

一个理智的编译器应该更容易将后一个函数转换为 与任何编译器相比,将反转一堆 uint16_t 值的最佳代码 对前一个函数做同样的事情,特别是如果它在一个 使用其他类型的循环,因为使用 memcpy 会强制 编译器承认所有类型的潜在别名,而不仅仅是 uint16_t 和 uint64_t。

【讨论】:

  • 非常感谢您的详细解答。所以如果我理解正确,你相信,递增指向 char 的指针(完整链看起来像这样:void pointer arg -&gt; char pointer mem iterator -&gt; align -&gt; long ptr -&gt; chunk opt-n)直到它适当对齐 是唯一正确的方法,不像直接分配(void ptr arg -&gt; long ptr -&gt; chunk opt-n)。对吗?
  • 如果实现适当地定义了指针类型和 uintptr_t 之间的转换,那么以数字方式处理指针然后将其转换为合适的类型可能是一个好方法,除非下一版本的 C 标准打破它通过将“出处”的概念添加到 uintptr_t [恕我直言,这是一件荒谬的事情,因为将指针转换为整数的最常见原因是在访问机器存储时绕过类型系统]。如果不通过 uintptr_t 处理对齐,如果底层数据可能是具有...的类型,则转换为 char* 可能是最好的。
  • ...任意对齐。如果已知一个指针可以识别 uint16_t 类型的对齐对象,但想使用 uint32_tuint64_t 类型来访问它们,那么我建议接受使用 uint16_t* 直到对齐,然后强制转换到uint32_t*uint64_t*
  • “C 标准允许笨拙的实现做任何他们想做的事情......” - 不。违反有效类型规则是未定义的行为。这不是津贴。
  • @supercat 所以基本上如果一个人想要实现一些块优化,他必须手动对齐指向块的指针?并且不允许在评估后直接使用作为替代方案?
猜你喜欢
  • 2011-01-20
  • 1970-01-01
  • 2017-08-16
  • 1970-01-01
  • 2017-10-02
  • 2018-01-17
  • 1970-01-01
  • 1970-01-01
  • 2021-06-13
相关资源
最近更新 更多