在嵌入式C开发中使用volatile


44

我一直在阅读一些文章,并在Stack Exchange的答案中找到有关使用volatile关键字来防止编译器对可能以编译器无法确定的方式更改的对象进行任何优化的答案。

如果我正在从ADC读取数据(我们将其称为变量adcValue),并且将该变量声明为全局变量,那么volatile在这种情况下是否应该使用关键字?

  1. 不使用volatile关键字

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    
  2. 使用volatile关键字

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    volatile uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    

我问这个问题是因为在调试时,虽然最佳实践表明(在我的情况下(直接从硬件更改的全局变量))volatile是强制性的,但我看不出两种方法之间的区别。


1
许多调试环境(一定是gcc)没有进行优化。生产版本通常会(取决于您的选择)。这可能导致版本之间的“有趣”差异。查看链接器输出映射是有用的。
彼得·史密斯

22
“就我而言(直接从硬件更改的全局变量)”-全局变量不会由硬件更改而只能由编译器知道的C代码更改。-ADC提供其结果的硬件寄存器必须是易失的,因为编译器无法知道其值是否/何时改变(如果/何时ADC硬件完成转换,它就会改变。)
JimmyB

2
您是否比较了两个版本生成的汇编程序?那应该告诉你
幕后

3
@stark:BIOS?在微控制器上?通过缓存规则和内存映射之间的设计一致性,内存映射的I / O空间将是不可缓存的(如果架构甚至首先具有数据缓存,则无法保证)。但是,volatile与内存控制器缓存无关。
Ben Voigt

1
@Davislor语言标准一般不需要多说什么。读易失性对象将执行实际加载(即使编译器最近做了一次加载并且通常会知道该值是什么),而对此类对象的写操作将执行真实存储(即使从对象中读取了相同的值) )。因此,if(x==1) x=1;写操作可以针对非易失性x进行优化,如果x是易失性则无法进行优化。OTOH如果需要特殊的指令来访问外部设备,则由您自己决定添加这些设备(例如,如果需要使存储范围为直写形式)。
curiousguy

Answers:


87

的定义 volatile

volatile告诉编译器变量的值可能会在编译器不知道的情况下更改。因此,编译器不能仅仅因为C程序似乎没有更改它就不能假定该值没有改变。

另一方面,这意味着在编译器不知道的其他地方可能需要(读取)变量的值,因此必须确保对变量的每个赋值实际上都是作为写操作执行的。

用例

volatile 何时需要

  • 将硬件寄存器(或内存映射的I / O)表示为变量-即使永远不会读取寄存器,编译器也不能只是跳过写操作,认为“愚蠢的程序员。试图将值存储在他/她所用的变量中永远都不会回读。如果我们忽略了写作,他甚至不会注意到。” 相反,即使程序从不向变量写入值,硬件也可能会更改其值。
  • 在执行上下文(例如ISR /主程序)之间共享变量(请参阅@kkramo的答案)

的影响 volatile

声明变量后volatile,编译器必须确保程序代码中对该变量的每个赋值均反映在实际的写操作中,并且每次程序代码中的读取均从(映射的)内存中读取该值。

对于非易失性变量,编译器假定它知道是否/何时改变了变量的值,并可以以不同的方式优化代码。

首先,编译器可以通过将值保留在CPU寄存器中来减少对内存的读/写次数。

例:

void uint8_t compute(uint8_t input) {
  uint8_t result = input + 2;
  result = result * 2;
  if ( result > 100 ) {
    result -= 100;
  }
  return result;
}

在这里,编译器可能甚至不会为result变量分配RAM ,并且永远不会将中间值存储在CPU寄存器中的任何位置。

如果result是易失性的,每次result在C代码中出现C都将要求编译器执行对RAM(或I / O端口)的访问,从而导致性能降低。

其次,编译器可以针对性能和/或代码大小对非易失性变量重新排序。简单的例子:

int a = 99;
int b = 1;
int c = 99;

可以重新订购

int a = 99;
int c = 99;
int b = 1;

