[英]Why is discarding the volatile qualifier in a function call a warning?
[英]Discarding the volatile qualifier in a later part of the C code
我有以下代码构造
int foo( volatile int *a)
{
if(*a != VALID)
{
// suspend for few seconds
suspend();
// check again
if(*a != VALID)
{
//error. data was unavailable
return -1;
}
}
// cast away the volatile. Does not accept volatile
call_external_interface((int*)a) ;
return 0;
}
这里由“a”指向的 memory 位置是从外部源(通过 DMA 传输)填充的。
一旦可用,“a”指向的缓冲区就会通过不接受“volatile”的外部接口调用进行处理,因此会丢弃“volatile”。
假设只有在缓冲区被外部资源填充之前才需要“易失性”,因此无论数据是否可用,都会检查 memory 位置两次(并且指令不会被丢弃,因为它被声明为易失性)。
假设也是一旦缓冲区可用,它只是在内部处理,因此可以丢弃 volatile。
我可以确定只有在执行之前编码的指令之后才调用“call_external_interface”吗? 我需要一个明确的 memory 屏障吗?
处理器可以乱序执行。
关于如何使此代码安全的任何其他评论?
C 标准邀请实现提供与客户最佳服务一样强或弱的volatile
语义(它明确将此类语义视为“实现定义的”)。 MSVC 历来使用的语义足够强大,可用于广泛的用途(后来被 Java 或 C# 等语言的设计者认为是有用的),并且仍然可以选择使用该行为。 在 MSVC 语义下,预计此代码的行为会很有用。
不幸的是,该标准没有提供程序员可以指示涉及非限定左值的操作不得在对volatile
限定左值的访问中重新排序的方法,并且一些编译器将更高的优先级放在代码的效率上,这可以通过弱语义(在易失性访问之间自由地重新排序volatile
访问),而不是最大化与为各种任务编写的代码的兼容性(将volatile
访问视为对不透明函数的调用,或将读取和写入视为具有获取/释放语义)。
C 标准中的任何内容都不会禁止实现在foo
执行的*a
易失性读取之前重新排序由call_external_interface
执行的volatile
性限定的*a
读取。 如果foo
在循环中被调用,那么“聪明”的编译器可能会提升所有在call_external_interface
中发生的对*a
的访问,以便它们只发生一次,而不是在循环的每次迭代中重复。 使代码可靠工作的唯一方法是在对*a
的 volatile 访问和任何非限定访问之间调用宏COMPILER_ORDERING_BARRIER
,并要求构建程序的人以适合所用编译器的方式定义宏。 请注意,在这种情况下,无需插入硬件 memory 屏障,并且一些编译器(如 MSVC)能够有效地处理代码,而无需任何编译器特定的语法,但其他编译器(如 gcc 或 Z2C55173DB7BC39754FFA4)据我所知,除了-O0
之外没有其他优化选项会导致它们使用与 MSVC 兼容的语义处理代码。
目前还不太清楚,您的volatile int *a
实际上也指向什么(DMA TCD 或 memory,其中数据也由 TCD.DESTADDR 中定义的 DMA 传输),以及谁在调用 foo()。
如果有 DMA 传输,您应该对 DMA 事件(例如主循环的 ISR 完成)做出反应以处理数据,而不是 DMA 传输的内容。 查看未完成的 DMA 传输的内容没有多大意义。
假设只有在缓冲区被外部资源填充之前才需要“易失性”,因此无论数据是否可用,都会检查 memory 位置两次(并且指令不会被丢弃,因为它被声明为易失性)。
假设也是一旦缓冲区可用,它只是在内部处理,因此可以丢弃 volatile。
C 语言规范定义的语义不支持这些假设。 它们可能适用于任何特定实现,也可能不适用,但如果您打算依赖 C 实现的详细信息,那么您需要找到一种方法来确定该实现提供的保证,超出语言规范,您可以依赖之上。 不受支持的假设会给人们带来麻烦。
我可以确定只有在执行之前编码的指令之后才调用“call_external_interface”吗?
一般来说,只要可观察的行为(包括 volatile 访问的模式)不变,C 规范中的任何内容都不会阻止其他操作围绕 volatile 访问重新排序或以其他方式优化。 特别是,如果suspend()
function 对执行环境没有副作用,并且编译器有办法知道这一点,那么对它的调用可能会被省略。
假设编译器不能证明对call_external_interface()
的调用对环境没有副作用,但是, *a
的第一次读取肯定会首先执行。 如果该读取的结果表明如此,则也将执行第二次读取。 如果执行第二次读取并且其结果在if
语句中表达的任何条件下测试无效,则 function 将在不调用call_external_interface()
的情况下返回 -1。 为了表现出您的代码描述的外部可观察行为所需的一切。
我需要一个明确的 memory 屏障吗?
我看不出你认为会得到什么。
关于如何使此代码安全的任何其他评论?
这取决于您所说的“安全”是什么意思,并且从这个角度来看,取决于suspend()
和call_external_interface()
函数的作用。 但是,强制类型转换通常具有轻微的不良代码气味,并且丢弃指针目标的类型限定符非常臭。 如果call_external_interface()
不接受(指向)易失性数据并且无法修改为这样做,那么最安全的解决方案是foo()
读取易失性数据并将其缓冲在非易失性存储中以供call_external_interface()
使用.
或者,可能是 C 对于您想要表达的细节来说太高级了。 您可以考虑在程序集中重写foo()
以更好地控制访问 memory 的方式和时间。
在许多情况下,这是一个非常糟糕的主意,我们应该避免它。 例子
void foo(volatile int *p)
{
while(*p != 10);
}
void bar(int *p)
{
while(*p != 10);
}
int goo(volatile int *p)
{
foo(p);
}
int goo1(volatile int *p)
{
bar((int *)p);
}
foo:
.L2:
ldr r3, [r0]
cmp r3, #10
bne .L2
bx lr
bar:
ldr r3, [r0]
cmp r3, #10
bxeq lr
.L8:
b .L8
goo:
.L10:
ldr r3, [r0]
cmp r3, #10
bne .L10
bx lr
goo1:
ldr r3, [r0]
cmp r3, #10
bxeq lr
.L15:
b .L15
在上面没有 volatile 的示例中,程序以死循环结束。
或者
void delay(unsigned delay)
{
while(delay--);
}
int foo(int *a)
{
int x = *a;
delay(1000);
return x + *a;
}
int bar(volatile int *a)
{
int x = *a;
delay(1000);
return x + *a;
}
int goo(volatile int *a)
{
return foo((int *)a);
}
int goo1(volatile int *a)
{
return bar(a);
}
delay:
cmp r0, #0
sub r0, r0, #1
bxeq lr
.L3:
subs r0, r0, #1
bxcc lr
subs r0, r0, #1
bxcc lr
b .L3
foo:
mov r3, #1000
ldr r0, [r0]
.L10:
subs r3, r3, #1
bne .L10
lsl r0, r0, #1
bx lr
bar:
mov r3, #1000
ldr r2, [r0]
.L13:
subs r3, r3, #1
bne .L13
ldr r0, [r0]
add r0, r0, r2
bx lr
goo:
mov r3, #1000
ldr r0, [r0]
.L16:
subs r3, r3, #1
bne .L16
lsl r0, r0, #1
bx lr
goo1:
mov r3, #1000
ldr r2, [r0]
.L19:
subs r3, r3, #1
bne .L19
ldr r0, [r0]
add r0, r2, r0
bx lr
在这个没有 volatile 的示例中,object 只被读取一次 - 但它可以由 DMA 在后台更改。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.