找到2214个回复 (用户: 老虎会游泳)
  • @无名啊,我知道你想采用的方法。我的观点是如果不确定就不要使用。

    注解
    restrict 限定符(像寄存器存储类)是有意使用以促进优化的。而从所有组成一致程序的预处理翻译单元中,删除所有此限定符的实例不会影响其含义(即可观的行为)。
    编译器可以忽略任何一个或全部使用 restrict 的别名使用暗示。
    欲避免未定义行为,程序员应该确保 restrict 限定指针所做的别名引用断言不会违规。

    此外实现类型转换解引用的另一个方案:

    许多编译器提供作为 restrict 对立面的语言扩展:指示即使指针类型不同,也可以别名使用的属性: may_alias (gcc)

  • @无名啊,有一个实验方法,就是用 gcc -O2 -S 编译一段函数,看看加restrict和不加有什么区别。

  • @无名啊,至于性能问题,据我所知编译器很少生成主动刷新CPU缓存的代码,大部分工作都是交由CPU自动完成的,除非涉及同步原语(信号量、互斥锁等)。

  • @无名啊,如果问题是阅读理解,我会回答“不行”。

    若某个可由 P (直接或间接)访问的对象会被任何手段修改,则该块中所有对该对象(读或写)的访问,都必须经由 P 出现,否则行为未定义。
    

    之前读和之后读都是读。文段中没有体现出时间前后的区别,只强调了定义域的区别。如果读访问发生在不含指针P的块中,则不会有问题。

  • @无名啊,根据以下文段,我觉得所有权的转移是在声明时发生的,而非使用时发生的。所以只要这个定义域内存在“restrict 指针 P”,就不能通过其他手段访问。

    在每个声明了 restrict 指针 P 的块(典型例子是函数体的执行,其中 P 为参数)中,若某个可由 P (直接或间接)访问的对象会被任何手段修改,则该块中所有对该对象(读或写)的访问,都必须经由 P 出现,否则行为未定义。

    只有一种情况可以存在其他别名:“restrict 指针 P”指向的内容不会进行任何修改。

    若对象决不被修改,则它可以被别名引用,并被异于 restrict 限定的指针访问。

  • @无名啊,对,restrict的作用应该是所有权的转移,规则应该和Rust类似:所有权转移给某别名之后,就不能再用其他别名访问了,但是转移之前则可以。

  • @无名啊,顺便一提,在安卓上long是64位,这就是符号位差异的来源

    Screenshot_20230127_170107.jpg(97.4 KB)

    还有,memcpy看起来是最佳选择,因为它没有任何多余的操作——我们想要的就是内存复制,所以我们就应该写内存复制。改成memcpy后,代码比用联合与指针类型转换都简单。

    #include <stdio.h>
    #include <stdint.h>                                                                                     float Q_rsqrt( float number ) {
      int32_t i = 0;
      const float threehalfs = 1.5F;
    
      float x2 = number * 0.5F;
      float y  = number;
      memcpy(&i, &y, sizeof(y));                       // evil floating point bit level hacking
      i  = 0x5f3759df - ( i >> 1 );               // what the fuck?
      memcpy(&y, &i, sizeof(y));
      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("sizeof(long):    %lu\n", sizeof(long));
      printf("sizeof(int32_t): %lu\n", sizeof(int32_t));
      printf("sizeof(float):   %lu\n", sizeof(float));
    
      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;
    }
    
  • @无名啊,你说得对,( long * ) &y是左值,* ( long * ) &y是对它求值。

    不过Q_rsqrt()改写成符合标准似乎很容易。

    还有符号位的处理与union版本存在差异,所以确实可能涉及未定义行为。得到负数解可能才是这种内存操作应该有的结果。

  • @无名啊,如果特别担心,就用联合吧。

    #include <stdio.h>
    
    float Q_rsqrt( float number ) {
      union { long l; float f; } i;
      float x2, y;
      const float threehalfs = 1.5F;
    
      x2 = number * 0.5F;
      y  = number;
      i.f = y;                       // evil floating point bit level hacking
      i.l  = 0x5f3759df - ( i.l >> 1 );               // what the fuck?
      y  = i.f;
      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;
    }
    

    代码实际上变简单了。

    不过有趣的是,使用联合的版本给出的都是负值(虽然也是正确解,平方根有两个解),不知道符号位的处理和非联合版本有什么不同。

    Screenshot_20230127_164309.jpg(144.41 KB)

  • @无名啊,经过一番思考之后,我还是认为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一定得在内存,所以无论怎么优化,结果应该都是正确的。

  • @无名啊,这里没有未定义行为,因为取地址操作会阻止优化。因为&a,所以a必须在内存,不能优化到寄存器。所以该代码没有未定义行为,但存在出现编程错误的风险(如果float和long长度不同)。

    float a = 1.0;
    long * b = (long *)&a;
    
    *b = 1;
    return a;
    
  • @无名啊,我还是要说,错误行为不是未定义行为。

    解引用指向float值的long指针具有明确的定义,因为float的内存表示在IEEE754定义,long的内存表示在C中定义。在特定的实现中,两者的长度可能相同,也可能不同,但当两者长度不同时,错误一定会以规定好的方式发生:float及其后不属于它的4字节会被访问。这只是编程错误,不是未定义行为。

    watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1N1bkNoZXJyeURyZWFt,size_16,color_FFFFFF,t_70.jpg(11.22 KB)

  • 需要注意的是,错误行为不是未定义行为

    char c;
    long i;
    
    // 这个行为非常不恰当,会导致紧接着`c`后面的3个字节被访问,这3个字节不属于`c`。
    // 但它只是错误行为,不是未定义行为。
    // 这个行为会发生什么具有明确的定义,就是`c`所指向的内存地址及其后方3个字节一同被赋值给`i`,在所有平台上都会发生同样的事情。
    // 所以,这里不含未定义行为,只含编程错误。
    i = * ( long * ) &c;
    
  • @无名啊,我已经对上述问题进行了回答。

  • @无名啊,把这段代码拆分成多个部分,应该有助于理解为什么没有未定义行为:

    long i;  long *p_i;
    float y; float *p_y;
    
    p_y = &y; // 只是一个简单的取地址操作,不是未定义行为
    p_i = (long *) p_y; // 对指针进行类型转换不是未定义行为,所有指针类型都是互相兼容的
    i = *p_i; // i 和 *p_i 类型一致,没有未定义行为
    

    操作的每一步都不含未定义行为,所以整体不含未定义行为。

  • @无名啊i = * ( long * ) &y不含未定义行为,因为long i,所以* ( long * )long显然是它的兼容类型。当赋值发生时,类型已经是long了。而把一个float指针转换为long指针显然也不是未定义行为,因为实际上只是绕过了编译器的类型检查,对于代码生成来说相当于什么也没有发生,指针的值没有任何变化。

  • 鼠标垫脏了或者不平也会有这种现象

  • 至于 i = 0x5f3759df - ( i >> 1 ) 到底意味着什么,其实也可以有纯数学的解释。

    0x5f3759dfi 其实都是浮点数,但是使用整数规则进行了运算,这些运算同时操作了浮点数的指数和尾数部分。

    比如 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位整数和单精度浮点数,只在packunpack时才转换为32位单精度,所以两者会有精度差异。

    此外32位和64位在处理符号位上可能也有差异,所以C版给出负数解的情况下PHP给出的是正数解。当然两者都是正确的解,因为负数的平方也是正数。

    Screenshot_20230127_144400.jpg(159.56 KB)

    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;
    }
    
  • @无名啊,此外,Q_rsqrt()函数中没有未定义行为,IEEE 754 标准已经精确的定义了单精度浮点数(float)的二进制表示,所以把它的二进制表示做为long使用不是未定义行为,结果应该是很明确的:符号位依然是符号位,指数和尾数则被拼接在一起做为整数的值。

    反向操作(把整数的二进制表示做为单精度浮点数使用)结果也很明确:符号位依然是符号位,然后接下来8位成为指数,最后23位成为尾数。

    所以,这只是一个“用户定义浮点数算法”,它与GMP等其他用户定义数学库中的自定义浮点数算法没有本质区别。代码中的每次类型转换在C中都有明确的定义。在所有使用IEEE754单精度浮点数的计算机中,结果都应该是一致的。