这可以节省汇编指令,因为该值99不必两次加载。

如果abc呈震荡编译器将不得不为发出,因为它们是在程序给出指定的确切顺序值的说明。

另一个经典示例是这样的:

volatile uint8_t signal;

void waitForSignal() {
  while ( signal == 0 ) {
    // Do nothing.
  }
}

如果signal不是volatile,编译器将“认为”这while( signal == 0 )可能是一个无限循环(因为该循环内的signal代码将永远不会对其进行更改),并且可能会生成等效的

void waitForSignal() {
  if ( signal != 0 ) {
    return; 
  } else {
    while(true) { // <-- Endless loop!
      // do nothing.
    }
  }
}

妥善处理volatile价值

如上所述,当volatile变量被访问的次数比实际需要的次数多时,可能会导致性能下降。为了缓解此问题,您可以通过将值分配给非易失性变量来使值“不变”,例如

volatile uint32_t sysTickCount;

void doSysTick() {
  uint32_t ticks = sysTickCount; // A single read access to sysTickCount

  ticks = ticks + 1; 

  setLEDState( ticks < 500000L );

  if ( ticks >= 1000000L ) {
    ticks = 0;
  }
  sysTickCount = ticks; // A single write access to volatile sysTickCount
}

这在ISR中尤其有用,因为在ISR中,知道不需要时,希望尽快不多次访问相同的硬件或内存,因为ISR运行时值不会改变。当ISR是变量值的“生产者”时,这很常见,sysTickCount如上例所示。在AVR上,让函数doSysTick()访问内存中相同的四个字节(四个指令=每次访问需要8个CPU周期sysTickCount)五到六次而不是只有两次是特别痛苦的,因为程序员确实知道该值不会在他/她doSysTick()运行时从其他代码更改。

使用此技巧,您实际上可以对非易失性变量执行与编译器完全相同的操作,即仅在必须时才从内存中读取它们,将值保留在寄存器中一段时间​​,然后仅在必须时将其写回内存中。 ; 但是这一次,是否/何时必须进行读/写比编译器更了解,因此您可以使编译器免于执行此优化任务,并自己完成。

局限性 volatile

非原子访问

volatile没有提供多字变量的原子访问。对于这些情况,除了使用之外,您还需要通过其他方式提供互斥volatile。在AVR,您可以使用ATOMIC_BLOCK<util/atomic.h>或简单的cli(); ... sei();电话。各个宏也充当内存屏障,这在访问顺序方面很重要:

执行顺序

volatile仅对其他易失变量施加严格的执行顺序。这意味着,例如

volatile int i;
volatile int j;
int a;

...

i = 1;
a = 99;
j = 2;

保证首先给分配1 i然后给分配2 j。但是,不能保证a在两者之间进行分配。编译器可以在代码段之前或之后进行分配,基本上可以在任何时候进行,直到第一次读取(可见)a

如果不是因为上述宏的内存障碍,编译器将被允许翻译

uint32_t x;

cli();
x = volatileVar;
sei();

x = volatileVar;
cli();
sei();

要么

cli();
sei();
x = volatileVar;

(为了完整起见,我必须说volatile,如果所有访问都用这些障碍括起来,那么像sei / cli宏所隐含的那些内存障碍实际上可能会避免使用。)


7
很好地讨论了性能的挥发:)
awjlogan '18

3
我总是喜欢在ISO / IEC 9899:1999 6.7.3(6)中提到volatile的定义: An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. 更多的人应该阅读它。
Jeroen3 '18

3
可能值得一提的是,如果您的唯一目标是实现内存屏障而不是防止中断,则cli/ sei解决方案过于繁琐。这些宏生成实际的cli/ sei指令,以及另外的缓冲存储器,正是这种缓冲导致了障碍。要只具有一个内存屏障而不禁用中断,您可以使用主体定义自己的宏__asm__ __volatile__("":::"memory")(例如,带有内存破坏符的空汇编代码)。
罗斯兰

