i++ 原子操作的讨论 转自http://gcoder.blogbus.com/logs/75275569.html

i++ 原子操作的讨论

日期:2010-09-19 分类:c & c++

今天在CU上和cjaizss 讨论i++是不是原子操作的问题,收获很多,我之前有很多想法都是错误的,做个总结,记录下来。(习惯i++了,下面讨论的i和x是同一含义)

我对原子操作的理解是需要一条cpu指令的操作,如果i是32位的,并且没有跨页,那么i++是原子的,如果i是64位的,那么不是原子的,它需要两条cpu指令。

i 是32位时的汇编,addl    $1, %eax是 i++ 对应的cpu指令

    movl    x, %eax

    addl    $1, %eax

    movl    %eax, x

i 是64位时的汇编,addl    $1, %eax 和adcl    $0, %edx 是i++对应的cpu指令

    movl    x, %eax

    movl    x+4, %edx                                             

    addl    $1, %eax

    adcl    $0, %edx                                              

    movl    %eax, x

    movl    %edx, x+4 

但是i++是原子的(假设i是32位的),并不能保证多个线程i++,操作同一个i,可以得到正确的结果。因为还有寄存器的因素,多个cpu对应多个寄存器。每次要先把i从内存复制到寄存器,然后++,然后再把i复制到内存中,这需要至少3步。从这个意义上讲,说i++是原子的并不对。

gcc扩展提供了原子的i++,其对应的汇编是

    lock

    addl    $1, x

多个cpu对应的寄存器不同,但是内存是相同的。(我记得cpu不能直接操作内存,这个addl    $1, x,我不知道是啥意思)

 

我之前以为只要加上volatile关键字就可以保证,就不会出现i对应的内容在多个cpu的寄存器中不一致的情况,看来理解是错的,它只是在编译器优化时,提示每次都从内存中读取i的值。

观察优化后的代码(O3)也很有意思,

没有volatile修饰的i对应的汇编

    addl    $100000000, x

略去了循环,一次累加了所有的。有volatile时对应的汇编

    movl    x, %eax

    addl    $1, %edx

    addl    $1, %eax

    cmpl    $100000000, %edx

    movl    %eax, x

和没优化时差别不大。

 

测试代码

  1. #include <stdio.h>
  2. #include <signal.h>
  3. #include <pthread.h>
  4. #define MYNUM 100000000
  5. #ifdef A0
  6.   int x = 0;
  7. # define ADD(x) do { (x)++; } while(0)
  8. #elif defined A1
  9.   volatile sig_atomic_t x = 0;
  10. # define ADD(x) do { (x)++; } while(0)
  11. #elif defined A2
  12.   int x = 0;
  13. # define ADD(x) do { __sync_fetch_and_add(&(x), 1); } while (0)
  14. #elif defined A3
  15.   int x = 0;
  16.   pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  17. # define ADD(x) do { pthread_mutex_lock(&mutex); (x)++; pthread_mutex_unlock(&mutex); } while (0)
  18. #else
  19. # error "no add method"
  20. #endif
  21. void* func1(void* arg) 
  22. {
  23.     int i;
  24.     for(i = 0; i < MYNUM; i++) 
  25.         ADD(x);
  26. }
  27. int main()
  28.     pthread_t id1,id2;
  29.     pthread_create(&id1, NULL, func1, NULL); 
  30.     pthread_create(&id2, NULL, func1, NULL); 
  31.     pthread_join(id1, NULL);
  32.     pthread_join(id2, NULL);
  33.     printf("x=%d\nMYNUM*2=%d\n", x, MYNUM*2);  
  34.     return 0;
  35. }

编译

gcc -o a0 inc.c -pthread -DA0

gcc -o a1 inc.c -pthread -DA1

gcc -o a2 inc.c -pthread -DA2

gcc -o a3 inc.c -pthread -DA3

 

运行(只少要俩cpu)

A0和A1,无疑运行最快,当然答案是错的。

A2比A1慢8倍左右,可见cpu寄存器的影响之大。

A3比A2慢5倍左右,可见加锁始终是个问题,不得已而为之。

 

这些体会,可能不怎么正确,还有待进一步验证。

我不会汇编,这是个问题。08年就认识到这个问题了,现在感觉如此强烈,是时候学习了。


收藏到: Del.icio.us

你可能感兴趣的:(i++ 原子操作的讨论 转自http://gcoder.blogbus.com/logs/75275569.html)