为什么转换不同类型的指针会导致TBAA(基于类型的别名分析)违规?

4
我正在阅读这篇博客,然后我发现了一些代码,但我并不真正理解。为什么这是糟糕的代码?
float *P;

void zero_array() {
  int i;
  for (i = 0; i < 10000; ++i)
  P[i] = 0.0f;
}

int main() {
  P = (float*)&P;  // cast causes TBAA violation in zero_array.
  zero_array();
}

我希望有人能够解释一下,请看下文。

所以,P 指向它自己。听起来对我没问题。 - Martin James
2
@MartinJames UB,违反了别名规则。 - ouah
啊...它是一个浮点指针,指向的是一个浮点数指针而不是一个浮点数...对吧! - Martin James
你是在问“为什么C语言有这个规则?”还是“这段代码如何违反了C语言的规则?” - M.M
@M.M:这段代码如何违反了这个规则。 - Michael Heidelberg
2个回答

4
以下是代码示例:
float *P;
P = (float*)&P; 
P[0] = 0.0f;

违反了严格别名规则。

对象P具有有效类型float *,因为这是它的声明类型。C11 6.5/6:

访问存储值的对象的有效类型是该对象的已声明类型(如果有)。

第二行执行后,表达式P[0]表示与P相同的内存位置。(注意:对于此解释,假设sizeof(float) == sizeof(float *)。显然,如果这些大小不同,则情况更糟!)

执行P[0] = 0.0f使用类型为float的lvalue访问类型为float *的对象。这违反了6.5/7:

一个对象只能由具有以下类型之一的lvalue表达式访问其存储的值

在这些引用中,“访问”表示“读取或写入”。所列出的“以下类型”列表不包括任何异常,可以涵盖使用float表达式来读取float *的情况。


这个示例有点难以理解,因为它是自我参考的。但是,它与这个更简单的示例完全相同:

float *Q;
float *P = &Q;
*P = 0.0f;

在这种情况下,Q 的类型为 float *,但是它通过 float 类型的左值进行了写入。
此示例演示了该规则的原理。假设不存在严格的别名规则,并且允许所有别名。然后,评估 P[0] = 0.0f 会更改 P。在常见的实现中,这将导致 P 现在成为空指针,但我们可以轻松想象分配其他值,该值使 P 成为指向其他变量的有效指针。
在那种情况下,行 P[1] = 0.0f 必须成功地写入另一个变量。因此,编译器将无法用 memset 替换循环;这样的 memset 将更新 P[1] 的原始位置,但不会更新执行了 P[0] = ....; 后的 P[1] 的新位置。优化不允许改变程序的可观察行为。
严格别名规则的存在意味着编译器可以执行优化,例如将此循环更改为 memset,而不必担心循环的内容可能会即时更改指针。
注意。行 P = (float *)&P 也可能是对齐违规。对齐规则和严格别名规则是完全独立的规则,不应混淆。 (一些编写不良的页面尝试将严格别名解释为某种对齐要求)。

我从这段代码中理解到的是:float *Q; float *P = &Q; *P = 0.0f;,现在 P 指向 Q 所在的位置,并将 Q 指向了 0.0f 或 NULL。我理解得对吗? - Michael Heidelberg
1
@MichaelHeidelberg 不是的,这会违反严格别名规则,导致未定义行为。如果没有严格别名规则,并且您的系统采用IEEE754标准,您的系统将所有位都设置为零作为空指针,并且float *float的位大小相同,则会使Q成为一个空指针。 - M.M
谢谢,我明白了。还有,“你的系统使用全零位表示空指针”,是否有一些系统使用不同的表示方法来表示NULL?(这可能是我们有NULL常量的原因)? - Michael Heidelberg

2

如博客中所述,这个:

for (i = 0; i < 10000; ++i)
    P[i] = 0.0f;
}

可以优化为以下形式:
memset(P, 0, 40000);

这是因为P应该指向一个float数组,其中一个float在这个例子中占据4个字节。

但如果您这样做:

P = (float*)&P;

然后P实际上指向一个指向float指针数组。如果float *大小为8个字节,则优化将失败。

以下是更具体的例子:

int main() {
    int i;

    P = malloc(10000 * sizeof(float));
    zero_array();   // this properly sets an array of 10000 floats to 0.
    free(P);

    float **PP = malloc(10000 * sizeof(float *));
    P = (float *)PP;
    zero_array();    // if sizeof(float *) == 8, the first 5000 pointers will be NULL, 
                     // and the next 5000 will contain garbage.
    free(PP);
}

2
从技术上讲,这是实现定义的,因为C语言不要求使用IEEE 754,它使用所有位零来表示0.0f,因此可能会有另一种标准将零表示为所有位都是1。对吧? - cadaniluk
1
@cad 技术上讲,这是 UB(别名规则违反),但也请注意 C 委员会在《Rationale C99》文档中所说的:“就委员会所知,所有机器都将所有位零视为浮点零的表示”。 - ouah
这个解释是错的。P = (float *)&P的问题与memset的大小无关。 - M.M
@M.M:我觉得解释还不错,可能是因为我的知识水平有限。您能否请再解释一下正在发生的事情? - Michael Heidelberg
1
然后 P 实际上指向一个指向浮点数的指针数组 - 呃,不是的,它指向一个单独的 float *P 不是一个数组。 "更具体的例子" 有很大的不同;实际上,那段代码是完全合法的。 是的,它没有正确地初始化空间,但并没有什么法律禁止这样做,你只是没有使用正确的长度。 - M.M
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接