3
@NicHartley C17 5.1.2.3§6定义了可观察到的行为:“严格根据抽象机的规则来评估对易失对象的访问。” C标准并不能真正确定总体上需要在哪些内存障碍。在使用该表达式的末尾volatile有一个序列点,其后的所有内容都必须“在此之后进行序列化”。这意味着表达各种各样的记忆障碍。编译器供应商选择散布各种神话,以将内存障碍的责任归咎于程序员,但这违反了“抽象机”的规则。
伦丁

2
@JimmyB本地volatile对于类似的代码可能有用volatile data_t data = {0}; set_mmio(&data); while (!data.ready);
Maciej Piechotka '18

13

volatile关键字告诉编译器对变量的访问具有明显的效果。这意味着每次您的源代码使用该变量时,编译器必须创建对变量的访问。是读或写访问。

这样的效果是,代码也会在正常代码流之外对变量进行任何更改。例如,如果中断处理程序更改了该值。或者,如果变量实际上是一些自己更改的硬件寄存器。

这个巨大的好处也是它的缺点。每次对变量的访问都会遍历该变量,并且该值永远不会保存在寄存器中,以便在任何时间段内都能更快地进行访问。这意味着一个可变变量将很慢。幅度较慢。因此,仅在实际需要的地方使用挥发物。

就您而言,就您显示的代码而言,仅当您自己通过对其进行更新时,才更改全局变量adcValue = readADC();。编译器知道何时会发生这种情况,并且永远不会在可能调用该readFromADC()函数的内容中将adcValue的值保存在寄存器中。或任何它不知道的功能。或将操纵可能指向adcValue此类的指针的任何内容。确实不需要volatile,因为变量永远不会以不可预测的方式变化。


6
我同意这个答案,但“幅度较慢”听起来太可怕了。
kkrambo

6
在现代超标量CPU上,可以在不到cpu的周期内访问CPU寄存器。另一方面,对实际未缓存内存的访问(请记住某些外部硬件会对此进行更改,因此不允许CPU缓存)可以在100-300个CPU周期内。所以,是的,数量级。在AVR或类似的微控制器上不会很糟糕,但是问题并没有说明硬件。
Goswin von Brederlow,

7
在嵌入式(微控制器)系统中,RAM访问的代价通常要少得多。例如,AVR仅需两个CPU周期即可读取或写入RAM(一次寄存器到寄存器的移动需要一个周期),因此将内容保存在寄存器中所节省的成本接近(但从未达到)最大值。每次访问2个时钟周期。-当然,相对而言,将值从寄存器X保存到RAM,然后立即将该值重新加载到寄存器X中进行进一步计算将花费2x2 = 4而不是0个周期(当仅将值保留在X中时),因此是无限的慢一点:)
JimmyB

1
在“写入或读取特定变量”的上下文中,“幅度变慢”,是的。但是,在一个完整程序的上下文中,它可能比一遍又一遍地对一个变量进行读/写操作要重要得多,不,不是。在那种情况下,整体差异可能“很小到可以忽略”。在对性能进行断言时,应注意弄清楚断言是与某个特定操作还是整个程序有关。将不常用的操作速度降低300倍几乎不是什么大问题。
aroth

1
你的意思是,最后一句话?从“过早的优化是万恶之源”的意义上讲,这意味着更多。显然,您不应该仅仅因为来使用volatile所有东西,但是如果您由于先发性能问题而认为合法地要求使用它,那么也不要回避它。
aroth

9

嵌入式C应用程序中volatile关键字的主要用途是标记在中断处理程序中写入的全局变量。在这种情况下,它当然不是可选的。

没有它,编译器将无法证明在初始化之后就已经写入了该值,因为它无法证明曾经调用过中断处理程序。因此,它认为可以优化不存在的变量。


2
当然还有其他实际用途,但是恕我直言,这是最常见的。
vicatcu

1
如果仅在ISR中读取该值(并从main()中更改),则可能还必须使用volatile来保证对多字节变量进行ATOMIC访问。
Rev1.0

15
@ Rev1.0不,volatile 保证具有芳香性。必须单独解决该问题。
克里斯·斯特拉顿

