objective-c中需要声明哪些场景关键字"volatile"?
Which scenes keyword "volatile" is needed to declare in objective-c?
据我所知,volatile
通常用于防止某些硬件操作过程中意外的编译优化。但是 volatile
应该在 属性 定义中声明哪些场景让我感到困惑。请举几个有代表性的例子。
谢谢。
volatile
来自 C。在您最喜欢的搜索引擎中输入 "C language volatile"(部分结果可能来自 SO),或阅读有关 C 编程的书籍。那里有很多例子。
这里给出了很好的解释:Understanding “volatile” qualifier in C
The volatile keyword is intended to prevent the compiler from applying any optimizations on objects that can change in ways that cannot be determined by the compiler.
Objects declared as volatile are omitted from optimization because their values can be changed by code outside the scope of current code at any time. The system always reads the current value of a volatile object from the memory location rather than keeping its value in temporary register at the point it is requested, even if a previous instruction asked for a value from the same object. So the simple question is, how can value of a variable change in such a way that compiler cannot predict. Consider the following cases for answer to this question.
1) Global variables modified by an interrupt service routine outside the scope: For example, a global variable can represent a data port (usually global pointer referred as memory mapped IO) which will be updated dynamically. The code reading data port must be declared as volatile in order to fetch latest data available at the port. Failing to declare variable as volatile, the compiler will optimize the code in such a way that it will read the port only once and keeps using the same value in a temporary register to speed up the program (speed optimization). In general, an ISR used to update these data port when there is an interrupt due to availability of new data
2) Global variables within a multi-threaded application: There are multiple ways for threads communication, viz, message passing, shared memory, mail boxes, etc. A global variable is weak form of shared memory. When two threads sharing information via global variable, they need to be qualified with volatile. Since threads run asynchronously, any update of global variable due to one thread should be fetched freshly by another consumer thread. Compiler can read the global variable and can place them in temporary variable of current thread context. To nullify the effect of compiler optimizations, such global variables to be qualified as volatile
If we do not use volatile qualifier, the following problems may arise
1) Code may not work as expected when optimization is turned on.
2) Code may not work as expected when interrupts are enabled and used.
编译器假定变量可以更改其值的唯一方法是通过更改它的代码。
int a = 24;
现在编译器假定 a
是 24
,直到它看到任何更改 a
值的语句。如果你在上面的语句下面的某个地方写代码说
int b = a + 3;
编译器会说“我知道 a
是什么,它是 24
!所以 b
是 27
。我不知道编写代码来执行该计算,我知道它将总是 27
”。编译器可能只是优化了整个计算。
但是如果 a
在赋值和计算之间发生了变化,编译器就会出错。但是,为什么 a
会那样做呢?为什么 a
突然有了不同的值?不会的。
如果 a
是一个堆栈变量,它不能改变值,除非你传递一个引用给它,例如
doSomething(&a);
函数doSomething
有一个指向a
的指针,这意味着它可以改变a
的值,在那行代码之后,a
可能不会24
不再。所以如果你写
int a = 24;
doSomething(&a);
int b = a + 3;
编译器不会优化计算。谁知道 a
在 doSomething
之后会有什么价值?编译器肯定不会。
使用对象的全局变量或实例变量,事情会变得更加棘手。这些变量不在堆栈上,它们在堆上,这意味着不同的线程可以访问它们。
// Global Scope
int a = 0;
void function ( ) {
a = 24;
b = a + 3;
}
b
会是 27
吗?很可能答案是肯定的,但也有极小的可能其他线程在这两行代码之间更改了 a
的值,然后它就不会是 27
。编译器关心吗?没有为什么?因为 C 对线程一无所知——至少它以前不知道(最新的 C 标准终于知道本机线程,但在此之前的所有线程功能只是 API 由操作系统提供,而不是本机的C)。所以 C 编译器仍然会假设 b
是 27
并优化计算,这可能会导致不正确的结果。
这就是 volatile
的好处。如果你像那样标记一个变量 volatile
volatile int a = 0;
你基本上是在告诉编译器:“a
的值可能随时改变。不严重,它可能会突然改变。你看不到它的到来和 *bang*,它有不同的值!”。对于编译器来说,这意味着它不能假定 a
具有某个值,只是因为它在 1 皮秒前曾经具有该值并且似乎没有代码更改它。没关系。当访问 a
时,总是 读取它的当前值。
过度使用 volatile 会阻止很多编译器优化,可能会显着降低计算代码的速度,而且人们经常在甚至没有必要的情况下使用 volatile。例如,编译器从不跨内存屏障进行值假设。内存屏障到底是什么?嗯,这有点超出我的回复范围。您只需要知道典型的同步结构是内存屏障,例如锁、互斥量或信号量等。考虑以下代码:
// 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
的值跨越内存屏障,never.
Objective-C 在所有这些方面都非常像 C,毕竟它只是一个带有扩展和 运行 时间的 C。需要注意的一件事是 Obj-C 中的 atomic
属性是内存屏障,因此您不需要声明属性 volatile
。如果你从多个线程访问属性,声明它atomic
,这甚至是默认的(如果你不标记它nonatomic
,它将是atomic
).如果您从不从多个线程访问它,将其标记为 nonatomic
将使访问 属性 的速度更快,但只有当您访问 属性 确实很多(很多不是一分钟十次,而是一秒几千次)。
所以你想要 Obj-C 代码,它需要 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
替换 !done
。而while (true)
是一个永远不会终止的死循环。
我还没有用现代编译器测试过。也许 clang
的当前版本比那个更智能。它还可能取决于您如何启动后台任务。如果你分派一个块,编译器实际上可以很容易地看到它是否改变了 done
。如果你在某处传递对 done
的引用,编译器知道接收者可能是 done
的值,并且不会做任何假设。但是我很久以前就测试过这段代码,当时 Apple 还在使用 GCC 2.x 而没有使用 volatile
确实导致了一个永不终止的无限循环(但仅在启用了优化的发布版本中,而不是在调试构建)。所以我不会依赖编译器足够聪明来做正确的事。
关于内存障碍的一些更有趣的事实:
如果您看过 Apple 在 <libkern/OSAtomic.h>
中提供的原子操作,那么您可能想知道为什么每个操作都存在两次:一次是 x
一次是 xBarrier
(例如 OSAtomicAdd32
和 OSAtomicAdd32Barrier
)。好吧,现在你终于知道了。名称中带有 "Barrier" 的是内存屏障,而另一个不是。
内存屏障不仅适用于编译器,它们也适用于 CPUs(存在 CPU 指令,它们被视为内存屏障,而普通指令则不是)。 CPU 需要知道这些障碍,因为 CPU 喜欢重新排序指令以乱序执行操作。例如。如果你这样做
a = x + 3 // (1)
b = y * 5 // (2)
c = a + b // (3)
并且加法流水线很忙,但乘法流水线不忙,CPU可能会在(1)
之前执行指令(2)
,毕竟顺序无关紧要到底。这可以防止流水线停顿。 CPU 也足够聪明,知道它不能在 (1)
或 (2)
之前执行 (3)
因为 (3)
的结果取决于另一个的结果两次计算。
然而,某些类型的顺序更改会破坏代码或程序员的意图。考虑这个例子:
x = y + z // (1)
a = 1 // (2)
添加管道可能很忙,那么为什么不在 (1)
之前执行 (2)
?他们不依赖彼此,顺序应该无关紧要,对吧?这要看情况。考虑另一个线程监视 a
的变化,一旦 a
变为 1
,它会读取 x
的值,如果指令,现在应该是 y+z
是按顺序进行的。然而,如果 CPU 对它们重新排序,那么 x
将具有它在到达此代码之前所具有的任何值,这会有所不同,因为另一个线程现在将使用不同的值,而不是该值程序员会预料到的。
所以在这种情况下,顺序很重要,这就是为什么 CPUs 也需要障碍:CPUs 不要跨越这些障碍来命令指令,因此指令 (2)
需要是屏障指令(或者需要在 (1)
和 (2)
之间有这样的指令;这取决于 CPU)。然而,重新排序指令仅由现代 CPUs 执行,一个更老的问题是延迟内存写入。如果 CPU 延迟内存写入(对于某些 CPU 来说很常见,因为内存访问对于 CPU 来说非常慢),它将确保所有延迟的写入都已执行并已完成在跨越内存屏障之前,所有内存都处于正确状态,以防另一个线程现在可能访问它(现在您也知道名称“内存屏障”的实际来源) .
你可能在内存障碍方面的工作比你意识到的要多得多(GCD - Grand Central Dispatch 充满了这些并且 NSOperation
/NSOperationQueue
基于 GCD),这就是为什么你真的只需要在非常罕见的例外情况下使用 volatile
。您可能会编写 100 个应用程序而无需使用它一次。但是,如果您编写大量低级、多线程代码以尽可能实现最大性能,您迟早会 运行 陷入只有 volatile
可以允许您纠正行为的情况;在这种情况下不使用它会导致奇怪的错误,其中循环似乎没有终止或变量似乎只是具有不正确的值并且您找不到对此的解释。如果您 运行 遇到这样的错误,特别是如果您只在发布版本中看到它们,您可能会错过代码中某处的 volatile
或内存障碍。
据我所知,volatile
通常用于防止某些硬件操作过程中意外的编译优化。但是 volatile
应该在 属性 定义中声明哪些场景让我感到困惑。请举几个有代表性的例子。
谢谢。
volatile
来自 C。在您最喜欢的搜索引擎中输入 "C language volatile"(部分结果可能来自 SO),或阅读有关 C 编程的书籍。那里有很多例子。
这里给出了很好的解释:Understanding “volatile” qualifier in C
The volatile keyword is intended to prevent the compiler from applying any optimizations on objects that can change in ways that cannot be determined by the compiler.
Objects declared as volatile are omitted from optimization because their values can be changed by code outside the scope of current code at any time. The system always reads the current value of a volatile object from the memory location rather than keeping its value in temporary register at the point it is requested, even if a previous instruction asked for a value from the same object. So the simple question is, how can value of a variable change in such a way that compiler cannot predict. Consider the following cases for answer to this question.
1) Global variables modified by an interrupt service routine outside the scope: For example, a global variable can represent a data port (usually global pointer referred as memory mapped IO) which will be updated dynamically. The code reading data port must be declared as volatile in order to fetch latest data available at the port. Failing to declare variable as volatile, the compiler will optimize the code in such a way that it will read the port only once and keeps using the same value in a temporary register to speed up the program (speed optimization). In general, an ISR used to update these data port when there is an interrupt due to availability of new data
2) Global variables within a multi-threaded application: There are multiple ways for threads communication, viz, message passing, shared memory, mail boxes, etc. A global variable is weak form of shared memory. When two threads sharing information via global variable, they need to be qualified with volatile. Since threads run asynchronously, any update of global variable due to one thread should be fetched freshly by another consumer thread. Compiler can read the global variable and can place them in temporary variable of current thread context. To nullify the effect of compiler optimizations, such global variables to be qualified as volatile
If we do not use volatile qualifier, the following problems may arise
1) Code may not work as expected when optimization is turned on.
2) Code may not work as expected when interrupts are enabled and used.
编译器假定变量可以更改其值的唯一方法是通过更改它的代码。
int a = 24;
现在编译器假定 a
是 24
,直到它看到任何更改 a
值的语句。如果你在上面的语句下面的某个地方写代码说
int b = a + 3;
编译器会说“我知道 a
是什么,它是 24
!所以 b
是 27
。我不知道编写代码来执行该计算,我知道它将总是 27
”。编译器可能只是优化了整个计算。
但是如果 a
在赋值和计算之间发生了变化,编译器就会出错。但是,为什么 a
会那样做呢?为什么 a
突然有了不同的值?不会的。
如果 a
是一个堆栈变量,它不能改变值,除非你传递一个引用给它,例如
doSomething(&a);
函数doSomething
有一个指向a
的指针,这意味着它可以改变a
的值,在那行代码之后,a
可能不会24
不再。所以如果你写
int a = 24;
doSomething(&a);
int b = a + 3;
编译器不会优化计算。谁知道 a
在 doSomething
之后会有什么价值?编译器肯定不会。
使用对象的全局变量或实例变量,事情会变得更加棘手。这些变量不在堆栈上,它们在堆上,这意味着不同的线程可以访问它们。
// Global Scope
int a = 0;
void function ( ) {
a = 24;
b = a + 3;
}
b
会是 27
吗?很可能答案是肯定的,但也有极小的可能其他线程在这两行代码之间更改了 a
的值,然后它就不会是 27
。编译器关心吗?没有为什么?因为 C 对线程一无所知——至少它以前不知道(最新的 C 标准终于知道本机线程,但在此之前的所有线程功能只是 API 由操作系统提供,而不是本机的C)。所以 C 编译器仍然会假设 b
是 27
并优化计算,这可能会导致不正确的结果。
这就是 volatile
的好处。如果你像那样标记一个变量 volatile
volatile int a = 0;
你基本上是在告诉编译器:“a
的值可能随时改变。不严重,它可能会突然改变。你看不到它的到来和 *bang*,它有不同的值!”。对于编译器来说,这意味着它不能假定 a
具有某个值,只是因为它在 1 皮秒前曾经具有该值并且似乎没有代码更改它。没关系。当访问 a
时,总是 读取它的当前值。
过度使用 volatile 会阻止很多编译器优化,可能会显着降低计算代码的速度,而且人们经常在甚至没有必要的情况下使用 volatile。例如,编译器从不跨内存屏障进行值假设。内存屏障到底是什么?嗯,这有点超出我的回复范围。您只需要知道典型的同步结构是内存屏障,例如锁、互斥量或信号量等。考虑以下代码:
// 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
的值跨越内存屏障,never.
Objective-C 在所有这些方面都非常像 C,毕竟它只是一个带有扩展和 运行 时间的 C。需要注意的一件事是 Obj-C 中的 atomic
属性是内存屏障,因此您不需要声明属性 volatile
。如果你从多个线程访问属性,声明它atomic
,这甚至是默认的(如果你不标记它nonatomic
,它将是atomic
).如果您从不从多个线程访问它,将其标记为 nonatomic
将使访问 属性 的速度更快,但只有当您访问 属性 确实很多(很多不是一分钟十次,而是一秒几千次)。
所以你想要 Obj-C 代码,它需要 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
替换 !done
。而while (true)
是一个永远不会终止的死循环。
我还没有用现代编译器测试过。也许 clang
的当前版本比那个更智能。它还可能取决于您如何启动后台任务。如果你分派一个块,编译器实际上可以很容易地看到它是否改变了 done
。如果你在某处传递对 done
的引用,编译器知道接收者可能是 done
的值,并且不会做任何假设。但是我很久以前就测试过这段代码,当时 Apple 还在使用 GCC 2.x 而没有使用 volatile
确实导致了一个永不终止的无限循环(但仅在启用了优化的发布版本中,而不是在调试构建)。所以我不会依赖编译器足够聪明来做正确的事。
关于内存障碍的一些更有趣的事实:
如果您看过 Apple 在 <libkern/OSAtomic.h>
中提供的原子操作,那么您可能想知道为什么每个操作都存在两次:一次是 x
一次是 xBarrier
(例如 OSAtomicAdd32
和 OSAtomicAdd32Barrier
)。好吧,现在你终于知道了。名称中带有 "Barrier" 的是内存屏障,而另一个不是。
内存屏障不仅适用于编译器,它们也适用于 CPUs(存在 CPU 指令,它们被视为内存屏障,而普通指令则不是)。 CPU 需要知道这些障碍,因为 CPU 喜欢重新排序指令以乱序执行操作。例如。如果你这样做
a = x + 3 // (1)
b = y * 5 // (2)
c = a + b // (3)
并且加法流水线很忙,但乘法流水线不忙,CPU可能会在(1)
之前执行指令(2)
,毕竟顺序无关紧要到底。这可以防止流水线停顿。 CPU 也足够聪明,知道它不能在 (1)
或 (2)
之前执行 (3)
因为 (3)
的结果取决于另一个的结果两次计算。
然而,某些类型的顺序更改会破坏代码或程序员的意图。考虑这个例子:
x = y + z // (1)
a = 1 // (2)
添加管道可能很忙,那么为什么不在 (1)
之前执行 (2)
?他们不依赖彼此,顺序应该无关紧要,对吧?这要看情况。考虑另一个线程监视 a
的变化,一旦 a
变为 1
,它会读取 x
的值,如果指令,现在应该是 y+z
是按顺序进行的。然而,如果 CPU 对它们重新排序,那么 x
将具有它在到达此代码之前所具有的任何值,这会有所不同,因为另一个线程现在将使用不同的值,而不是该值程序员会预料到的。
所以在这种情况下,顺序很重要,这就是为什么 CPUs 也需要障碍:CPUs 不要跨越这些障碍来命令指令,因此指令 (2)
需要是屏障指令(或者需要在 (1)
和 (2)
之间有这样的指令;这取决于 CPU)。然而,重新排序指令仅由现代 CPUs 执行,一个更老的问题是延迟内存写入。如果 CPU 延迟内存写入(对于某些 CPU 来说很常见,因为内存访问对于 CPU 来说非常慢),它将确保所有延迟的写入都已执行并已完成在跨越内存屏障之前,所有内存都处于正确状态,以防另一个线程现在可能访问它(现在您也知道名称“内存屏障”的实际来源) .
你可能在内存障碍方面的工作比你意识到的要多得多(GCD - Grand Central Dispatch 充满了这些并且 NSOperation
/NSOperationQueue
基于 GCD),这就是为什么你真的只需要在非常罕见的例外情况下使用 volatile
。您可能会编写 100 个应用程序而无需使用它一次。但是,如果您编写大量低级、多线程代码以尽可能实现最大性能,您迟早会 运行 陷入只有 volatile
可以允许您纠正行为的情况;在这种情况下不使用它会导致奇怪的错误,其中循环似乎没有终止或变量似乎只是具有不正确的值并且您找不到对此的解释。如果您 运行 遇到这样的错误,特别是如果您只在发布版本中看到它们,您可能会错过代码中某处的 volatile
或内存障碍。