层主 @老虎会游泳 于 2023-01-27 17:20 删除了该楼层。
@老虎会游泳,我在给
SQLite
写流式解析 csv 的小插件,想只用一个缓冲区存储:数据块、某一行解析好的CSV数据。比如,读取 4K 数据,然后边解析,边往这个缓冲区开头写入。
由于 CSV 数据解析完不会需要更多内存来存储,只会由于转义变得需要更少内存,所以不会发生写指针超出读指针问题。
所以,我在想写指针能不能加
restrict
?加了有没有用?读指针需要加restrict
吗?
@无名啊,对,
restrict
的作用应该是所有权的转移,规则应该和Rust类似:所有权转移给某别名之后,就不能再用其他别名访问了,但是转移之前则可以。
@无名啊,顺便一提,在安卓上long是64位,这就是符号位差异的来源
还有,
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; }
@老虎会游泳,另外,我这篇帖子,主要是想讨论
restrict
的(因为我已经认下严格别名了)。。我刚才看了看你一开始的其他回复。
我认为,根据 cppreference 对
restrict
的叙述,应该是允许有其他别名的。只要保证:通过
restrict
修饰的指针写入某个对象后,不再使用其他别名访问这个对象,即可?
@老虎会游泳,关于联合体,应该只有 C 能用,C++ 不能:
联合体的大小仅足以保有其最大的数据成员。其他数据成员在该最大成员的一部分相同的字节分配。分配的细节是实现定义的,且读取并非最近写入的联合体成员是未定义行为。许多编译器以非标准语言扩展实现读取联合体的不活跃成员的能力。
@无名啊,你说得对,
( long * ) &y
是左值,* ( long * ) &y
是对它求值。不过
Q_rsqrt()
改写成符合标准似乎很容易。还有符号位的处理与union版本存在差异,所以确实可能涉及未定义行为。得到负数解可能才是这种内存操作应该有的结果。
@老虎会游泳,诶,好像只是访问时 UB 噢,转换指针类型应该没问题?
@老虎会游泳,比如,
(long *)&y
,编译器认为结果是NULL
。。。
@无名啊,如果特别担心,就用联合吧。
#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; }
代码实际上变简单了。
不过有趣的是,使用联合的版本给出的都是负值(虽然也是正确解,平方根有两个解),不知道符号位的处理和非联合版本有什么不同。
@老虎会游泳,
( 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
。。。