1
没有从硬件读取的内容,也没有发布代码中的任何中断。您正在假设问题中没有的事情。不能真正以当前形式回答它。
伦丁'18

3
“不标记在中断处理程序中写入的全局变量”。是为了标记一个变量;全局或其他;它可能会被编译器无法理解的某些内容更改。不需要中断。它可以是共享内存,也可以是有人对内存进行探查(不建议将其用于超过40年的更新)
UKMonkey

9

在两种情况下,必须volatile在嵌入式系统中使用。

  • 从硬件寄存器读取时。

    这就是说,内存映射寄存器本身就是MCU内部硬件外设的一部分。它可能会具有一些神秘的名称,例如“ ADC0DR”。必须使用工具供应商或您自己提供的一些寄存器映射表,以C代码定义此寄存器。要自己做,您可以做(假设使用16位寄存器):

    #define ADC0DR (*(volatile uint16_t*)0x1234)

    其中0x1234是MCU映射寄存器的地址。由于volatile已经是上述宏的一部分,因此对其的任何访问都将是volatile限定的。所以这段代码很好:

    uint16_t adc_data;
    adc_data = ADC0DR;
    
  • 使用ISR的结果在ISR和相关代码之间共享变量时。

    如果您有这样的事情:

    uint16_t adc_data = 0;
    
    void adc_stuff (void)
    {
      if(adc_data > 0)
      {
        do_stuff(adc_data);
      } 
    }
    
    interrupt void ADC0_interrupt (void)
    {
      adc_data = ADC0DR;
    }
    

    然后,编译器可能会认为:“ adc_data始终为0,因为它不会在任何地方更新。并且永远不会调用ADC0_interrupt()函数,因此无法更改该变量”。编译器通常不会意识到中断是由硬件而不是软件调用的。因此,编译器去掉并删除了代码,if(adc_data > 0){ do_stuff(adc_data); }因为它认为它永远不可能是真的,从而导致了一个非常奇怪且难以调试的错误。

    通过声明adc_data volatile,不允许编译器做出任何此类假设,也不允许优化对变量的访问。


重要笔记:

  • ISR必须始终在硬件驱动程序中声明。在这种情况下,ADC ISR应该位于ADC驱动器内部。除驱动程序外,其他任何人都不应与ISR进行通信-其他所有操作都是意大利面条式编程。

  • 编写C时,必须保护ISR与后台程序之间的所有通信免受竞争条件的影响。始终,每次都没有例外。MCU数据总线的大小无关紧要,因为即使您用C语言执行一个8位拷贝,该语言也不能保证操作的原子性。除非您使用C11功能_Atomic。如果此功能不可用,则必须使用某种信号量或在读取等过程中禁用中断。内联汇编程序是另一种选择。volatile不保证原子性。

    可能发生的情况是:-将
    堆栈中的值
    加载到寄存器中-发生中断-
    使用寄存器中的值

    然后,“使用值”部分本身是否为一条指令也没关系。可悲的是,所有嵌入式系统程序员中有很大一部分人对此一无所知,这可能使其成为有史以来最常见的嵌入式系统错误。总是断断续续,很难挑衅,很难找到。


正确编写ADC驱动程序的示例如下所示(假设C11 _Atomic不可用):

adc.h

// adc.h
#ifndef ADC_H
#define ADC_H

/* misc init routines here */

uint16_t adc_get_val (void);

#endif

交流电

// adc.c
#include "adc.h"

#define ADC0DR (*(volatile uint16_t*)0x1234)

static volatile bool semaphore = false;
static volatile uint16_t adc_val = 0;

uint16_t adc_get_val (void)
{
  uint16_t result;
  semaphore = true;
    result = adc_val;
  semaphore = false;
  return result;
}

