C语言 解引用指针总是会导致内存访问吗?

oyjwcjzk  于 2023-06-21  发布在  其他
关注(0)|答案(4)|浏览(120)

我想知道,不管编译器如何优化,解引用指针是否总是会被转换为机器级的Load/Store指令。
假设我们有两个线程,一个(我们称之为Tom)接收用户输入并写入一个bool变量。这个变量被另一个人(这是Jerry)读取,以决定是否继续循环。我们知道,优化编译器在编译循环时可能会将变量存储在寄存器中。因此,在运行时,Jerry可能会读取一个过时的值,该值与Tom实际写入的值不同。因此,我们应该将bool变量声明为volatile
但是,如果引用指针总是会导致内存访问,那么两个线程可以使用指针来引用变量。在每次写操作中,Tom将通过解引用指针并向其写入来将新值存储到内存中。在每次阅读时,杰瑞都可以通过取消引用同一指针来阅读汤姆写的内容。这似乎比依赖于实现的volatile更好
我是多线程编程的新手,所以这个想法可能看起来微不足道,没有必要。但我真的很好奇。

tpgth1q7

tpgth1q71#

解引用指针是否总是导致内存访问?

否,例如:

int five() {
    int x = 5;
    int *ptr = &x;
    return *ptr;
}

任何合理的优化编译器都不会在这里从堆栈内存发出mov,而是沿着于以下代码:

five():
  mov eax, 5
  ret

这是允许的,因为as-if rule

bool*线程间通信怎么做?

这就是std::atomic<bool>的作用。你不应该使用非原子对象在线程之间进行通信,因为通过两个线程以冲突的方式访问同一个内存位置1)在C++中是未定义的行为。std::atomic使其成为线程安全的,volatile没有。例如:

void thread(std::atomic<bool> &stop_signal) {
    while (!stop_signal) {
        do_stuff();
    }
}
  • 从技术上讲 *,这并不意味着来自stop_signal的每个加载都会实际发生。允许编译器执行部分循环展开,如:
void thread(std::atomic<bool> &stop_signal) {
    // only possible if the compiler knows that do_stuff() doesn't modify stop_signal
    while (!stop_signal) {
        do_stuff();
        do_stuff();
        do_stuff();
        do_stuff();
    }
}

一个原子load()被允许观察陈旧的值,因此编译器可以假设四个load()都将读取相同的值。只有一些操作,如fetch_add(),需要观察最近的值。即使这样,这种优化也是可能的。
实际上,在任何编译器中都没有为std::atomic实现这样的优化,所以std::atomic是准volatile。这同样适用于C的atomic_bool_Atomic类型。
1)如果在相同位置处的两个存储器访问中的至少一个正在写入,则它们发生冲突,即:在相同位置的两个读取不冲突。参见intro.races(https://eel.is/c++draft/intro.races)

参见
q9rjltbz

q9rjltbz2#

一些使用左值的方法是通过取消引用一个指针而产生的,但不会导致访问。例如,给定int arr[5][4]; int *p;,语句p = *arr;不会解引用任何与arr相关的存储,而只是使编译器识别出赋值右半部分的左值是int[4],它将衰减为int*
在这些情况之外,标准试图将所有情况归类为未定义行为,其中标准旨在允许实现通过执行访问或不执行访问来处理解引用操作,在其空闲时间,* 并且其决定将明显影响程序行为 *。
这种哲学导致了一些相当模糊的情况,即程序使用一些存储来保存类型T的结构,然后是类型U的结构,然后再次是类型T的结构,然后在没有写入所有字段的情况下复制T,最后使用fwrite输出T的整个副本。如果编译器知道原始T中的某个字段是用某个值写的,它可能会生成将相同值存储到副本中的代码,而不考虑底层存储是否已经改变。如果宇宙中没有任何东西会关心通过fwrite处理的数据中与该字段相关联的字节,那么这应该不会造成问题,并且要求程序员确保在将T复制为类型T之前使用该类型写入与T相关联的所有存储器,这将使程序员和计算机都有必要It’运行程序是为了做额外的无用工作。该标准没有描述程序行为的方法,这将允许一个实现在复制它时明显地无法取消引用T的所有字段,而不会将程序描述为调用未定义行为。

e4yzc0pl

e4yzc0pl3#

  • p不会导致内存访问的另一种情况是使用sizeof运算符:sizeof(*p)将简单地确定p指向的静态类型的大小。注意,在C中,使用可变长度参数,sizeof(*p)实际上可能需要内存访问,但VLA是C++中的编译器扩展。
v6ylcynt

v6ylcynt4#

当使用多线程时,显式性是好的。所以我会把每一部分都分解。

“解引用指针是否总是导致内存访问。"
**否。**考虑表达式语句(void)*p*p执行 * 间接 *。来自[expr.unary.op]:

一元 * 运算符执行间接寻址:它所应用的表达式应该是指向对象类型的指针,或者指向函数类型的指针,并且结果是引用表达式所指向的对象或函数的左值。
所以结果是一个左值引用。这本身不足以导致对由p指向的数据的“读取”。在上面的例子中,我显式地丢弃了结果,所以没有理由读取内存。
当然,有人可能会说p的内存是读的。只是为了卖弄学问,我想指出这是对这个词的一种解释。然而,优化编译器可以看到p指向的左值在这里是不需要的,所以它实际上根本不需要读/写指针。
那么在多线程环境中呢?其中的关键是[intro.multithread]中的“happens-before”关系。这是一种非常枯燥的形式语言,但其基本思想是,如果A在B之前排序(在单个线程中),或者如果A在B之前发生,则事件A在事件B之前发生。后者是一种花哨的语言,律师称之为一种工具,用于捕获同步原语(如互斥锁和原子)的行为。
如果A不发生在B之前,B也不发生在A之前,那么这两个事件就没有相对于彼此的顺序。这就是当没有互斥锁之类的东西来强制排序时,两个线程上发生的情况。如果一个事件写入内存位置,而另一个事件读取或写入该地址,则结果是数据竞争。数据竞争是未定义的行为:你得到你得到的。规范没有任何关于发生这种情况时会发生什么的说法。它没有说它是否触发了记忆访问...上面完全没有提到。

**作为[intro.multithread]中的规则的一个效果,编译器被有效地允许优化其代码,就像一个线程在完全隔离的情况下运行一样 *,除非 * 线程原语(如互斥或原子)强制执行其他操作。**这包括所有常见的省略,例如如果没有必要,不从内存读取。

相关问题