登录 立即注册

首页 > 绿虎论坛 > 电脑 > 讨论/求助 (发帖)

如何理解 C/C++ 中的 指针别名(pointer alias)、restrict、const 的关系呢?


『回复列表(80|隐藏机器人聊天)』

20.

@老虎会游泳,cppreference 说,这是 UB:

image.png(64.78 KB)

(/@Ta/2023-01-27 16:11//)

21.
层主 @老虎会游泳 于 2023-01-27 16:16 删除了该楼层。
(/@Ta/2023-01-27 16:13//
被锁定
)

22.

@老虎会游泳,我没能力修改 gcc,所以就认了。要么加 -fno-strict-aliasing,要么用 memcpy,来防止结果出错。

但这还会影响到性能低下问题(特别是修改了 char * 后,编译器会认为一大堆对象有可能被修改了,所以缓存失效,需要重新读取),所以我要搞懂 restrict

(/@Ta/2023-01-27 16:16//)

23.

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

(/@Ta/2023-01-27 16:30//)

24.

@老虎会游泳( long * ) &y 这个是左值吗?解引用后,类型是 long,不是字符类型,与 float 也不是兼容类型,所以应该是未定义行为

我 C/C++ 写得不多,现在看到那篇文章,就更怕编译器没能正确实现 UB 了。。

(/@Ta/2023-01-27 16:33//)

25.

@老虎会游泳,万一哪天,编译器改成,看到不兼容类型的指针转换,后续都不管对这个对象的任何操作了,咋办?

反正也符合标准。。

(/@Ta/2023-01-27 16:41//)

26.

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

#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)

(/@Ta/2023-01-27 16:43//)

27.

@老虎会游泳,比如,(long *)&y,编译器认为结果是 NULL。。。

(/@Ta/2023-01-27 16:44//)

28.

@老虎会游泳,诶,好像只是访问时 UB 噢,转换指针类型应该没问题?

(/@Ta/2023-01-27 16:47//)

29.

@无名啊,你说得对,( long * ) &y是左值,* ( long * ) &y是对它求值。

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

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

(/@Ta/2023-01-27 16:47//)

30.

@老虎会游泳,关于联合体,应该只有 C 能用,C++ 不能:

cppreference - 联合体 - 解释 说:

联合体的大小仅足以保有其最大的数据成员。其他数据成员在该最大成员的一部分相同的字节分配。分配的细节是实现定义的,且读取并非最近写入的联合体成员是未定义行为。许多编译器以非标准语言扩展实现读取联合体的不活跃成员的能力。

(/@Ta/2023-01-27 16:54//)

31.

@无名啊,顺便一提,在安卓上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;
}
(/@Ta/2023-01-27 17:07//)

32.

@老虎会游泳,另外,我这篇帖子,主要是想讨论 restrict 的(因为我已经认下严格别名了)。。

我刚才看了看你一开始的其他回复。

我认为,根据 cppreference 对 restrict 的叙述,应该是允许有其他别名的。

只要保证:通过 restrict 修饰的指针写入某个对象后,不再使用其他别名访问这个对象,即可?

(/@Ta/2023-01-27 17:04//)

33.

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

(/@Ta/2023-01-27 17:09//)

34.

@老虎会游泳,我在给 SQLite 写流式解析 csv 的小插件,想只用一个缓冲区存储:数据块、某一行解析好的CSV数据。

比如,读取 4K 数据,然后边解析,边往这个缓冲区开头写入。

由于 CSV 数据解析完不会需要更多内存来存储,只会由于转义变得需要更少内存,所以不会发生写指针超出读指针问题。

所以,我在想写指针能不能加 restrict?加了有没有用?读指针需要加 restrict 吗?

(/@Ta/2023-01-27 17:10//)

35.

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

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

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

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

(/@Ta/2023-01-27 17:23//)

36.
层主 @老虎会游泳 于 2023-01-27 17:20 删除了该楼层。
(/@Ta/2023-01-27 17:18//
被锁定
)

37.

@老虎会游泳,“对象类型”是指非函数类型。。

另外,只是指向 char 类型的读写指针。

另外,根据严格别名说的,char * 可修改任何类型的数据。

所以,我认为给 char * 添加 restrict,可防止【写指针写入数据后,编译器认为所有指针的数据缓存都失效不能用了,需要重新读取】。

不知这个想法对不对

(/@Ta/2023-01-27 17:23//)

38.

@老虎会游泳,只是【通过 char * restrict 修改后,不再使用其他别名】,也不行?

因为解析了一个字符,并写入后,就不再需要读取这个及之前的字符对象了

(/@Ta/2023-01-27 17:29//)

39.

@老虎会游泳,写指针 和 读指针,都指向同一块缓冲区。只是写指针写入过后的字符对象,都绝不再读取。(因为写指针不会超出读指针,如上所述)

(/@Ta/2023-01-27 17:30//)

下一页 上一页 2/5页,共80楼

回复需要登录

12月1日 10:59 星期天

本站由hu60wap6华为CPU驱动

备案号: 京ICP备18041936号-1