哪个场景关键字“易失性”是否需要在objective-c中声明? [英] Which scenes keyword "volatile" is needed to declare in objective-c?

查看:85
本文介绍了哪个场景关键字“易失性”是否需要在objective-c中声明?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

据我所知, volatile 通常用于防止在某些硬件操作期间出现意外的编译优化。但是应该在属性定义中声明哪些场景 volatile 让我感到困惑。请举几个有代表性的例子。

As i know, volatile is usually used to prevent unexpected compile optimization during some hardware operations. But which scenes volatile should be declared in property definition puzzles me. Please give some representative examples.

Thx。

推荐答案

编译器假定变量改变其值的唯一方法是通过改变它的代码。

A compiler assumes that the only way a variable can change its value is through code that changes it.

int a = 24;

现在编译器认为 a 24 直到它看到任何更改 a 值的语句。如果您在上面的语句中写下代码

Now the compiler assumes that a is 24 until it sees any statement that changes the value of a. If you write code somewhere below above statement that says

int b = a + 3;

编译器会说我知道 a 是,它是 24 !所以 b 27 。我不必编写代码来执行该计算,我知道总是 27 。编译器可能只是优化整个计算。

the compiler will say "I know what a is, it's 24! So b is 27. I don't have to write code to perform that calculation, I know that it will always be 27". The compiler may just optimize the whole calculation away.

但是如果 a 之间发生了变化,编译器就会出错分配和计算。但是, a 为什么这样做呢?为什么 a 突然有不同的值?它不会。

But the compiler would be wrong in case a has changed between the assignment and the calculation. However, why would a do that? Why would a suddenly have a different value? It won't.

如果 a 是一个堆栈变量,它不能改变值,除非你传递一个引用它,例如

If a is a stack variable, it cannot change value, unless you pass a reference to it, e.g.

doSomething(&a);

函数 doSomething 有一个指针 a ,这意味着它可以更改 a 的值,在该行代码后, a 可能不再是 24 。所以,如果你写

The function doSomething has a pointer to a, which means it can change the value of a and after that line of code, a may not be 24 any longer. So if you write

int a = 24;
doSomething(&a);
int b = a + 3;

编译器不会优化计算。谁知道在 doSomething 之后 a 会有什么价值?肯定的编译器没有。

the compiler will not optimize the calculation away. Who knows what value a will have after doSomething? The compiler for sure doesn't.

对于全局变量或对象的实例变量,事情变得更加棘手。这些变量不在堆栈上,它们在堆上,这意味着不同的线程可以访问它们。

Things get more tricky with global variables or instance variables of objects. These variables are not on stack, they are on heap and that means that different threads can have access to them.

// Global Scope
int a = 0;

void function ( ) {
    a = 24;
    b = a + 3;
}

b be 27 ?很可能答案是肯定的,但是其他一些线程很可能在这两行代码之间改变了 a 的值,然后它就不会 27 。编译器在乎吗?没有为什么?因为C对线程一无所知 - 至少它不习惯(最新的C标准最终知道本机线程,但之前的所有线程功能只是操作系统提供的API而不是C本机)。所以C编译器仍然会假设 b 27 并优化计算,这可能会导致错误的结果。

Will b be 27? Most likely the answer is yes, but there is a tiny chance that some other thread has changed the value of a between these two lines of code and then it won't be 27. Does the compiler care? No. Why? Because C doesn't know anything about threads - at least it didn't used to (the latest C standard finally knows native threads, but all thread functionality before that was only API provided by the operating system and not native to C). So a C compiler will still assume that b is 27 and optimize the calculation away, which may lead to incorrect results.

这就是 volatile 的好处。如果你像这样标记变量volatile

And that's what volatile is good for. If you tag a variable volatile like that

volatile int a = 0;

你基本上是在告诉编译器: 的值a 可能会随时发生变化。严重的是,它可能会突然发生变化。你看不到它会发生* *爆炸*,它有不同的价值!。对于编译器,这意味着它不能假设 a 具有某个值,因为它曾经有1皮秒前的值,并且没有代码似乎有改变了无所谓。访问 a 时,始终读取其当前值。

you are basically telling the compiler: "The value of a may change at any time. No seriously, it may change out of the blue. You don't see it coming and *bang*, it has a different value!". For the compiler that means it must not assume that a has a certain value just because it used to have that value 1 pico-second ago and there was no code that seemed to have changed it. Doesn't matter. When accessing a, always read its current value.

