How the program goes slow

最近读了一些关于program performance的文章。结合自己的一些编程体会,总结如下。

(1) Algorithm Complexity.

一般情况,我们写代码首要的任务是要让它work,之后才考虑performance的问题。 但如果写代码的时候总是留意自己的算法不会是什么坏事,“不要进行不成熟的优化”,但也”不要进行不必要的劣化“是写代码时的一个很重要的守则.

总是优先考虑选择O(N)的算法,然后是O(Nlog(N))...


(2) Spin Looping.

永远考虑不要让自己的程序”忙“等待某种资源(除非你确定要等待资源能够马上就需(在Driver里面我们或许会碰到Spin loop and wait的case))。考虑使用Event Driven/Conditional Var - Mutex/Messaging 等方式解决忙等的问题。


(3) Hanging.

程序挂起有多种原因:等待用户输入,等待网络IO,磁盘I/O,死锁等等。可以考虑使用Async I/O或timed I/O interface解决IO死等的问题。对于解决死锁这类问题,已有很多文章有所讨论,理论上感觉有多种方法。但实际中我使用过的是对资源排序(level 深度),锁资源的时候,先从level低的开始锁(以后有待补偿)。


(4) Thrashing.

写程序的时候不但要考虑memory efficiency,还要考虑cache friendly。毕竟现在是多核时代,memory总体来说还是够用的。现代OS/Compiler都会采取各种优化手段来让程序跑的更快(Instruction Pipe Line / Prefetch etc),要尽量让Compiler和OS”喜欢“和你的程序一起工作(OS/CPU 喜欢sequentially访问数据)。举两个例子:

例子1: 对二维数组求和。应为C++是一般按照row来连续存放一个二维数组的,所以第一种方法比第二种方法效率更高。因为我们在访问连续内存的时候,OS/CPU会进行prefetch,这时候将要读的数据一般情况下已经在上一次访问操作的时候读入CPU cache里面了;而第二种方案,每一次访问的数据都在scatter在不连续的内存里面,此时会造成cache thrashing,也就是说prefetch失效。prefetch失效需要做更多的工作,比如把原来cache里面的数据清除,载入新的prefetch的数据,如此之类。显然performance会更糟。

按row优先取

for(int row = 0; row < N; ++row)
   for(int col = 0; j < N; ++col)
      sum += arr[row][col];

按col优先取

for(int col = 0; j < N; ++col)
   for(int row = 0; row < N; ++row)
      sum += arr[row][col];


例子2: 假设两个CPU,64bit cache line size, int 的size 是32bit。一个CPU专门访问下面结构中的push,另外一个专门访问下面结构中的pop。假定同一个instance被load到两个CPU的cache里面去了,当其中一个CPU改写push的时候,需要通知另外一个CPU更新它的cache,因为此时另外一个CPU的cache是脏的,反之亦然。这种情况被称为False Sharing, Cache Ping Pong等等。解决的办法很简单,”Keep the unrelated data member far far away“,只需要在加入相应的padding就ok了,如struct MemoryWastedButCacheFriendly所示。

struct MemoryPackedButNotCacheFriendly
{
    int push;
    int pop;
    ...
};

struct MemoryPackedButNotCacheFriendly
{
    int push;
    char pad1[CACHE_LINE_SIZE - sizeof[int]];
    int pop;
    char pad2[CACHE_LINE_SIZE - sizeof[int]];
    ...
};
  

(5) Sharing - Lock Contention.

可以说”资源共享“是多核多线程的程序中影响performance的罪魁祸首。有了资源共享(读写),就意味着一定需要Lock来串行地访问共享资源。不经意地,程序的hot spot就出现在了访问共享资源上面。所以尽量不要共享数据;如果不得已,把需要共享和不需要的共享的数据分离(separate them far far away)。让访问共享资源的代码竟可能的短,快速(减少了hold lock的时间)。尽量避免hold lock,然后去做磁盘/网络IO,一般情况可以使用Active Object解决这个问题(但永远不要over engineering)。锁的粒度方面也可以适当调整,太粗,太细都是不好的。实验和数据可以指导我们如何选择。


(6) Memory Copy.

尽量避免copy内存中的数据,虽然内存操作是比较快的。可以考虑使用C++11Moving/unique_ptr特性。但请记住,在程序的Clarity和Performance方面做审慎的选择(对一般的APP,我永远选择程序的Clarity).


(7) Context Switch.

多线程程序很令人头疼的一件事就是thread context switch的开销。虽然在OS层面已经解决”惊群“效应,但过多的thread context switch会导致程序的性能急剧下降,最烂的时候,程序光顾着做context switch,真正的BLO从来没机会执行。如何减少Context Switch是比较困难的,可以思考几个问题:程序是否需要spawn这么多线程,是否可以采取其它方案(Async IO),是否可以采用thread pool + blocking queue的方案减少线程数量和lively 创建线程的开销?





你可能感兴趣的:(thread,多线程,cache,struct,performance,compiler)