interrupt void ADC0_interrupt (void)
{
  if(!semaphore)
  {
    adc_val = ADC0DR;
  }
}
  • 该代码假定中断本身不能被中断。在这样的系统上,一个简单的布尔值可以充当信号量,并且不必是原子的,因为如果在设置布尔值之前发生中断,就不会造成任何危害。上述简化方法的缺点是,在发生竞争情况时,它将使用先前的值代替ADC读取。也可以避免这种情况,但是随后代码变得更加复杂。

  • 这里volatile可以防止优化错误。它与硬件寄存器中的数据无关,仅与ISR共享数据有关。

  • static通过使变量位于驱动程序本地,可以防止意大利面条编程和名称空间污染。(这在单核,单线程应用程序中很好,但在多线程应用程序中不是。)


很难调试是相对的,如果删除了代码,您会注意到有价值的代码已经消失了-这是一个非常大胆的声明,表明存在问题。但我同意,效果可能非常奇怪且难以调试。
阿森纳

@Arsenal如果您有一个不错的调试器,可以用C内联汇编程序,并且至少了解一点asm,那么可以很容易地发现它。但是对于较大的复杂代码,机器生成的大量asm并非难事。或者,如果您不认识asm。或者,如果您的调试器是废话并且不显示asm(cougheclipsecough)。
伦丁'18

那时使用Lauterbach调试器可能会让我有些生气。如果您尝试在经过优化的代码中设置一个断点,则会将其设置为其他地方,并且您知道那里发生了什么。
阿森纳

@Arsenal Yep,在Lauterbach中可以得到的混合C / asm绝不是标准。如果有的话,大多数调试器会在单独的窗口中显示asm。
伦丁'18

semaphore应该肯定是volatile!事实上,它是最基本的使用情况至极电话volatile:信号的东西从一个执行上下文到另一个。-在您的示例中,编译器可能会省略,semaphore = true;因为它“看到”其值在被覆盖之前从未被读取semaphore = false;
JimmyB

5

在问题中提出的代码段中,还没有使用volatile的理由。值是否adcValue来自ADC 无关紧要。而adcValue作为全球应该让你怀疑是否adcValue应该波动,但它不是一个理由本身。

全局性是一个线索,因为它打开了adcValue可以从多个程序上下文访问的可能性。程序上下文包括中断处理程序和RTOS任务。如果全局变量被一个上下文更改,则其他程序上下文无法假定它们知道先前访问的值。每个上下文每次使用变量值时都必须重新读取该变量值,因为该值可能已在其他程序上下文中更改。程序上下文不知道何时发生中断或任务切换,因此它必须假定,由于可能的上下文切换,多个上下文使用的任何全局变量可能在变量的任何访问之间改变。这就是volatile声明的用途。它告诉编译器该变量可以在您的上下文之外更改,因此每次访问都读取该变量,并且不要假设您已经知道该值。

如果变量被内存映射到硬件地址,则硬件所做的更改实际上是程序上下文之外的另一个上下文。因此,内存映射也是一个线索。例如,如果您的readADC()函数访问一个内存映射的值以获取ADC值,则该内存映射的变量可能应该是易失的。

因此,回到您的问题上,如果您的代码还有更多内容,并且adcValue可以被在不同上下文中运行的其他代码访问,那么是的,adcValue应该是可变的。


4

“直接从硬件更改的全局变量”

仅仅因为该值来自某个硬件ADC寄存器,并不意味着它被硬件“直接”更改。

在您的示例中,您只需调用readADC(),它会返回一些ADC寄存器值。对于编译器来说这很好,因为知道此时已为adcValue分配了新值。

如果您使用ADC中断例程来分配新值,即准备好新的ADC值时调用该值,则情况会有所不同。在这种情况下,编译器不会知道何时调用相应的ISR,可能会决定不会以这种方式访问​​adcValue。这是易挥发的地方。


1
由于您的代码从不“调用” ISR函数,因此编译器会看到该变量仅在没有人调用的函数中更新。因此,编译器对其进行了优化。
斯旺南德'18

1
这取决于其余代码,如果没有在任何地方读取adcValue(例如仅通过调试器读取),或者在一个位置仅读取一次,则编译器可能会对其进行优化。
达米安

2
@Damien:总是“依赖”,但是我的目的是解决实际的问题“在这种情况下我应该使用关键字volatile吗?” 越短越好。
Rev1.0

