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

回复列表(80|隐藏机器人聊天)
  • @Ta / 2023-01-27 / /

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

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

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

  • @Ta / 2023-01-27 / /

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

  • @Ta / 2023-01-27 / /

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

  • @Ta / 2023-01-27 / /

    @老虎会游泳,画了个草图,容易理解:
    image.png(13.01 KB)

  • @Ta / 2023-01-27 / /

    @无名啊,我知道你想采用的方法。我的观点是如果不确定就不要使用。

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

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

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

  • @Ta / 2023-01-27 / /

    @老虎会游泳,感觉 43 楼的设计,没法加 restrict

    那岂不读指针和结束指针都没法缓存至寄存器,每次读取前都要读一次内存?(因为编译器认为,写指针可能会修改 char *read_next, *end?)

  • @Ta / 2023-01-27 / /

    @无名啊restrict针对的是指针指向的内容,不是指针本身。指针本身是否被优化到寄存器与restrict无关。如果不对指针进行取地址操作,它就可以被优化到寄存器。如果不确定,你可以用gcc -O2 -S查看汇编代码。

  • @Ta / 2023-01-27 / /

    @老虎会游泳,老虎用 restrict 的情景多吗?

    我是不是要写完那个小插件,再试试加 restrict,才理解得更快呢。。

  • @Ta / 2023-01-27 / /

    @无名啊,至于43楼的设计到底能不能认为所有权发生了转移,我认为是值得争议的问题。

    因为从瞬时来看,每个单独的时刻,所有权都从读指针转移到了写指针。

    但从全局来看,所有权在读指针和写指针之间共享。

    所以到底算不算转移,可能是“实现定义的”

  • @Ta / 2023-01-27 / /

    @老虎会游泳

    指针本身是否被优化到寄存器与restrict无关。如果不对指针进行取地址操作,它就可以被优化到寄存器。

    char buf[16];
    char *read_next = buf, *write_next = buf + 16;
    
    *write_next = '\0'; // 此时 read_next 岂不就被修改了吗?(假设 read_next 就在 buf 后面)
    

    编译器咋会放心把 read_next 缓存至寄存器的。。

  • @Ta / 2023-01-27 / /

    @无名啊

    缓存至寄存器

    没有这种操作。

    一个变量要么在寄存器,要么在内存,不会同时位于两者。

    位于内存的变量只会被缓存到L3/L2/L1 Cache中,不会位于寄存器。而这个缓存操作是CPU自动进行的,不需要程序控制。

    所以,变量在不在寄存器,看汇编代码就能知道,不需要运行时确定。

  • @Ta / 2023-01-27 / /

    @老虎会游泳,意思是,拿某个寄存器来存 read_next,而不存入栈上了

    好像写入数组末尾之后,是未定义行为。这么说,编译器认为 read_next 未被修改也无不可。。

    哎,还是等写完之后,再加 restrict 试试吧。反正也不是啥速度极为严苛的场景,只是想顺便学学 restrict 而已

  • @Ta / 2023-01-27 / /

    @无名啊,以下是可能的分配:

    char buf[16]; // 数组只能在内存,因为数组访问操作涉及取地址。内存中的数据会自动逐级缓存在L3/L2/L1 Cache,该操作由CPU自动完成。
    char *read_next = buf; // 指针本身可以在寄存器,指向的内容当然不在寄存器,只可能在内存和缓存中
    char *write_next = buf + 16; // 指针本身可以在寄存器,指向的内容当然不在寄存器,只可能在内存和缓存中
    

    如果一个内容可以被指针指向,意味着它一定有一个内存地址,也就是说它一定在内存中,当然它也可以同时在L3/L2/L1 Cache中,但不会在寄存器中。在寄存器中的内容没有内存地址。

    但是指针本身(也就是内存地址这个数值本身)可以在寄存器中。

  • @Ta / 2023-01-27 / /

    @老虎会游泳,不对,你看看 知乎那篇文章 提到的第三个例子,

    修改了 this->target 的值(unsigned char 类型),导致编译器认为 this 也可能被修改了,所以后续每次 this->target 都要重新读取 this。。

    (我记得 C++ 的 this 是只读的?还是啥?编译器咋会认为被修改了呢。。)

  • @Ta / 2023-01-27 / /

    @无名啊,寄存器没有内存地址,所以任何有地址的内容必然在内存,但是在内存不意味着慢,因为它还可以同时存在于L3/L2/L1缓存中,而且也不必马上写回内存。

    任何不需要有内存地址的内容,都可以放进寄存器,只要寄存器还没满。

    内容在不在寄存器是静态分配的,编译的时候就决定了,查看汇编代码就能看出来。

    如果寄存器还没满,并且内容不需要有内存地址,编译器没有理由不把它放进寄存器,除非专门指示它不进行此类优化(-O0volatile)。

  • @Ta / 2023-01-27 / /

    @老虎会游泳,我能看懂一点点汇编,你说的我能理解。(看过半本墨绿色的 x86 汇编书,但没咋写过,又都忘得差不多了。。

  • @Ta / 2023-01-27 / /

    @无名啊this是一个内存地址,既然是内存地址,所以当然在内存。编译器的问题只是在没有必要的情况下反复读取了它。

    而我说的寄存器变量,是完全不在内存,在且仅在寄存器的变量,这样的变量根本没有内存地址。

  • @Ta / 2023-01-27 / /

    @无名啊,举个例子:

    int a, b, c, d, e, f, g;
    

    如果有5个空闲寄存器,那么a到e就只在寄存器,完全不在内存,f和g在内存,根据需要读取到寄存器。

    既然可以完全优化至寄存器,自然不需要考虑缓存失效问题。

    而指向缓冲区的局部指针变量恰好符合完全优化至寄存器的条件。

  • @Ta / 2023-01-27 / /

    @老虎会游泳this 符合【完全优化至寄存器的条件】吗?我记得是只读的啊。。那篇文章里,也没有要 &this。。

  • @Ta / 2023-01-27 / /
    int a, b, c, d, e, f, g;
    int *h = &b;
    

    而在这种情况下,可能是a、c、d、e、h在寄存器,b在内存,因为&b需要一个内存地址。指针h通常在寄存器,因为这样更快。至于a、c、d、e、f、g哪个在寄存器哪个在内存,取决于它们的使用情况。

添加新回复
回复需要登录