为什么即使使用了 volatile 关键字,编译器也会因 strncmp() 而优化掉共享内存读取?
Why does the compiler optimize away shared memory reads due to strncmp() even if volatile keyword is used?
这是一个将数据写入共享内存的程序foo.c
。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key;
int shmid;
char *mem;
if ((key = ftok("ftok", 0)) == -1) {
perror("ftok");
return 1;
}
if ((shmid = shmget(key, 100, 0600 | IPC_CREAT)) == -1) {
perror("shmget");
return 1;
}
printf("key: 0x%x; shmid: %d\n", key, shmid);
if ((mem = shmat(shmid, NULL, 0)) == (void *) -1) {
perror("shmat");
return 1;
}
sprintf(mem, "hello");
sleep(10);
sprintf(mem, "exit");
return 1;
}
这是另一个程序 bar.c
从同一共享内存中读取数据。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key;
int shmid;
volatile char *mem;
if ((key = ftok("ftok", 0)) == -1) {
perror("ftok");
return 1;
}
if ((shmid = shmget(key, sizeof (int), 0400 | IPC_CREAT)) == -1) {
perror("shmget");
return 1;
}
printf("key: 0x%x; shmid: %d\n", key, shmid);
if ((mem = shmat(shmid, NULL, 0)) == (void *) -1) {
perror("shmat");
return 1;
}
printf("looping ...\n");
while (strncmp((char *) mem, "exit", 4) != 0)
;
printf("exiting ...\n");
return 0;
}
我运行 编写程序在一个终端中第一个。
touch ftok && gcc foo.c -o foo && ./foo
当 writer 程序还在 运行ning 时,我 运行 在另一个终端中 reader 程序。
gcc -O1 bar.c -o bar && ./bar
reader 程序进入死循环。看起来优化器已经优化了下面的代码
while (strncmp((char *) mem, "exit", 4) != 0)
;
至
while (1)
;
因为在 mem
读取一次数据后,它在循环中看不到任何可以修改数据的内容。
但正是出于这个原因,我将 mem
声明为 volatile
;以防止编译器对其进行优化。
volatile char *mem;
为什么编译器仍然优化掉 mem
的读取?
顺便说一句,我找到了一个有效的解决方案。有效的解决方案是修改
while (strncmp((char *) mem, "exit", 4) != 0)
;
至
while (mem[0] != 'e' || mem[1] != 'x' || mem[2] != 'i' || mem[3] != 't')
;
为什么编译器会优化掉 strncmp((char *) mem, "exit", 4) != 0
而不会优化掉 mem[0] != 'e' || mem[1] != 'x' || mem[2] != 'i' || mem[3] != 't'
,即使在这两种情况下 char *mem
都被声明为 volatile
?
6.7.3 Type qualifiers
6 [...] If an attempt is made to refer to an object defined with a volatile-qualified type through use of an lvalue with non-volatile-qualified type, the behavior is undefined.133)
133) This applies to those objects that behave as if they were defined with qualified types, even if they are never actually defined as objects in the program (such as an object at a memory-mapped input/output address).
这正是您在代码中观察到的。编译器基本上是在“行为无论如何都未定义”的狂野自由下优化你的代码。
换句话说,不可能将 strncmp
直接应用于易失性数据。
你可以做的是实现你自己的比较,不丢弃 volatile
限定符(你已经这样做了),或者使用一些易失性感知方法将易失性数据复制到非volatile 存储,他们将 strncmp
应用于后者。
通过编写 (char *)mem
,您告诉 strncmp
函数它实际上不是易失性缓冲区。事实上,strncmp
和其他 C 库函数并非设计用于易失性缓冲区。
您实际上需要修改您的代码以不在易失性缓冲区上使用 C 库函数。您的选择包括:
- 编写您自己的 C 库函数替代方案,该函数适用于易失性缓冲区。
- 使用适当的内存屏障。
您选择了第一个选项;但是想想如果另一个进程在你的四次读取之间修改了内存会发生什么。为避免此类问题,您需要使用第二个选项,即进程间内存屏障——在这种情况下,缓冲区不再需要 volatile
并且您可以返回使用 C 库函数. (编译器必须假定屏障检查可能会更改缓冲区)。
这是一个将数据写入共享内存的程序foo.c
。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key;
int shmid;
char *mem;
if ((key = ftok("ftok", 0)) == -1) {
perror("ftok");
return 1;
}
if ((shmid = shmget(key, 100, 0600 | IPC_CREAT)) == -1) {
perror("shmget");
return 1;
}
printf("key: 0x%x; shmid: %d\n", key, shmid);
if ((mem = shmat(shmid, NULL, 0)) == (void *) -1) {
perror("shmat");
return 1;
}
sprintf(mem, "hello");
sleep(10);
sprintf(mem, "exit");
return 1;
}
这是另一个程序 bar.c
从同一共享内存中读取数据。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main()
{
key_t key;
int shmid;
volatile char *mem;
if ((key = ftok("ftok", 0)) == -1) {
perror("ftok");
return 1;
}
if ((shmid = shmget(key, sizeof (int), 0400 | IPC_CREAT)) == -1) {
perror("shmget");
return 1;
}
printf("key: 0x%x; shmid: %d\n", key, shmid);
if ((mem = shmat(shmid, NULL, 0)) == (void *) -1) {
perror("shmat");
return 1;
}
printf("looping ...\n");
while (strncmp((char *) mem, "exit", 4) != 0)
;
printf("exiting ...\n");
return 0;
}
我运行 编写程序在一个终端中第一个。
touch ftok && gcc foo.c -o foo && ./foo
当 writer 程序还在 运行ning 时,我 运行 在另一个终端中 reader 程序。
gcc -O1 bar.c -o bar && ./bar
reader 程序进入死循环。看起来优化器已经优化了下面的代码
while (strncmp((char *) mem, "exit", 4) != 0)
;
至
while (1)
;
因为在 mem
读取一次数据后,它在循环中看不到任何可以修改数据的内容。
但正是出于这个原因,我将 mem
声明为 volatile
;以防止编译器对其进行优化。
volatile char *mem;
为什么编译器仍然优化掉 mem
的读取?
顺便说一句,我找到了一个有效的解决方案。有效的解决方案是修改
while (strncmp((char *) mem, "exit", 4) != 0)
;
至
while (mem[0] != 'e' || mem[1] != 'x' || mem[2] != 'i' || mem[3] != 't')
;
为什么编译器会优化掉 strncmp((char *) mem, "exit", 4) != 0
而不会优化掉 mem[0] != 'e' || mem[1] != 'x' || mem[2] != 'i' || mem[3] != 't'
,即使在这两种情况下 char *mem
都被声明为 volatile
?
6.7.3 Type qualifiers
6 [...] If an attempt is made to refer to an object defined with a volatile-qualified type through use of an lvalue with non-volatile-qualified type, the behavior is undefined.133)
133) This applies to those objects that behave as if they were defined with qualified types, even if they are never actually defined as objects in the program (such as an object at a memory-mapped input/output address).
这正是您在代码中观察到的。编译器基本上是在“行为无论如何都未定义”的狂野自由下优化你的代码。
换句话说,不可能将 strncmp
直接应用于易失性数据。
你可以做的是实现你自己的比较,不丢弃 volatile
限定符(你已经这样做了),或者使用一些易失性感知方法将易失性数据复制到非volatile 存储,他们将 strncmp
应用于后者。
通过编写 (char *)mem
,您告诉 strncmp
函数它实际上不是易失性缓冲区。事实上,strncmp
和其他 C 库函数并非设计用于易失性缓冲区。
您实际上需要修改您的代码以不在易失性缓冲区上使用 C 库函数。您的选择包括:
- 编写您自己的 C 库函数替代方案,该函数适用于易失性缓冲区。
- 使用适当的内存屏障。
您选择了第一个选项;但是想想如果另一个进程在你的四次读取之间修改了内存会发生什么。为避免此类问题,您需要使用第二个选项,即进程间内存屏障——在这种情况下,缓冲区不再需要 volatile
并且您可以返回使用 C 库函数. (编译器必须假定屏障检查可能会更改缓冲区)。