4

volatile参数的行为在很大程度上取决于代码,编译器和完成的优化。

我个人使用了两种用例volatile

  • 如果有一个我想用调试器查看的变量,但是编译器已经对其进行了优化(意味着已经删除了它,因为它发现没有必要使用该变量),添加volatile将迫使编译器保留它,因此可以在调试时看到。

  • 如果变量可能会“超出代码范围”更改,通常是在您有一些硬件访问它或将变量直接映射到地址时。

在嵌入式系统中,有时编译器中也会出现很多错误,无法进行优化,有时甚至volatile可以解决问题。

假设您的变量是全局声明的,只要在代码上使用该变量(至少是在读取和读取的情况下),就可能不会对其进行优化。

例:

void test()
{
    int a = 1;
    printf("%i", a);
}

在这种情况下,变量可能会被优化为printf(“%i”,1);

void test()
{
    volatile int a = 1;
    printf("%i", a);
}

不会被优化

另一个:

void delay1Ms()
{
    unsigned int i;
    for (i=0; i<10; i++)
    {
        delay10us( 10);
    }
}

在这种情况下,编译器可能会通过优化(如果您针对速度进行优化),从而丢弃该变量

void delay1Ms()
{
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
}

对于您的用例,“它可能取决于”其余的代码,adcValue在其他地方如何使用以及所使用的编译器版本/优化设置。

有时,没有优化但无法优化的代码就会烦人。

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
}

可以将其优化为printf(“%i”,readADC());。

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
  callAnotherFunction(adcValue);
}

-

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
}

void anotherFunction()
{
   // Do something with adcValue
}

这些可能不会进行优化,但是您永远不会知道“编译器有多好”,并且可能会随编译器参数的变化而变化。通常,具有良好优化的编译器已获得许可。


1
例如a = 1; b = a; 和c = b; 编译器可能会想一会儿,a和b没有用,我们直接将1放到c中即可。当然,您不会在代码中这样做,但是编译器比发现这些代码要好,而且如果您尝试立即编写优化的代码,则将难以理解。
达米安

2
使用正确的编译器的正确代码不会在打开优化的情况下中断。编译器的正确性有点问题,但是至少对于IAR,我还没有遇到过优化导致代码中断的情况。
阿森纳

5
在很多情况下,优化也会破坏代码,这是当您也涉足UB领域
管道

2
是的,volatile的副作用是它可以帮助调试。但这不是使用volatile的充分理由。如果您的目标是轻松调试,则可能应该关闭优化。这个答案甚至没有提到中断。
kkrambo

2
添加到调试参数后,将volatile强制编译器将变量存储在RAM中,并在将值分配给变量后立即更新该RAM。在大多数情况下,编译器不会“删除”变量,因为我们通常不会写没有效果的赋值,但是编译器可能会决定将变量保留在某些CPU寄存器中,并且可能以后再也不会将该寄存器的值写入RAM。调试器通常无法找到保存该变量的CPU寄存器,因此无法显示其值。
JimmyB

1

很多技术说明,但我想专注于实际应用。

volatile关键字强制编译器来读取或从存储器中每次使用的时间写变量的值。通常,编译器将尝试优化而不进行不必要的读取和写入,例如通过将值保留在CPU寄存器中,而不是每次都访问内存来进行优化。

这在嵌入式代码中有两个主要用途。首先,它用于硬件寄存器。硬件寄存器可以更改,例如ADC结果寄存器可以由ADC外设写入。硬件寄存器在访问时也可以执行操作。一个常见的示例是UART的数据寄存器,该寄存器通常在读取时清除中断标志。

编译器通常会假设该值永远不会改变,因此无需重复访问,就可以尝试优化寄存器的重复读取和写入操作,但是volatile关键字将强制每次执行读取操作。

第二种常见用法是用于中断代码和非中断代码都使用的变量。不会直接调用中断,因此编译器无法确定何时执行,因此假定中断内的任何访问都不会发生。由于volatile关键字强制编译器每次都访问变量,因此该假设被删除。

