免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: dingyujie

[内核同步] 关于自旋锁中的忙等的疑问 [复制链接]

论坛徽章:
1
水瓶座
日期:2013-09-28 21:40:25
发表于 2012-12-10 22:38 |显示全部楼层
1. 编译器的不会把这样编译吧?!
2. CPU会保证Cache一致性吧?!
leslielg 发表于 2012-12-10 08:51
我觉得这句话还有这样的作用,让寄存器的值都失效,而要cpu重新从memory加载变量(lock)的值。否则如果一直从 ...

论坛徽章:
0
发表于 2012-12-11 09:10 |显示全部楼层
回复 11# bensenq


    1. 编译器确实这样编译,名曰优化,volatile关键字就这么来的。
    2. 这不是cache一致性,别弄混了。

论坛徽章:
1
水瓶座
日期:2013-09-28 21:40:25
发表于 2012-12-11 23:31 |显示全部楼层
本帖最后由 bensenq 于 2012-12-12 00:01 编辑

回复 12# leslielg

你认为那样写的原因是处于逻辑正确性考虑么?我完全不赞同,我认为仅仅是性能优化考虑,就像上面几个帖子描述的那样。

你说“否则如果一直从寄存器读取lock的值,那这就是个死循环了。”我认为这个理解是完全错误的.
while (lock != 1); 这句代码的语义是:只要lock的值不等于1就继续检查lock的值,直到判断到lock为1再执行后面的指令。lock变量当然是在内存里,任何一个可用编译器都会按照该语义要求CPU循环从内存里读取lock值,而不是所谓的"一直从寄存器读取lock的值"。

直接上实验吧:

test.c
  1. int lock;
  2. int main()
  3. {
  4.     while(lock != 1);
  5.     return 0;
  6. }
复制代码
编译,反汇编为test.s

gcc test.c
objdump a.out -d > test.s

直接跳到main函数:
  1. 080483b4 <main>:
  2. 80483b4:   55                      push   %ebp
  3. 80483b5:   89 e5                   mov    %esp,%ebp
  4. 80483b7:   90                      nop
  5. 80483b8:   a1 18 a0 04 08          mov    0x804a018,%eax
  6. 80483bd:   83 f8 01                cmp    $0x1,%eax
  7. 80483c0:   75 f6                   jne    80483b8 <main+0x4>
  8. 80483c2:   b8 00 00 00 00          mov    $0x0,%eax
  9. 80483c7:   5d                      pop    %ebp
  10. 80483c8:   c3                      ret
复制代码
可以发现while循环对应80483b8~80483c0处的3条指令,分别读取lock值(访存)、比值较和跳转功能,是不是没一次循环都重新读取lock的值呢?到这里我想结论已经非常明显了:程序不会一直从寄存器读取值,因为这违背了这条C代码的语义。当然CPU每次读取lock的值也不一定都会访Memory,因为CPU和Memory之间还有Cache,只要Cache命中就会直接从Cache中取以节省时间,这也是我为什么提Cache一致性的原因。

回过头来我们在来验证一下楼主的疑问,看看内嵌汇编的写法到底带来了那些优化。

test1.c
  1. int lock;
  2. int main()
  3. {

  4.     while (lock != 1) {
  5.             __asm__ __volatile__("rep;nop": : :"memory");
  6.     }
  7.     return 0;
  8. }
复制代码
编译,反汇编为test1.s
gcc test1.c
objdump a.out -d > test1.s

main函数:
  1. 080483b4 <main>:
  2. 80483b4:   55                      push   %ebp
  3. 80483b5:   89 e5                   mov    %esp,%ebp
  4. 80483b7:   eb 02                   jmp    80483bb <main+0x7>
  5. 80483b9:   f3 90                   pause
  6. 80483bb:   a1 18 a0 04 08          mov    0x804a018,%eax
  7. 80483c0:   83 f8 01                cmp    $0x1,%eax
  8. 80483c3:   75 f4                   jne    80483b9 <main+0x5>
  9. 80483c5:   b8 00 00 00 00          mov    $0x0,%eax
  10. 80483ca:   5d                      pop    %ebp
  11. 80483cb:   c3                      ret
复制代码
while循环对应指令为80483b9~80483c3 4条指令,发现区别了吧?其实就是在每次读lock值前多了个pause指令(第一次除外,见80483b7处jmp)。关于pause的用途,楼上好几位都说过了,主要影响多核、多线程下的CPU性能及功耗,和CPU体系结构很密切,我也小google了一下,stackoverflow上有个回答供大家参看:How does x86 pause instruction work in spinlock *and* can it be used in other scenarios?

论坛徽章:
1
水瓶座
日期:2013-09-28 21:40:25
发表于 2012-12-11 23:42 |显示全部楼层
本帖最后由 bensenq 于 2012-12-12 00:00 编辑

回复 12# leslielg
当然,volatile关键字是使用来告诉编译器:不要"随便"帮我优化这段代码,否则“可能”会造成逻辑错误。

例如如果用 -O 选项优化test.c程序,会发现其指令会有所改变(不影响本例子的结果)。

gcc -O1 test.c
objdump a.out -d > test.s
  1. 080483b4 <main>:
  2. 80483b4:   83 3d 18 a0 04 08 01    cmpl   $0x1,0x804a018
  3. 80483bb:   75 06                   jne    80483c3 <main+0xf>
  4. 80483bd:   b8 00 00 00 00          mov    $0x0,%eax
  5. 80483c2:   c3                      ret
  6. 80483c3:   eb fe                   jmp    80483c3 <main+0xf>
