我们先从amdahl定律[1]说起,这是一个经验公式,描述了一个系统中,任务执行的延迟加速比。任务的延迟加速描述如下:
    4ddfe7ea1f14ac8da03a6eda65459d1f8d85f6b9-2.svg
    其中,p是整个任务中可以被并行的部分,s则是执行任务的并行线程数。假设当p=1/2时,也是说一个任务中可并行的部分占比整个任务的一半,这个时候即使并行的线程数s=∞,加速比也只有2,任务只能快一倍。所以在一个多核处理器系统中,即使核数再多,线程再多,任务可并行的部分决定了最大的加速比。因此并发编程中最关键的部分是将尽可能地扩大任务中可并行的部分,这也是后续内容的核心。

    上一节中,我举过一个累加的例子,在线程数增加的情况下,延迟反而没有减少。原因就是对内存中同一个变量的加一操作本身就是一个不可并行的任务,再加上多线程并行执行的情况下,该变量更新的cache冲突会变得很严重(后续讲解cache coherence protocol[2]的时候会细说这里),导致延迟不但没有得到优化,反而还下降了。这里我用c++代码展示这个累加的例子:

    1. void th(int* cnt)
    2. {
    3. while (__sync_fetch_and_add(cnt, 1) < max_cnt) {}
    4. return;
    5. }
    6. void Test(const int thread_cnt)
    7. {
    8. vector<thread> ths;
    9. int cnt = 0;
    10. uint64_t c = rdtscp();
    11. for (int i = 0; i < thread_cnt; i++) {
    12. ths.push_back(thread(&th, &cnt));
    13. }
    14. for (int i = 0; i < thread_cnt; i++) {
    15. ths.at(i).join();
    16. }
    17. uint64_t d = rdtscp();
    18. cout<<"thread cnt" << thread_cnt <<" used:"<<d - c<<endl;
    19. }
    20. int main()
    21. {
    22. Test(1000);//为了预热
    23. Test(1); //latency : 1702538261
    24. Test(10); //latency : 4816327883
    25. Test(100); //latency : 5773059772
    26. Test(1000);//latency : 5850909219
    27. return 0;
    28. }

    结果:

    线程数 延迟
    1 1702538261
    10 4816327883
    100 5773059772
    1000 5850909219

    用amdahl定律作为指导思想,我们需要扩大任务中可并行的部分。为了达到这个目标,需要将原本单个变量拆解为多个变量,将原本累加100000000次的任务,拆解为n个累加100000000/n次的任务,从而增加amdahl定律中,可并行化的部分p,因而提升任务执行的加速比。c++代码如下:

    1. struct CacheAlignT
    2. {
    3. int cnt = 0;
    4. } __attribute__ ((aligned (64)));
    5. void th(int target_cnt, CacheAlignT* t)
    6. {
    7. while (__sync_fetch_and_add(&t->cnt, 1) < target_cnt) {}
    8. return;
    9. }
    10. void Test(const int thread_cnt)
    11. {
    12. vector<thread> ths;
    13. vector<CacheAlignT> cnts(thread_cnt);
    14. uint64_t c = rdtscp();
    15. for (int i = 0; i < thread_cnt; i++) {
    16. ths.push_back(thread(&th, max_cnt / thread_cnt, &cnts[i]));
    17. }
    18. for (int i = 0; i < thread_cnt; i++) {
    19. ths.at(i).join();
    20. }
    21. uint64_t d = rdtscp();
    22. cout<<"thread cnt" << thread_cnt <<" used:"<<d - c<<endl;
    23. }
    24. int main()
    25. {
    26. Test(10);//为了预热
    27. Test(1); // latency : 1699145424
    28. Test(10); // latency : 171877508
    29. Test(100); // latency : 104265484
    30. Test(1000);// latency : 102759167
    31. return 0;
    32. }

    结果:

    线程数 延迟
    1 1699145424
    10 171877508
    100 104265484
    1000 102759167

    可以看到将累加任务拆开后,加大并发线程数是可以减少延迟的。但是细心的同学应该发现了,我将CacheAlignT指定为64Byte也就是cache line对齐。如果不把CacheAlignT指定cache line对齐结果又如何呢?

    线程数 延迟
    1 1695433169
    10 4660681022
    100 5628817031
    1000 5698281676

    可以看到,只是简单的拆开变量并不能提升性能,必须将拆开的变量打散到不重复的cache line上,才会有性能的提升。后面的章节会详细介绍cache coherence protocol的相关内容,再来解答这其中的奥秘。

    [1] https://en.wikipedia.org/wiki/Amdahl%27s_law
    [2] https://en.wikipedia.org/wiki/Cache_coherence