重要的是要注意,volatile关键字不能完全解决这些问题,因此必须小心避免它们。例如,在8位系统上,一个16位变量需要两次内存访问才能进行读取或写入,因此,即使编译器被迫进行这些访问,它们也依序发生,并且硬件有可能对第一次访问或两者之间发生中断。


0

在没有volatile限定符的情况下,在代码的某些部分中,对象的值可以存储在多个位置。例如,考虑以下内容:

int foo;
int someArray[64];
void test(void)
{
  int i;
  foo = 0;
  for (i=0; i<64; i++)
    if (someArray[i] > 0)
      foo++;
}

在C的早期,编译器会处理该语句

foo++;

通过以下步骤:

load foo into a register
increment that register
store that register back to foo

但是,更复杂的编译器将认识到,如果“ foo”的值在循环期间保存在寄存器中,则只需在循环之前加载一次,然后再存储一次。但是,在循环过程中,这意味着“ foo”的值将保存在两个位置-全局存储内和寄存器内。如果编译器可以看到循环中可能会访问“ foo”的所有方式,那么这将不是问题,但是如果以某种编译器不知道的机制访问“ foo”的值,则可能会造成麻烦(例如中断处理程序)。

标准的作者可能有可能添加一个新的限定词,该限定词将明确邀请编译器进行此类优化,并说老式的语义将在不存在该语义的情况下适用,但在这种情况下,优化有用的数量大大超过了那些有问题的地方,因此该标准允许编译器在没有证据表明并非如此的前提下,认为这种优化是安全的。volatile关键字的目的是提供此类证据。

在某些情况下,某些编译器编写者和程序员之间会发生一些争论:

unsigned short volatile *volatile output_ptr;
unsigned volatile output_count;

void interrupt_handler(void)
{
  if (output_count)
  {
    *((unsigned short*)0xC0001230) = *output_ptr; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 1; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 0; // Hardware I/O register
    output_ptr++;
    output_count--;
  }
}

void output_data_via_interrupt(unsigned short *dat, unsigned count)
{
  output_ptr = dat;
  output_count = count;
  while(output_count)
     ; // Wait for interrupt to output the data
}

unsigned short output_buffer[10];

void test(void)
{
  output_buffer[0] = 0x1234;
  output_data_via_interrupt(output_buffer, 1);
  output_buffer[0] = 0x2345;
  output_buffer[1] = 0x6789;
  output_data_via_interrupt(output_buffer,2);
}

从历史上看,大多数编译器要么允许写入volatile存储位置可能触发任意副作用的可能性,要么避免在此类存储中缓存寄存器中的任何值,否则它们将避免在对以下函数的调用中缓存寄存器中的值:不合格的“内联”,因此会将0x1234写入output_buffer[0],设置内容以输出数据,等待数据完成,然后将0x2345写入,然后output_buffer[0]从那里继续。该标准不需要实现来处理将地址存储output_buffervolatile-限定的指针,表明可能通过某种方式发生了某种事情,这意味着编译器无法理解,因为作者认为编译器是针对各种平台和目的的编译器的编写者会认识到这样做的目的将在那些平台上满足这些目的无需告知。因此,某些“灵巧”的编译器(例如gcc和clang)将假定,即使将的地址output_buffer写入到的两个存储之间的volatile限定的指针中output_buffer[0],也没有理由假设任何东西都可能关心该对象处的值那时。

此外,虽然直接从整数强制转换的指针除了用于以编译器不太可能理解的方式操纵事物之外,很少用于其他目的,但该标准再次不需要编译器将此类访问视为volatile。因此,*((unsigned short*)0xC0001234)像gcc和clang这样的“聪明”编译器可能会省略首次写入,因为此类编译器的维护者宁愿声明忽略了限定volatile“破损”的代码,也不愿承认这种代码的兼容性是有用的。 。许多供应商提供的头文件都省略了volatile限定符,并且与供应商提供的头文件兼容的编译器比没有供应商的头文件有用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.