过度使用volatile会阻止许多编译器优化可能会大大减慢计算代码的速度,并且人们常常在甚至不需要的情况下使用volatile。例如,编译器从不对内存障碍进行值假设。究竟什么是内存障碍?嗯,这远远超出了我的回复范围。您只需要知道典型的同步结构是内存屏障,例如:锁,互斥量或信号量等。考虑以下代码:

Overuse of volatile prevents a lot of compiler optimizations, may slow down calculation code dramatically and very often people use volatile in situations where it isn't even necessary. For example, the compiler never makes value assumptions across memory barriers. What exactly a memory barrier is? Well, that's a bit far beyond the scope of my reply. You just need to know that typical synchronization constructs are memory barriers, e.g. locks, mutexes or semaphores, etc. Consider this code:

// Global Scope
int a = 0;

void function ( ) {
    a = 24;
    pthread_mutex_lock(m);
    b = a + 3;
    pthread_mutex_unlock(m);
}

pthread_mutex_lock 是一个内存障碍(顺便说一下, pthread_mutex_unlock ),因此没有必要将 a 声明为 volatile ,编译器不会在内存屏障中假设 a 的值,从不

pthread_mutex_lock is a memory barrier (pthread_mutex_unlock as well, by the way) and thus it's not necessary to declare a as volatile, the compiler will not make an assumption of the value of a across a memory barrier, never.

