@无名啊,
this是一个内存地址,既然是内存地址,所以当然在内存。编译器的问题只是在没有必要的情况下反复读取了它。而我说的寄存器变量,是完全不在内存,在且仅在寄存器的变量,这样的变量根本没有内存地址。
@无名啊,寄存器没有内存地址,所以任何有地址的内容必然在内存,但是在内存不意味着慢,因为它还可以同时存在于L3/L2/L1缓存中,而且也不必马上写回内存。
任何不需要有内存地址的内容,都可以放进寄存器,只要寄存器还没满。
内容在不在寄存器是静态分配的,编译的时候就决定了,查看汇编代码就能看出来。
如果寄存器还没满,并且内容不需要有内存地址,编译器没有理由不把它放进寄存器,除非专门指示它不进行此类优化(
-O0或volatile)。
@无名啊,以下是可能的分配:
char buf[16]; // 数组只能在内存,因为数组访问操作涉及取地址。内存中的数据会自动逐级缓存在L3/L2/L1 Cache,该操作由CPU自动完成。 char *read_next = buf; // 指针本身可以在寄存器,指向的内容当然不在寄存器,只可能在内存和缓存中 char *write_next = buf + 16; // 指针本身可以在寄存器,指向的内容当然不在寄存器,只可能在内存和缓存中如果一个内容可以被指针指向,意味着它一定有一个内存地址,也就是说它一定在内存中,当然它也可以同时在L3/L2/L1 Cache中,但不会在寄存器中。在寄存器中的内容没有内存地址。
但是指针本身(也就是内存地址这个数值本身)可以在寄存器中。
@无名啊,
缓存至寄存器
没有这种操作。
一个变量要么在寄存器,要么在内存,不会同时位于两者。
位于内存的变量只会被缓存到L3/L2/L1 Cache中,不会位于寄存器。而这个缓存操作是CPU自动进行的,不需要程序控制。
所以,变量在不在寄存器,看汇编代码就能知道,不需要运行时确定。
@无名啊,至于43楼的设计到底能不能认为所有权发生了转移,我认为是值得争议的问题。
因为从瞬时来看,每个单独的时刻,所有权都从读指针转移到了写指针。
但从全局来看,所有权在读指针和写指针之间共享。
所以到底算不算转移,可能是“实现定义的”
@无名啊,
restrict针对的是指针指向的内容,不是指针本身。指针本身是否被优化到寄存器与restrict无关。如果不对指针进行取地址操作,它就可以被优化到寄存器。如果不确定,你可以用gcc -O2 -S查看汇编代码。
@无名啊,我知道你想采用的方法。我的观点是如果不确定就不要使用。
注解
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位,这就是符号位差异的来源
还有,
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; }代码实际上变简单了。
不过有趣的是,使用联合的版本给出的都是负值(虽然也是正确解,平方根有两个解),不知道符号位的处理和非联合版本有什么不同。
@无名啊,经过一番思考之后,我还是认为
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字节会被访问。这只是编程错误,不是未定义行为。
需要注意的是,错误行为不是未定义行为。
char c; long i; // 这个行为非常不恰当,会导致紧接着`c`后面的3个字节被访问,这3个字节不属于`c`。 // 但它只是错误行为,不是未定义行为。 // 这个行为会发生什么具有明确的定义,就是`c`所指向的内存地址及其后方3个字节一同被赋值给`i`,在所有平台上都会发生同样的事情。 // 所以,这里不含未定义行为,只含编程错误。 i = * ( long * ) &c;
@无名啊,我已经对上述问题进行了回答。
