【问题标题】:Volatile and multiple reads by compiler编译器的易失性和多次读取
【发布时间】:2021-03-26 00:48:14
【问题描述】:

我正在阅读 kernel/timer.c 文件的一部分,并遇到了使用 READ_ONCE 的这一部分 以及一条注释,指出使用 READ_ONCE() 可以防止编译器进行多次读取。

/*
         * We need to use READ_ONCE() here, otherwise the compiler
         * might re-read @tf between the check for TIMER_MIGRATING
         * and spin_lock().
         */
        tf = READ_ONCE(timer->flags);

        if (!(tf & TIMER_MIGRATING)) {
            base = get_timer_base(tf);
            raw_spin_lock_irqsave(&base->lock, *flags);
            if (timer->flags == tf)
                return base;

然后我在 SO 上找到了这个问题: Can a compiler read twice from a global variable, instead of storing a local one?

这表明编译器确实会多次读取全局变量。 但是,在这种情况下,共享全局变量被分配给局部非易失性变量。 根据question: $tf 也不能省略。 因此,由于他们将易失性变量(标志)分配给非易失性变量($tf),因此易失性的属性是否也转移到非易失性变量($tf),因此也可以防止 $tf重读?

【问题讨论】:

  • So since they are assigning a volatile variable (flags)flags 不是易变的。 flags 一个普通的u32
  • #define __READ_ONCE(x) (*(const volatile __unqual_scalar_typeof(x) *)&(x))
  • 是的,但是 flags 不是易变的。如果你会做tf = timer->flags,那么编译器可能会多次读取它。我不明白,如果多次读取tf,它具有相同的常量值,这不是问题。多次读取timer->flags是有问题的,因为两次读取之间可能会被另一个线程异步修改。
  • @KamilCuk 是的,我的问题来自代码中的评论,如果你去 timer.c 链接,他们有一个 __acquire(timer->lock),我不知道如果它确实获得了锁,因为如果它确实获得了锁,那么计时器->标志将不会在其他地方被修改。
  • 不,它没有。那完全是另一回事。它用于分析内核代码。而且是在{}之前,不在函数体中!

标签: c linux linux-kernel compiler-optimization volatile


【解决方案1】:

所以因为他们分配了一个 volatile 变量(标志)

flags 不是volatile。使用volatile 访问访问。请注意,“volatile”是不允许编译器优化的关键字——仅此而已。

到非易失性变量($tf),是否也将易失性的属性转移到非易失性变量($tf),这样$tf也可以防止重读?

不,tf 是一个普通变量,它可能被多次读取或写入。没有所谓的“转让财产”。

如果你会写:

   u32 tf;
   tf = timer->flags;
    if (!(tf & TIMER_MIGRATING)) {
        base = get_timer_base(tf);
        raw_spin_lock_irqsave(&base->lock, *flags);
        if (timer->flags == tf)
            return base;

那么变量tf可以优化出来,代码可以优化为:

    if (!(timer->flags & TIMER_MIGRATING)) {
        // another thread may asynchronously modify timer->flags here
        base = get_timer_base(timer->flags);
        raw_spin_lock_irqsave(&base->lock, *flags);
        // save here - we are in irqsave section
        if (timer->flags == timer->flags) // could be optimized further...
            return base;

因为timer->flags 上的操作 是通过volatile 限定句柄完成的,它无法优化,它必须准确地发生在你想要的地方- 在检查必须完成之前,只有一次,在其他任何事情和所有副作用(即其他易失性写入和读取)之前。

【讨论】:

  • 示例中错过了unlock() 调用。
【解决方案2】:

volatile 变量必须在使用时多次读取。非易失性变量不必(如果您启用优化)。

volatile int x;
int y;

int foo(void)
{
    return x + x + x;
}

int bar(void)
{
    return y + y + y;
}
foo:
        mov     eax, DWORD PTR x[rip]
        mov     ecx, DWORD PTR x[rip]
        mov     edx, DWORD PTR x[rip]
        add     eax, ecx
        add     eax, edx
        ret
bar:
        mov     eax, DWORD PTR y[rip]
        lea     eax, [rax+rax*2]
        ret

https://godbolt.org/z/P4aT47

【讨论】:

  • 感谢编译器游乐场链接,我检查了链接中的以下代码,第一个没有 const volatile *,godbolt.org/z/nKG96r,接下来是 const volatile *,godbolt.org/z/vM55MM,前面的代码跳过了比较,所以 $ts 也表现为 volatile
猜你喜欢
  • 2012-12-19
  • 2018-01-18
  • 2013-05-19
  • 2019-03-06
  • 1970-01-01
  • 1970-01-01
  • 2016-04-04
  • 1970-01-01
  • 2011-01-26
相关资源
最近更新 更多