volatile高效使用讨论
volatile的具体使用时机到底是什么?搜了很多资料,了解到volatile的滥用会导致性能的下降
那么有几个问题希望大牛能帮忙解释下,不胜感激
1. #define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
是为了把非volatile变量拯救。或者说达到“非volatile类型的变量,依然可以直接从内存读取而非寄存器"的目的
那么为什么内核类似源码中。对变量的写操作未加该宏的控制。
是否说对寄存器的写操作 是会立马回写到内存的,也就是说volatile只是控制每次读从内存读,不控制写?
2. 内核加了锁。如spin_lock 是否还需volatile
如
加锁
读a
解锁
其他操作
加锁
读a
解锁
这里的第二次读a,难道就一定是内存中的,而非寄存器?
按照我的理解是volatile的使用时机
1. 防止代码优化。如驱动中写时序,就可能对一个变量重复赋值
这到不难理解
2.循环。尤其是循环检测一个状态。那么这个状态变量可能一直在寄存器中。那么多核情况下不用volatile就可能导致检测不到其他cpu的修改
3.实时性要求高的情况。
一般的内核编码。应该不用考虑volatile吧。因为
1.除非代码很短且集中或声明了register变量。否者寄存器中的值应该是很快被替换掉的。毕竟寄存器不像cache那么大
2.一般代码即便是读到了register缓存值。问题也不大吧。那有那么多代码逻辑要求那么高的。那么是否可以认为出了问题再解决也可以?
1 从宏定义就可以看出它是强制读操作,写操作当然用不上,因为写操作本身就说明你已经有数据的最新版本了,不需要再读。
2 在函数中:
加锁
读a// 独占访问,无须加关键字
解锁
如果多次加解锁:
加锁
读a
解锁
其他操作// 这引入了竞争条件
加锁
读a // 是否数据最新版本,依赖于具体编译器的实现。如果要从语言层面消除此不确定性,加关键字声明
解锁
回复 2# 爻易
1 从宏定义就可以看出它是强制读操作,写操作当然用不上,因为写操作本身就说明你已经有数据的最新版本了,不需要再读。
可能这个我没太说清楚。可以看下下面我对代码的理解
2 在函数中:
加锁
读a
解锁
其他操作// 这引入了竞争条件
加锁
读a // 是否数据最新版本,依赖于具体编译器的实现。如果要从语言层面消除此不确定性,加关键字声明
解锁
您这个理解跟我是一样的。那能否解释下下面代码的理解?
其实 这个问题是看内核netfilter模块rcu锁的使用时产生的。不知道有人看过这块没
代码如下
int nf_register_hook(struct nf_hook_ops *reg)
{
struct nf_hook_ops *elem;
int err;
err = mutex_lock_interruptible(&nf_hook_mutex);
if (err < 0)
return err;
list_for_each_entry(elem, &nf_hooks, list) {
/*这里没有使用ACCESS_ONCE相关的函数.基本上相关的资料显示跟您说的是一致的,因为mutex_lock_interruptible屏蔽掉了其它的写操作。那么当前寄存器的值肯定是最新的。那么我的疑问产生了
1.根据大多数资料的显示。volatile类型的变量。其写和读都是立即响应到内存的。那么一个ACCESS_ONCE如何能达到volatile的写目的?
还是说那些资料中说的写其实是很多人的误解,就好像很多人误认为volatile跟cache有关系一样
2.锁屏蔽掉了其他的写操作好像不能说一定就不会出问题吧。
如 cpu1
rcu_read_lock()
list_for_each_continue_rcu //这里使用了 ACCESS_ONCE保证寄存器中为最新的。这里假设链表为空,即表头的next和prev都指向自己
rcu_read_unlock()
//这个间隔cpu2 在表里加了一个list节点。这里就有个问题cpu2的寄存器的值会立马写回内存吗?
cpu1继续执行
mutex_lock
list_for_each_entry//这里没有使用ACCESS_ONCE访问,那么有没有可能寄存中的值还是旧的如现在读到的表为空。那么继续的写操作可就非常危险了。
mutex_unlock
*/
if (reg->priority < elem->priority)
break;
}
list_add_rcu(®->list, elem->list.prev);
mutex_unlock(&nf_hook_mutex);
return 0;
}
int nf_hook_slow(u_int8_t pf, unsigned int hook, struct sk_buff *skb,
struct net_device *indev,
struct net_device *outdev,
int (*okfn)(struct sk_buff *),
{
……
rcu_read_lock();
elem = &nf_hooks;
next_hook:
verdict = nf_iterate(&nf_hooks, skb, hook, indev,
outdev, &elem, okfn, hook_thresh);
……
}
rcu_read_unlock();
……
}
unsigned int nf_iterate(struct list_head *head,
struct sk_buff *skb,
unsigned int hook,
const struct net_device *indev,
const struct net_device *outdev,
struct list_head **i,
int (*okfn)(struct sk_buff *),
int hook_thresh)
{
……
list_for_each_continue_rcu(*i, head) {
}
……
}
#define list_for_each_continue_rcu(pos, head) \
for ((pos) = rcu_dereference_raw((pos)->next); \
prefetch((pos)->next), (pos) != (head); \
(pos) = rcu_dereference_raw((pos)->next))
#define rcu_dereference_raw(p) ({ \
typeof(p) _________p1 = ACCESS_ONCE(p); \
smp_read_barrier_depends(); \
(_________p1); \
})
只要有过解锁,就意味着缓存在寄存器中的相应数据应当作废,不应该再使用。
但语言层并无加解锁的语义,编译器不明白此语义,它会尽量重复利用寄存器中的值代表变量。
这就需要额外信息通知编译器,明确告诉它某些数据可能已经失去有效性,需要重新装载。
关于写的问题,如果解锁后仍有访问行为,同样可能有缓存行为导致数据不一致。如果不再访问此变量,应该问题不大,编译器通过分析可知此变量已经使用完毕,可以提交到内存(除非你碰到极度脑残编译器)。
语言有弹性,编译器有自己的实现方式,这也是一些程序员更喜欢汇编+人工优化的原因,减少不确定性。 yimeng4a309 发表于 2014-10-29 10:57 static/image/common/back.gif
回复 2# 爻易
根据大多数资料的显示。volatile类型的变量。其写和读都是立即响应到内存的。那么一个ACCESS_ONCE如何能达到volatile的写目的?
还是说那些资料中说的写其实是很多人的误解,就好像很多人误认为volatile跟cache有关系一样 ...
非volatile变量,如果编译器决定用某个寄存器来缓存它,那么读写变量生成的是读写寄存器指令,编译器再决定在适当的时间提交到内存。如果编译器决定不缓存它,编译器生成的是读写内存的指令。
volatile变量,就是通知编译器不要用寄存器来缓存变量,编译器生成的是读写内存的指令。
Documentation/volatile-considered-harmful.txt
ACCESS_ONCE()
http://lwn.net/Articles/508991/ 外文多了头晕,中文资料已经足够深入明白,剩下的就是去尝试。 回复 6# asuka2001
外文确实头晕,不过小有收获
结合其他资料大概总结出来一个结论
加锁后不需要volatile
因为一般锁都会有Barrie防止乱序。而cpu跨过barrier后,会自动刷新寄存器缓存。
当然汇编搞的太少。这个也不知道对不对
回复 7# 爻易
说的对。实测出来的才是最正确的结果。不管是不同版本内核 或 lib库,总会有微小区别,导致代码在临界点出现完全不同的结果
可有时候硬件和编译环境有限。没法测试所有情况,就只能先做到理论上的无错,来防止后期环境的扩展
回复 8# yimeng4a309
Barrie针对的是处理器,让它按期望的序列执行。
volatile针对的是编译器,让它生成期望的内存访问指令。
它们功能不同,所以不能相互替代,正如<<程序员的自我修养...>>里面描述的那样,两方面都要考虑。
对volatile来说,只有在能确定不会发生数据不一致的情况下,可以不加volatile以提高性能,但如果没有把握或无法确定的话还是得加。
页:
[1]