zoukankan      html  css  js  c++  java
  • 原子操作耗时对比

      早前写的原子测试demo,一直挂在心头准备来篇介绍文章。

      今天在编译服务器中找了半天还是没找到,最后总算在个人PC中找到了,再不做总结的话可能哪天真会不小心误删了。

      代码已上传到这里,有需要的可以拿去测试(本人的demo大多基于Android4.4进行编译和调试,在其他版本上可能出现编译问题)。

    1. 背景

      多个线程访问同一个资源时,出现并发访问(读和写)问题,如果对资源的管理不合理,可能出现跟预期不同的结果。

      例如,多个线程同时对某个全局变量进行自增操作,这个自增操作至少涉及三条指令:

    • 从内存单元读入寄存器
    • 在寄存器中对变量操作(加/减1)
    • 把新值写回到内存单元

      如果一个线程执行上面三个步骤过程中,其他线程进入,则会打断前一个的执行动作,导致结果不为预期值。

      因此,需要将这几个动作作为原子操作,来加以保护。

    2. Android平台下的几种锁使用(以自增为例)

      2.1. cutils中实现的锁android_atomic_inc()

        其实是对android_atomic_add()的一层封装,参考这里

        

         

      2.2. bionic中实现的锁__atomic_inc()

        其实是对__sync_fetch_and_add()的一层封装,参考这里

        

      2.3. bionic中实现的互斥锁pthread_mutex_lock()

        其实现参考这个文件

        

        这个与前两个使用场景有些差异,前面两个主要用于对某个内存变量进行原子操作,这个除具有这个功能外,还可以保护临界资源,

      使先拿到锁的线程先运行,只有当释放锁后其他线程才能访问。

    3. 性能对比

      这几种锁的性能如何呢?参考demo,接下来分几种场景我对其进行对比。

    源码如下:

      1 #define LOG_TAG "AtomicTester"
      2 #include <utils/Log.h>
      3 
      4 #include <sys/atomics.h>
      5 
      6 #include <utils/Atomic.h>
      7 
      8 #define USE_ATOMIC_TYPE     1   //1-cutils; 2-bionic; 3-others(mutex)
      9 #define THREAD_COUNT        10
     10 #define ITERATION_COUNT     500000
     11 
     12 #define TEST_BIONIC
     13 #ifdef TEST_BIONIC
     14 extern int __atomic_cmpxchg(int old, int _new, volatile int *ptr);
     15 extern int __atomic_swap(int _new, volatile int *ptr);
     16 extern int __atomic_dec(volatile int *ptr);
     17 extern int __atomic_inc(volatile int *ptr);
     18 #endif
     19 
     20 static pthread_mutex_t waitLock = PTHREAD_MUTEX_INITIALIZER;
     21 static pthread_cond_t waitCond = PTHREAD_COND_INITIALIZER;
     22 
     23 
     24 #define TEST_MUTEX // not define it will cause wrong result when multi-threads ops the same var.
     25 
     26 #ifdef TEST_MUTEX
     27 static pthread_mutex_t incLock = PTHREAD_MUTEX_INITIALIZER;
     28 static pthread_mutex_t decLock = PTHREAD_MUTEX_INITIALIZER;
     29 static pthread_mutex_t addLock = PTHREAD_MUTEX_INITIALIZER;
     30 #endif
     31 
     32 static volatile int threadsStarted = 0;
     33 
     34 /* results */
     35 static int incTest = 0;
     36 static int decTest = 0;
     37 static int addTest = 0;
     38 static int andTest = 0;
     39 static int orTest = 0;
     40 static int casTest = 0;
     41 static int failingCasTest = 0;
     42 
     43 /*
     44  * Get a relative time value.
     45  */
     46 static int64_t getRelativeTimeNsec()
     47 {
     48 #define HAVE_POSIX_CLOCKS
     49 #ifdef HAVE_POSIX_CLOCKS
     50     struct timespec now;
     51     clock_gettime(CLOCK_MONOTONIC, &now);
     52     return (int64_t) now.tv_sec*1000000000LL + now.tv_nsec;
     53 #else
     54     struct timeval now;
     55     gettimeofday(&now, NULL);
     56     return (int64_t) now.tv_sec*1000000000LL + now.tv_usec * 1000LL;
     57 #endif
     58 }
     59 
     60 
     61 /*
     62  * Non-atomic implementations, for comparison.
     63  *
     64  * If these get inlined the compiler may figure out what we're up to and
     65  * completely elide the operations.
     66  */
     67 #define USE_NOINLINE 0
     68 #if USE_NOINLINE == 1
     69 static void incr() __attribute__((noinline));
     70 static void decr() __attribute__((noinline));
     71 static void add(int addVal) __attribute__((noinline));
     72 static int compareAndSwap(int oldVal, int newVal, int* addr) __attribute__((noinline));
     73 #else
     74 static void incr() __attribute__((always_inline));
     75 static void decr() __attribute__((always_inline));
     76 static void add(int addVal) __attribute__((always_inline));
     77 static int compareAndSwap(int oldVal, int newVal, int* addr) __attribute__((always_inline));
     78 #endif
     79 
     80 static void incr()
     81 {
     82 #ifdef TEST_MUTEX
     83     pthread_mutex_lock(&incLock);
     84 #endif
     85     incTest++;
     86 #ifdef TEST_MUTEX
     87     pthread_mutex_unlock(&incLock);
     88 #endif
     89 }
     90 static void decr()
     91 {
     92 #ifdef TEST_MUTEX
     93     pthread_mutex_lock(&decLock);
     94 #endif
     95     decTest--;
     96 #ifdef TEST_MUTEX
     97     pthread_mutex_unlock(&decLock);
     98 #endif
     99 }
    100 static void add(int32_t addVal)
    101 {
    102 #ifdef TEST_MUTEX
    103     pthread_mutex_lock(&addLock);
    104 #endif
    105     addTest += addVal;
    106 #ifdef TEST_MUTEX
    107     pthread_mutex_unlock(&addLock);
    108 #endif
    109 }
    110 static int compareAndSwap(int32_t oldVal, int32_t newVal, int32_t* addr)
    111 {
    112 #ifdef TEST_MUTEX
    113     pthread_mutex_lock(&addLock);
    114 #endif
    115     if (*addr == oldVal) {
    116         *addr = newVal;
    117 #ifdef TEST_MUTEX
    118     pthread_mutex_unlock(&addLock);
    119 #endif
    120         return 0;
    121     }
    122 #ifdef TEST_MUTEX
    123     pthread_mutex_unlock(&addLock);
    124 #endif
    125     return 1;
    126 }
    127 
    128 /*
    129  * Exercise several of the atomic ops.
    130  */
    131 static void doAtomicTest(int num)
    132 {
    133     int addVal = (num & 0x01) + 1;
    134 
    135     int i;
    136     for (i = 0; i < ITERATION_COUNT; i++) {
    137         if (USE_ATOMIC_TYPE == 1) {         //cutils, duration=865ms on ARM_CPU_PART_CORTEX_A53
    138             android_atomic_inc(&incTest); // wrapper for android_atomic_add(1, addr)
    139             android_atomic_dec(&decTest);
    140             android_atomic_add(addVal, &addTest);
    141 
    142             int val;
    143             do {
    144                 val = casTest;
    145             } while (android_atomic_release_cas(val, val+3, &casTest) != 0);
    146             //} while (android_atomic_cmpxchg(val, val+3, &casTest) != 0);
    147             do {
    148                 val = casTest;
    149             } while (android_atomic_acquire_cas(val, val-1, &casTest) != 0); // same with release_cas
    150         } else if (USE_ATOMIC_TYPE == 2) {  //bionic, duration=650ms
    151             __atomic_inc(&incTest); // wrapper for __sync_fetch_and_add(addr, 1), which is gcc built-in API
    152             __atomic_dec(&decTest);
    153             __sync_fetch_and_add(&addTest, addVal);
    154 
    155             int val;
    156             do {
    157                 val = casTest;
    158             } while (android_atomic_release_cas(val, val+3, &casTest) != 0);
    159             do {
    160                 val = casTest;
    161             } while (android_atomic_acquire_cas(val, val-1, &casTest) != 0);
    162         } else {                            //mutex, noinline_duration=3500ms, inline_duration=28000ms
    163             //usleep(1); // if no sleep, result may be right; when have sleep, result must be wrong!
    164             incr();
    165             decr();
    166             add(addVal);
    167 
    168             int val;
    169             do {
    170                 val = casTest;
    171             } while (compareAndSwap(val, val+3, &casTest) != 0);
    172             do {
    173                 val = casTest;
    174             } while (compareAndSwap(val, val-1, &casTest) != 0);
    175         }
    176     }
    177 }
    178 
    179 /*
    180  * Entry point for multi-thread test.
    181  */
    182 static void* atomicTest(void* arg)
    183 {
    184     pthread_mutex_lock(&waitLock);
    185     threadsStarted++;
    186     ALOGD("thread[%d] wait to run...", (int)arg);
    187     pthread_cond_wait(&waitCond, &waitLock);
    188     pthread_mutex_unlock(&waitLock);
    189     doAtomicTest((int) arg);
    190 
    191     return NULL;
    192 }
    193 
    194 static void doBionicTest()
    195 {
    196 #ifdef TEST_BIONIC
    197     /*
    198      * Quick function test on the bionic ops.
    199      */
    200     int prev;
    201     int initVal = 7;
    202     prev = __atomic_inc(&initVal);
    203     __atomic_inc(&initVal);
    204     __atomic_inc(&initVal);
    205     ALOGD("bionic __atomic_inc: %d -> %d
    ", prev, initVal);
    206     prev = __atomic_dec(&initVal);
    207     __atomic_dec(&initVal);
    208     __atomic_dec(&initVal);
    209     ALOGD("bionic __atomic_dec: %d -> %d
    ", prev, initVal);
    210     prev = __atomic_swap(27, &initVal);
    211     ALOGD("bionic __atomic_swap: %d -> %d
    ", prev, initVal);
    212     int swap_ret = __atomic_cmpxchg(27, 72, &initVal);
    213     ALOGD("bionic __atomic_cmpxchg: %d (%d)
    ", initVal, swap_ret);
    214 #endif
    215 }
    216 
    217 int main(void)
    218 {
    219     ALOGW("-------------AtomicTester begin--------------");
    220 
    221     pthread_t threads[THREAD_COUNT];
    222     void *(*startRoutine)(void*) = atomicTest;
    223     int64_t startWhen, endWhen;
    224 
    225 #if defined(__ARM_ARCH__)
    226     ALOGD("__ARM_ARCH__ is %d", __ARM_ARCH__);
    227 #endif
    228 #if defined(ANDROID_SMP)
    229     ALOGD("ANDROID_SMP is %d", ANDROID_SMP);
    230 #endif
    231     ALOGD("Creating threads...");
    232 
    233     int i;
    234     for (i = 0; i < THREAD_COUNT; i++) {
    235         void* arg = (void*) i;
    236         if (pthread_create(&threads[i], NULL, startRoutine, arg) != 0) {
    237             ALOGD("thread create failed");
    238         }
    239     }
    240 
    241     /* wait for all the threads to reach the starting line */
    242     while (1) {
    243         pthread_mutex_lock(&waitLock);
    244         if (threadsStarted == THREAD_COUNT) {
    245             ALOGD("Starting test!");
    246             startWhen = getRelativeTimeNsec();
    247             pthread_cond_broadcast(&waitCond);
    248             pthread_mutex_unlock(&waitLock);
    249             break;
    250         }
    251         pthread_mutex_unlock(&waitLock);
    252         usleep(100000);
    253     }
    254 
    255     for (i = 0; i < THREAD_COUNT; i++) {
    256         void* retval;
    257         if (pthread_join(threads[i], &retval) != 0) {
    258             ALOGE("thread join (%d) failed
    ", i);
    259         }
    260     }
    261 
    262     endWhen = getRelativeTimeNsec();
    263     if (USE_ATOMIC_TYPE == 3) {
    264         ALOGD("test pthread_mutex_t with %s", USE_NOINLINE==1 ? "noinline":"always_inline");
    265     }
    266     ALOGD("TYPE=%d, All threads stopped, duration time is %.6fms
    ",
    267         USE_ATOMIC_TYPE, (endWhen - startWhen) / 1000000.0);
    268 
    269     /*
    270      * Show results; expecting:
    271      *
    272      * incTest = 5000000
    273      * decTest = -5000000
    274      * addTest = 7500000
    275      * casTest = 10000000
    276      */
    277     ALOGD("incTest = %d
    ", incTest);
    278     ALOGD("decTest = %d
    ", decTest);
    279     ALOGD("addTest = %d
    ", addTest);
    280     ALOGD("casTest = %d
    ", casTest);
    281 
    282     doBionicTest();
    283 
    284     ALOGW("-------------AtomicTester end--------------");
    285 
    286     return 0;
    287 }
    View Code

      测试手段:10个线程,同时操作同一个全局变量,操作次数为50k次,测算耗时。

    测试时,通过宏#define USE_ATOMIC_TYPE 1 来控制测试方法的选择。

      3.1. 使用cutils实现的原子操作函数:

      

      3.2. 使用bionic中实现的原子操作函数(其实是gcc build-in api):

      

      3.3. 使用常用的pthread_mutex_t来保护全局变量:

        测试inline:(无函数调用开销,速度快)

      

        测试no-inline:(有函数调用开销,速度稍慢)

      

      3.4. 不使用任何锁来保护:(结果可能出错)

      

      3.5. 不使用任何锁来保护+sleep:(结果一定出错)

      

      3.6. pthread_mutex_lock+sleep:

      

      经以上对比,可以得到,速度方面:__atomic_inc() > android_atomic_inc() >  pthread_mutex_lock()

      其他补充:

    • 编译器gcc的build-in实现__sync_fetch_and_add()性能最好。
    • 测试机器硬件平台为Cortex-A53。
    • inline函数因为少了函数调用开销,速度较快,但会导致代码膨胀,因为其是在“预-编-汇-链”步骤中的预处理阶段进行了代码展开。
    • 对变量自增操作,如果不加锁或不用原子操作,结果仍有可能正确,毕竟自增操作的几条指令被别的线程打断的概率非常低(另外一个极端的猜测是——在一个cpu时间片周期内,就做完了50k的自增,这样不被打断时结果就不出错了)。 但是,不加锁并且sleep,被打断的概率非常大,参考3.5的测试结果,导致结果出错(其实解释起来也有点牵强,在sleep将要执行前,自增操作的几条指令已经全部完成)。
    • 加sleep()会导致耗时大大增加,因为其休眠时长不精确,再扩大50k倍后,导致耗时非常大。但是,usleep(1)不精确,即使扩大5倍后,单个线程执行耗时为:5us*50k=250ms,最后10个线程累加为:250ms*10=2.5s,但是3.5/3.6的测试显示总耗时大约为:39s,因此猜测得出下条的结论:
    • sleep是线程被调用时,让出系统资源,放弃当前cpu时间片并阻塞指定时间,让其他线程可以占用cpu。因此,耗时增加如此之多,大概就是大部分时间耗在了线程上下文切换上,每自增一次,线程抢占一次。但是,这个分析可能有误,因为top看cpu占用时,远低于100%,如果不sleep的话,基本上就是100%。后面需要辅助用其他手段来求证。。。
    • 关于cpu切换时间片的一点说明,时间片设得太短会导致过多的进程切换,降低了CPU效率;而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100毫秒通常是一个比较合理的折衷。
  • 相关阅读:
    XML基础总结
    异常处理
    集合总结
    事件源与监听器
    JAVA中的GUI---swing 和awt
    docker搭建wordpress
    Jenkins--第四关_扩展
    docker 安装centos 7
    Jenkins_第五关_系统管理(1)
    Jenkins--第三关_Gitlab安装和配置(续)
  • 原文地址:https://www.cnblogs.com/Dreaming-in-Gottingen/p/14589808.html
Copyright © 2011-2022 走看看