复制代码
代码被优化为只读一次lock值,如果不等于1就直接死循环: jmp    80483c3 <main+0xf>。
   

论坛徽章:
0
发表于 2012-12-12 08:42 |显示全部楼层
回复 13# bensenq


    no,你说的不对,volatile如果修饰变量,就是为了防止编译器优化为从寄存器取值而必须要每次都从内存取值。一个变量不是总是从内存取值。如果都是从内存取值,那volatile修饰变量的用法就没必要存在了。你先研究volatile的用法和含义吧,这个讨论的基础差别有点远了。

论坛徽章:
0
发表于 2012-12-12 09:08 |显示全部楼层
你举的例子我试了,恰好说明了我的推测是正确的:

int lock;

int main()
{
    while (lock != 1);
    return 0;
}

gcc -O2 test.c,反汇编后是:

08048300 <main>:
8048300:        83 3d 18 a0 04 08 01         cmpl   $0x1,0x804a018
8048307:        75 03                        jne    804830c <main+0xc>
8048309:        31 c0                        xor    %eax,%eax
804830b:        c3                           ret   
804830c:        eb fe                        jmp    804830c <main+0xc>
804830e:        90                           nop
804830f:        90                           nop

说明被优化为只读一次,并且编译器自己做了逻辑判断认为是死循环了。

int lock;

int main()
{
    while (lock != 1)
       __asm__ __volatile__("rep;nop":::"memory");
    return 0;
}

gcc -O2 test.c
反汇编后:

08048300 <main>:
8048300:        eb 08                        jmp    804830a <main+0xa>
8048302:        8d b6 00 00 00 00            lea    0x0(%esi),%esi
8048308:        f3 90                        pause  
804830a:        83 3d 18 a0 04 08 01         cmpl   $0x1,0x804a018
8048311:        75 f5                        jne    8048308 <main+0x8>
8048313:        31 c0                        xor    %eax,%eax
8048315:        c3                           ret   
8048316:        90                           nop
8048317:        90                           nop

这样就每次从内存取值,并且逻辑上也正确了。

这就是我想说的:::memory在这里的作用,除了你们说的性能优化,最重要的是,加上这句才能让这个代码运行上正确。

论坛徽章:
4
天秤座
日期:2013-10-18 13:58:33金牛座
日期:2013-11-28 16:17:01辰龙
日期:2014-01-14 09:54:32戌狗
日期:2014-01-24 09:23:27
发表于 2012-12-12 09:39 |显示全部楼层
本帖最后由 liuiang 于 2012-12-14 10:08 编辑

volatile和__asm__ __volatile__("rep;nop":::"memory")都是谢绝编译器针对变量的寄存器优化的方法,

他们都是仅对生成的汇编指令有影响,而与体系结构无关(编辑:注1),不会引入特殊指令,换句话说指令本身不会关注是访问cache还是memory还是

外部寄存器(编辑:注2)或者是通过pci访问其设备的任何空间。

前者可以保证所有对volatile修饰的变量的访问,编译器都会产生一次load或者store指令。后者貌似限定更强一些,似乎是清空/复位整个寄存器分配(后者是我的猜测,有错请指正)。

注1:编辑:这里描述不正确,事实上改成“微体系”也是不对的,原本意思是,编译器不会生成诸如“屏障”“cache刷新,无效”等额外特殊指令

注2:编辑:原帖这里是“寄存器”,描述的不准确,实际上指的是内存映射外部设备寄存器,而非处理器内部通用寄存器

编辑:volatile在VS2005以后版本和Java中已经隐含带有了屏障语义(我没验证过,感兴趣可以研究研究),这里不再展开更细节的讨论。

论坛徽章:
1
水瓶座
日期:2013-09-28 21:40:25
发表于 2012-12-12 10:08 |显示全部楼层
本帖最后由 bensenq 于 2012-12-12 10:09 编辑

回复 16# leslielg


1. 首先如果只说x86下生成的pause指令,毫无疑问是优化作用,见上面13楼;
2. volatile的话,就是用来抑制优化的,是和编译器的优化功能紧密相关的。你看在test.c(没有内嵌汇编)中,如果不加-O2选项下生成的代码也是完全符合预期的(每次都从内存中加载)。关于volatile的作用我在14楼已经说的很明白,我不明白跟你说的“这个讨论的基础差别有点远了”是什么意思?好吧,那我就自己不补课吧,C Programming Language关于volatile关键字大概有几点说明,如果还有理解的不到的地方的请你告诉我。

i. declaring it volatile announces that it has special properties relevant to optimization.
ii. The purpose of volatile is to force an implementation to suppress optimization that could otherwise occur.


3. 即便是加-O2后的优化,也仅仅是只读一次内存就死循环了,而不会重复的从寄存器判断值。这和你所说的“一直从寄存器读取lock的值”是完全矛盾的,不知道你是不是和register关键字搞混了。

论坛徽章:
1
水瓶座
日期:2013-09-28 21:40:25
发表于 2012-12-12 10:11 |显示全部楼层
回复 17# liuiang

汇编指令<-->机器指令<-->CPU指令。。。。。。。不就是体系结构相关嘛?没见过体系结构无关的内嵌汇编哦...

   

论坛徽章:
4
天秤座
日期:2013-10-18 13:58:33金牛座
日期:2013-11-28 16:17:01辰龙
日期:2014-01-14 09:54:32戌狗
日期:2014-01-24 09:23:27
发表于 2012-12-12 11:20 |显示全部楼层
回复 19# bensenq


    不好意思,搞错了,是微体系。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP