指针对齐和别名

Voi*_*ker 2 c pointers memory-management strict-aliasing

我见过很多以下代码(抽象示例):

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 */

/* ... */
Run Code Online (Sandbox Code Playgroud)

这是可以理解的,因为从char指针转换为更严格的指针类型(在这种情况下指向long的指针)要求对齐是相同的.

这同样适用于void指针吗?

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

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


PS提前抱歉超过1个问题,但显然我的知识存在差距,我不知道如何缩小范围.

sup*_*cat 5

如果使用除标准中列出的特定类型之外的任何类型的指针修改任何类型的对象,C标准允许obtuse实现执行他们想要的任何操作,无论编译器是否有理由期望对象是改性.无论指针是否正确对齐都无关紧要.根据基本原理,规则存在,以便给定代码如:

float f;
void hey(int *p)
{
  f=1.0f;
  *p=6;
  f+=1.0f;
}
Run Code Online (Sandbox Code Playgroud)

编译器不必悲观地假设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);
  }
}
Run Code Online (Sandbox Code Playgroud)

当然,这将要求编译器假设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];
}
Run Code Online (Sandbox Code Playgroud)

对于理智的编译器来说,将后一个函数转换为最优的代码应该要容易得多,这个代码将反转一堆uint16_t值,而不是任何编译器对前一个函数执行的操作,特别是如果它在一个使用其他类型的循环中调用因为使用memcpy会强制编译器确认所有类型的潜在别名,而不仅仅是uint16_t和uint64_t.

  • "C标准允许obtuse实现做他们想做的任何事情......" - 否.违反有效类型规则是未定义的行为.这是没有津贴的. (2认同)