@老虎会游泳,
( long * ) &y这个是左值吗?解引用后,类型是long,不是字符类型,与float也不是兼容类型,所以应该是未定义行为。我 C/C++ 写得不多,现在看到那篇文章,就更怕编译器没能正确实现 UB 了。。
@无名啊,经过一番思考之后,我还是认为
i = * ( long * ) &y没有问题,因为没有生成新的别名,整个表达式应该被视为一个右值。相反,把它分开的操作反而是有问题的,这违反了严格别名规则。
long i; long *p_i; float y; float *p_y; p_y = &y; p_i = (long *) p_y; i = *p_i;但它应该也没有副作用,因为不涉及对
*p_i的写入。
最重要的是,
i = * ( long * ) &y的目标是读取y的值,它根本没有任何优化空间。&y意味着y一定得在内存,所以无论怎么优化,结果应该都是正确的。
@老虎会游泳,我没能力修改
gcc,所以就认了。要么加-fno-strict-aliasing,要么用memcpy,来防止结果出错。但这还会影响到性能低下问题(特别是修改了
char *后,编译器会认为一大堆对象有可能被修改了,所以缓存失效,需要重新读取),所以我要搞懂restrict
层主 @老虎会游泳 于 2023-01-27 16:16 删除了该楼层。
@老虎会游泳,cppreference 说,这是 UB:
@无名啊,这里没有未定义行为,因为取地址操作会阻止优化。因为
&a,所以a必须在内存,不能优化到寄存器。所以该代码没有未定义行为,但存在出现编程错误的风险(如果float和long长度不同)。float a = 1.0; long * b = (long *)&a; *b = 1; return a;
@老虎会游泳,按照标准,可能根本不会发生解引用指向float值的long指针,因为可能已经被优化掉了。。也就没有你后面说的浮点定义如何如何……
@老虎会游泳,如果按 cppreference 所说,应该是未定义行为。
转换指针类型没问题,只要不访问就行。(但不访问,转了也没用。可认为不能转)
比如:
float a = 1.0; long * b = (long *)&a; *b = 1; return a;按照标准,编译器可认为,
a未被修改(因为*b不是a的兼容类型,所以修改*b不应该污染a),所以优化掉b,直接返回1.0。。。
@无名啊,我还是要说,错误行为不是未定义行为。
解引用指向float值的long指针具有明确的定义,因为float的内存表示在IEEE754定义,long的内存表示在C中定义。在特定的实现中,两者的长度可能相同,也可能不同,但当两者长度不同时,错误一定会以规定好的方式发生:float及其后不属于它的4字节会被访问。这只是编程错误,不是未定义行为。
需要注意的是,错误行为不是未定义行为。
char c; long i; // 这个行为非常不恰当,会导致紧接着`c`后面的3个字节被访问,这3个字节不属于`c`。 // 但它只是错误行为,不是未定义行为。 // 这个行为会发生什么具有明确的定义,就是`c`所指向的内存地址及其后方3个字节一同被赋值给`i`,在所有平台上都会发生同样的事情。 // 所以,这里不含未定义行为,只含编程错误。 i = * ( long * ) &c;
@老虎会游泳,你看下 cppreference - 指针 - 注解 说的:
尽管任何指向对象的指针能被转型成指向其他类型对象的指针,解引用指向类型异于对象声明类型的指针几乎总是未定义行为。细节见严格别名使用。
@无名啊,我已经对上述问题进行了回答。
@无名啊,把这段代码拆分成多个部分,应该有助于理解为什么没有未定义行为:
long i; long *p_i; float y; float *p_y; p_y = &y; // 只是一个简单的取地址操作,不是未定义行为 p_i = (long *) p_y; // 对指针进行类型转换不是未定义行为,所有指针类型都是互相兼容的 i = *p_i; // i 和 *p_i 类型一致,没有未定义行为操作的每一步都不含未定义行为,所以整体不含未定义行为。
@老虎会游泳,我比较怕的是未定义行为导致的结果错误,所以想弄清楚别名规则。
看到知乎那篇文章中的第三个例子,我又觉得
restrict有助于减少性能损耗(修改char *导致编译器认为this可能被修改,进而没法重复利用缓存好的this),所以顺便想弄清楚restrict
@无名啊,
i = * ( long * ) &y不含未定义行为,因为long i,所以* ( long * )即long显然是它的兼容类型。当赋值发生时,类型已经是long了。而把一个float指针转换为long指针显然也不是未定义行为,因为实际上只是绕过了编译器的类型检查,对于代码生成来说相当于什么也没有发生,指针的值没有任何变化。
@老虎会游泳,回复好多啊,我刚看到第一条:
此外,Q_rsqrt()函数中没有未定义行为
未定义行为是:
* ( long * ) &y某左值表达式,是某个对象的[cvr修饰][有/无符号]兼容类型/含有第一项的结构体或联合体/字符类型,才能
赋值访问,否则为未定义行为。
long不是float的兼容类型,也不是字符类型,所以是未定义行为。严重时,会产生结果错误/性能低下等后果(见 知乎 - 严格别名(Strict Aliasing)规则是什么? - 严格别名(strict aliasing)为什么讨厌 中的三个例子)
鼠标垫脏了或者不平也会有这种现象
至于
i = 0x5f3759df - ( i >> 1 )到底意味着什么,其实也可以有纯数学的解释。
0x5f3759df和i其实都是浮点数,但是使用整数规则进行了运算,这些运算同时操作了浮点数的指数和尾数部分。比如
i >> 1也就是把指数和尾数同时向后挪动一位,两者的最后一位都被抛弃,然后指数的最后一位变成尾数的第一位。
0x5f3759df - $x也就是把指数和尾数同时减小,并且尾数减到小于0时向指数借位。这些操作都可以写成数学公式,从而让运算具有数学上的解析表达——也就是说,运算结果是确定的,没有未定义行为。
@无名啊,这是这个函数的PHP版本,有助于理解为什么没有未定义行为:
<?php function Q_rsqrt(float $number) { $threehalfs = 1.5; $x2 = $number * 0.5; $y = $number; $i = unpack("l", pack("f", $y))[1]; $i = 0x5f3759df - ($i >> 1); $y = unpack("f", pack("l", $i))[1]; $y = $y * ( $threehalfs - ($x2 * $y * $y) ); $y = $y * ( $threehalfs - ($x2 * $y * $y) ); return $y; } printf("%0.7f\n", Q_rsqrt(3.14)); printf("%0.7f\n", Q_rsqrt(1024.0)); printf("%0.7f\n", Q_rsqrt(10086.0)); printf("%0.7f\n", Q_rsqrt(2147483647.0)); printf("%0.14f\n", Q_rsqrt(3.14)); printf("%0.14f\n", Q_rsqrt(1024.0)); printf("%0.14f\n", Q_rsqrt(10086.0)); printf("%0.14f\n", Q_rsqrt(2147483647.0));在给定的定义域和有效数字范围内,它和C版本的结果一致。如果继续增加输出的位数,结果就开始不一致了,因为PHP在内部使用64位整数和双精度浮点数,而非C代码的32位整数和单精度浮点数,只在
pack和unpack时才转换为32位单精度,所以两者会有精度差异。此外32位和64位在处理符号位上可能也有差异,所以C版给出负数解的情况下PHP给出的是正数解。当然两者都是正确的解,因为负数的平方也是正数。
C版本:
#include <stdio.h> float Q_rsqrt( float number ) { long i; float x2, y; const float threehalfs = 1.5F; x2 = number * 0.5F; y = number; i = * ( long * ) &y; // evil floating point bit level hacking i = 0x5f3759df - ( i >> 1 ); // what the fuck? y = * ( float * ) &i; y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed return y; } int main() { printf("%0.7f\n", Q_rsqrt(3.14)); printf("%0.7f\n", Q_rsqrt(1024.0)); printf("%0.7f\n", Q_rsqrt(10086.0)); printf("%0.7f\n", Q_rsqrt(2147483647.0)); printf("%0.14f\n", Q_rsqrt(3.14)); printf("%0.14f\n", Q_rsqrt(1024.0)); printf("%0.14f\n", Q_rsqrt(10086.0)); printf("%0.14f\n", Q_rsqrt(2147483647.0)); return 0; }