在所有这些方面,Objective-C与C非常相似,毕竟它只是一个带有扩展和运行时的C语言。需要注意的一点是,Obj-C中的 atomic 属性是内存障碍,因此您无需声明属性 volatile 。如果从多个线程访问该属性,请将其声明为 atomic ,这甚至是默认的(如果你没有标记它 nonatomic ,它将是 atomic )。如果您从未从多个线程访问它,标记它 nonatomic 将更快地访问该属性,但只有在您真正访问该属性时才能获得回报(a很多并不意味着每分钟十次,而是每秒几千次。

Objective-C is pretty much like C in all these aspects, after all it's just a C with extensions and a runtime. One thing to note is that atomic properties in Obj-C are memory barriers, so you don't need to declare properties volatile. If you access the property from multiple threads, declare it atomic, which is even default by the way (if you don't mark it nonatomic, it will be atomic). If you never access it from multiple thread, tagging it nonatomic will make access to that property a lot faster, but that only pays off if you access the property really a lot (a lot doesn't mean ten times a minute, it's rather several thousand times a second).

所以你想要Obj-C代码,这需要不稳定?

So you want Obj-C code, that requires volatile?

@implementation SomeObject {
  volatile bool done;
}

- (void)someMethod {
  done = false;

  // Start some background task that performes an action
  // and when it is done with that action, it sets `done` to true.
  // ...

  // Wait till the background task is done
  while (!done) {
    // Run the runloop for 10 ms, then check again
    [[NSRunLoop currentRunLoop] 
      runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]
    ];
  }
}
@end

不含 volatile ,编译器可能很愚蠢,假设已完成将永远不会改变并替换!done 只需 true 。并且而(true)是一个永无止境的无限循环。

Without volatile, the compiler may be dumb enough to assume, that done will never change here and replace !done simply with true. And while (true) is an endless loop that will never terminate.

我还没有用现代测试编译器。也许当前版本的 clang 比这更聪明。它还可能取决于您如何启动后台任务。如果你发送一个块,编译器实际上可以很容易地看到它是否改变已完成。如果你在某处传递对 done 的引用,编译器知道接收者可能的值并且不会使任何假设。但是很久以前我在Apple仍在使用GCC 2.x并且没有使用 volatile 时确实测试了那段代码确实造成了一个永无止境的无限循环(但仅在发布时)在启用优化的情况下构建,而不是在调试版本中。所以我不会依赖编译器足够聪明来做正确的事。

I haven't tested that with modern compilers. Maybe the current version of clang is more intelligent than that. It may also depend on how you start the background task. If you dispatch a block, the compiler can actually easily see whether it changes done or not. If you pass a reference to done somewhere, the compiler knows that the receiver may the value of done and will not make any assumptions. But I tested exactly that code a long time ago when Apple was still using GCC 2.x and there not using volatile really caused an endless loop that never terminated (yet only in release builds with optimizations enabled, not in debug builds). So I would not rely on the compiler being clever enough to do it right.



关于内存障碍的一些更有趣的事实:

如果你曾经看过Apple在< libkern / OSAtomic.h> ,那么你可能想知道为什么每个操作都存在两次:一次为 x ,一次为 xBarrier (例如 OSAtomicAdd32 OSAtomicAdd32Barrier )。好吧,现在你终于明白了。名字中有障碍的是一个记忆障碍,另一个不是。

If you ever had a look at the atomic operations that Apple offers in <libkern/OSAtomic.h>, then you might have wondered why every operation exists twice: Once as x and once as xBarrier (e.g. OSAtomicAdd32 and OSAtomicAdd32Barrier). Well, now you finally know it. The one with "Barrier" in its name is a memory barrier, the other one isn't.

内存障碍不仅适用于编译器,也适用于CPU(存在CPU指令,被视为内存屏障,而普通指令则不存在)。 CPU需要知道这些障碍,因为CPU喜欢重新排序指令以无序执行操作。例如。如果你这样做

Memory barriers are not just for compilers, they are also for CPUs (there exists CPU instructions, that are considered memory barriers while normal instructions are not). The CPU needs to know these barriers because CPUs like to reorder instructions to perform operations out of order. E.g. if you do

a = x + 3 // (1)
b = y * 5 // (2)
c = a + b // (3)

并且添加的管道很忙,但是乘法的管道不是,CPU可以在(1)之前执行指令(2),毕竟订单最终无关紧要。这可以防止管道停滞。此外,CPU非常聪明,知道在(1)或<$ c之前它无法执行(3) $ c>(2)因为(3)的结果取决于其他两个计算的结果。

and the pipeline for additions is busy, but the pipeline for multiplication is not, the CPU may perform instruction (2) before (1), after all the order won't matter in the end. This prevents a pipeline stall. Also the CPU is clever enough to know that it cannot perform (3) before either (1) or (2) because the result of (3) depends on the results of the other two calculations.

然而,某些类型的订单更改会破坏代码或程序员的意图。考虑这个例子:

Yet, certain kinds of order changes will break the code, or the intention of the programmer. Consider this example:

x = y + z // (1)
a = 1 // (2)

加法管道可能很忙,所以为什么不执行(2)(1)之前?他们不依赖对方,顺序应该无关紧要吧?错误!但为什么?因为另一个线程监视 a 进行更改,并且只要 a 变为 1 ,它读取 x 的值,现在应该是 y + z 如果说明是在但是,如果CPU重新排序了上面的两行,则不是。

The addition pipe might be busy, so why not just perform (2) before (1)? They don't depend on each other, the order shouldn't matter, right? Wrong! But why? Because another thread monitors a for changes and as soon as a becomes 1, it reads the value of x, which should now be y+z if the instructions were performed in order, but it isn't if the CPU has reordered the two line above.

因此,在这种情况下,订单将很重要,这就是为什么CPU也需要障碍的原因:CPU不会在这些障碍中订购指令,因此指令(2)需要是一个屏障指令(或者需要在(1)(2);这取决于CPU)。重新排序指令相当新,但更老的问题是延迟的内存写入。如果CPU延迟了内存写入(对于某些CPU非常常见,因为CPU的内存访问非常慢),它将确保在超过内存屏障之前执行延迟写入(现在您知道名称记忆障碍实际上来自。

So in this case the order will matter and that's why barriers are needed also for CPUs: CPUs don't order instructions across such barriers and thus instruction (2) would need to be a barrier instruction (or there needs to be such an instruction between (1) and (2); that depends on the CPU). Reordering instructions is rather new, though, a much older problem are delayed memory writes. If CPUs delay memory writes (very common for some CPUs, as memory access is horribly slow for a CPU), it will make sure that delayed writes are performed before a memory barrier is crossed (and now you know where the name "memory barrier" actually comes from).

你可能在记忆障碍方面的工作量比你所知的要多得多(GCD - Grand Central Dispatch is充满了这些和 NSOperation / NSOperationQueue 基于GCD),这就是为什么你真的需要使用 volatile 仅在非常罕见的特殊情况下。您可能会忘记编写100个应用程序,甚至不必使用它一次。但是,如果您编写了很多低级别的多线程代码,旨在实现最高性能,那么您迟早会遇到只有 volatile 才能授予您权限的情况正确的行为。

You are probably working a lot more with memory barriers than you are even aware of (GCD - Grand Central Dispatch is full of these and NSOperation/NSOperationQueue bases on GCD), that's why your really need to use volatile only in very rare, exceptional cases. You might get away writing 100 apps and never have to use it even once. However, if you write a lot low level, multi-threading code that aims to achieve maximum performance possible, you will sooner or later run into a situation where only volatile can grantee you correct behavior.

这篇关于哪个场景关键字“易失性”是否需要在objective-c中声明?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