linux下多进程和多线程的性能比较

446 views
Skip to first unread message

Yan Wang

unread,
Nov 28, 2014, 12:02:27 PM11/28/14
to ustc...@googlegroups.com
一直以来接受的教育都是linux下面fork()的overhead(中文是啥?)极小,和多线程的性能几乎相同。同时因为线程的共享内存模型需要在多核处理器之间同步,反而性能会更慢。所以我们应该多用多进程而不是多线程。(来源:The Art of Unix Programming, StackOverflow)

但我做了个小实验,发现了正好相反的结果:fork()比pthread_create()慢了大概5倍。。所以想问问大家第一段介绍的这个结论是合理的吗?是不是我的代码有问题?我们在什么情况下应该用多进程什么情况下应该用多线程呢?

编译用的是g++ benchmark.cpp -lpthread -std=c++11 -o benchmark
time ./benchmark t或者time ./benchmark p来测试运行时间。在我的机器上结果如下:

$ time ./benchmark p && time ./benchmark t                                
Process...
./benchmark p  0.03s user 1.18s system 25% cpu 4.757 total
Thread...
./benchmark t  0.67s user 0.24s system 105% cpu 0.867 total

因为主要是为了测试多线程/多进程的overhead,所以都是新开一个线程/进程,立马等它结束,然后再开新的。没有测试他们并行一起跑的效果。

谢谢!

XIAO Qi

unread,
Nov 28, 2014, 12:10:24 PM11/28/14
to USTC_LUG
在 2014年11月28日 下午6:02,Yan Wang <gra...@gmail.com> 写道:
> 一直以来接受的教育都是linux下面fork()的overhead(中文是啥?)极小,和多线程的性能几乎相同。同时因为线程的共享内存模型需要在多核处理器之间同步,反而性能会更慢。所以我们应该多用多进程而不是多线程。(来源:The
> Art of Unix Programming, StackOverflow)
>

overherad = 开销。

> 但我做了个小实验,发现了正好相反的结果:fork()比pthread_create()慢了大概5倍。。所以想问问大家第一段介绍的这个结论是合理的吗?是不是我的代码有问题?我们在什么情况下应该用多进程什么情况下应该用多线程呢?
>
> 我的代码贴在https://gist.github.com/grapeot/5853a4d4f414f170555b
> 编译用的是g++ benchmark.cpp -lpthread -std=c++11 -o benchmark
> 用time ./benchmark t或者time ./benchmark p来测试运行时间。在我的机器上结果如下:
>
> $ time ./benchmark p && time ./benchmark t
> Process...
> ./benchmark p 0.03s user 1.18s system 25% cpu 4.757 total
> Thread...
> ./benchmark t 0.67s user 0.24s system 105% cpu 0.867 total
>
> 因为主要是为了测试多线程/多进程的overhead,所以都是新开一个线程/进程,立马等它结束,然后再开新的。没有测试他们并行一起跑的效果。
>
> 谢谢!
>

本来写了一点,不过想到 ustc-lug 里有不少比我懂得多的巨巨,就不献丑了……我只能说“线程比进程快”这个结论一点都不奇怪 ╮( ̄▽ ̄")╭


--
Best regards,
肖骐 XIAO Qi

Yan Wang

unread,
Nov 28, 2014, 1:23:43 PM11/28/14
to ustc...@googlegroups.com
谢谢指教!对对是开销~

windows下面的确是这样的,但看了一些书籍和材料,大家都说linux下面进程线程差不多,所以想验证一下,但没想到差这么多倍。。
--
-- 来自USTC LUG
请使用gmail订阅,不要灌水。
更多信息more info:http://groups.google.com/group/ustc_lug?hl=en?hl=en
---
You received this message because you are subscribed to the Google Groups "USTC_LUG" group.
To unsubscribe from this group and stop receiving emails from it, send an email to ustc_lug+u...@googlegroups.com.
To post to this group, send email to ustc...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

XIAO Qi

unread,
Nov 28, 2014, 1:30:56 PM11/28/14
to USTC_LUG
在 2014年11月28日 下午7:23,Yan Wang <gra...@gmail.com> 写道:
> 谢谢指教!对对是开销~
>
> windows下面的确是这样的,但看了一些书籍和材料,大家都说linux下面进程线程差不多,所以想验证一下,但没想到差这么多倍。。
>

Linux 的进程和线程都是用 clone syscall 实现的,从这一方面来说的确是“差不多”……

但是因为进程需要分出来的东西要多得多所以性能差是可以想见的恩……

Guo, Jiahua

unread,
Nov 28, 2014, 1:32:37 PM11/28/14
to ustc...@googlegroups.com
从那篇文章和我的理解,进程和线程的性能开销大概有三个方面吧:
  • 创建
  • 上下文切换
  • 共享内存的缓存一致性


创建

创建进程和线程的过程类似。

差别只是创建进程时会把相关的数据结构都复制一份,而创建线程时部分数据结构是共享的,如页表。因此,创建线程自然比创建进程快。

下面的第906行就是线程间共享内存的代码:

http://lxr.free-electrons.com/source/kernel/fork.c?v=3.17#L880


上下文切换

这里有篇旧文测试了进程和线程的切换开销:

How long does it take to make a context switch?

进程上下文切换会更换页表(cr3),且导致相关缓存失效。线程上下文切换不需要。


共享内存的缓存一致性

原文:

In fact, if you are on a multi-processor system, not sharing may actually be beneficial to performance: if each task is running on a different processor, synchronizing shared memory is expensive.

这块细节我不清楚,我说我的理解,如果有错误请指正。

如果一块内存被多个 CPU 频繁使用,它就会持续出现在多个 CPU 的 L1 Cache 中。如果其中某个 CPU 修改了这块内存,其他 CPU 中相应的 L1 Cache 会更新或失效以保持缓存一致性,这是有代价的。

因此,如果多个核频繁读写同一块内存会比独立的读写各自的内存慢。


如果只是多个 CPU 频繁读取一块内存,并不修改,情况似乎不一样。

根据 Cache_coherenceReference 第30页底的描述,Nehalem 使用的同步协议是 MESIF

如果一块内存是只读的(如 .text),它在缓存中应当是处于 Shared 状态。我猜测读取 Shared 状态的缓存是没有额外代价的。


因此,这个问题和进程或线程无关,只和是否使用可修改的共享内存有关。


总结

线程创建比进程快。

线程上下文切换比进程快。

共享内存的缓存一致性和线程或进程没有本质的关联。

所以,从这几方面看,线程比进程快。



--

Yan Wang

unread,
Nov 28, 2014, 1:56:25 PM11/28/14
to ustc...@googlegroups.com
感谢各位的回复!只是从这些原因(clone syscall所以成本大体差不多,页表的拷贝应该不会花太多时间,我的代码里没有涉及到频繁的上下文切换和缓存更新)出发,进程比线程也慢不了太多,感觉开销多50%已经是一个很激进的估计了,为什么实际测出来的性能是线程的1/5还不到呢。。这个感觉很奇怪。。是不是我们理解的哪里有问题或者我的测试代码有问题?

Guo, Jiahua

unread,
Nov 28, 2014, 2:49:26 PM11/28/14
to ustc...@googlegroups.com
你这程序只是测进程/线程创建时间吧。

单说内存。
http://lxr.free-electrons.com/source/kernel/fork.c?v=3.16#L865
==========
865 static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
866 {
867         struct mm_struct *mm, *oldmm;
868         int retval;
869 
870         tsk->min_flt = tsk->maj_flt = 0;
871         tsk->nvcsw = tsk->nivcsw = 0;
872 #ifdef CONFIG_DETECT_HUNG_TASK
873         tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
874 #endif
875 
876         tsk->mm = NULL;
877         tsk->active_mm = NULL;
878 
879         /*
880          * Are we cloning a kernel thread?
881          *
882          * We need to steal a active VM for that..
883          */
884         oldmm = current->mm;
885         if (!oldmm)
886                 return 0;
887 
888         /* initialize the new vmacache entries */
889         vmacache_flush(tsk);
890 
891         if (clone_flags & CLONE_VM) {
892                 atomic_inc(&oldmm->mm_users);
893                 mm = oldmm;
894                 goto good_mm;
895         }
896 
897         retval = -ENOMEM;
898         mm = dup_mm(tsk);
899         if (!mm)
900                 goto fail_nomem;
901 
902 good_mm:
903         tsk->mm = mm;
904         tsk->active_mm = mm;
905         return 0;
906 
907 fail_nomem:
908         return retval;
909 }
==========

创建线程走的是第 892 行,这就几条指令吧。
创建进程走的是第 898 行,dup_mm 相对而言就复杂多了。


此外,我这里 pthread_create 调用 clone 时的 flags 是
CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID

而 fork 对应的 flags 是
CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD

相比之下,pthread_create 比 fork 多了以下 flags:
CLONE_VM
CLONE_FS
CLONE_FILES
CLONE_SIGHAND
CLONE_THREAD
CLONE_SYSVSEM
CLONE_SETTLS
CLONE_PARENT_SETTID


再看 copy_process 中的代码片段:
http://lxr.free-electrons.com/source/kernel/fork.c?v=3.16#L1318
==========
1330         /* copy all the process information */
1331         retval = copy_semundo(clone_flags, p);
1332         if (retval)
1333                 goto bad_fork_cleanup_audit;
1334         retval = copy_files(clone_flags, p);
1335         if (retval)
1336                 goto bad_fork_cleanup_semundo;
1337         retval = copy_fs(clone_flags, p);
1338         if (retval)
1339                 goto bad_fork_cleanup_files;
1340         retval = copy_sighand(clone_flags, p);
1341         if (retval)
1342                 goto bad_fork_cleanup_fs;
1343         retval = copy_signal(clone_flags, p);
1344         if (retval)
1345                 goto bad_fork_cleanup_sighand;
1346         retval = copy_mm(clone_flags, p);
1347         if (retval)
1348                 goto bad_fork_cleanup_signal;
1349         retval = copy_namespaces(clone_flags, p);
1350         if (retval)
1351                 goto bad_fork_cleanup_mm;
1352         retval = copy_io(clone_flags, p);
1353         if (retval)
1354                 goto bad_fork_cleanup_namespaces;
1355         retval = copy_thread(clone_flags, stack_start, stack_size, p);
1356         if (retval)
1357                 goto bad_fork_cleanup_io;
==========

创建线程相比于创建进程,少复制了不少数据结构。



这样看来,fork 时间是 pthread_create 5倍也不是完全没有可能嘛,除非有更详细的分析或定量数据作说明。
或者说,我不知道怎么估算出 fork 和 pthread_create 的相对运行时间。

顺便贴以下我这(虚拟机中)的测试程序运行结果:
==========
$ time ./t p
Process...

real    0m3.032s
user    0m0.000s
sys     0m1.044s

$ time ./t t
Thread...

real    0m1.022s
user    0m0.024s
sys     0m1.184s
==========


Yan Wang

unread,
Nov 28, 2014, 3:44:32 PM11/28/14
to ustc...@googlegroups.com
非常感谢你的详细回复!现在有些明白了~在创建过程中进程的开销还是比较大的。从相关内核代码来看,新进程复制的数据结构的开销的确是几倍于新建线程。只是现在开始不解为什么之前那么多人都持linux下应该用多进程的观点。。是我的感受有误还是真的有这样的观点。。?

我的测试程序其实不是很合理,因为线程/进程体非常短。这样的测试只能说明进程不是一个轻量的概念,不要没事干去建几万个玩(相比之下F#里面几百万个线程是常见的轻量的操作,我猜node.js也是这样)。也许在实际计算中没有太大差别。

Zhang Cheng

unread,
Nov 28, 2014, 9:31:57 PM11/28/14
to USTC LUG
我稍微歪个楼。关于pthread和fork的性能区别我不了解,平时没有使用的经验。看到这个话题,想起前几天在另外一个地方看到关于vfork的讨论,分享一下摘要。

早年的时候,fork() 需要copy整个父进程的data space,开销巨大,然而在大多数使用场景下,都是fork()之后立刻exec(),所以这个开销是没有意义的。于是BSD就引入了vfork(),vfork()不会copy整个data space,而是借用父进程的空间,直到调用execve()。然而,vfork()是一个大坑,因为它和父进程是共用一个内存空间的,所以有很多限制,比如里面不能用printf,不能执行return(要退出只能用exit()),总之,任何会修改当前函数栈上内容的事情都不能做,否则父进程就会出问题。后来Linux下fork()实现了copy-on-write,性能就上去了,于是vfork()就正式被列入“黑名单”(我加引号的意思不是说不许用,而是强烈不推荐使用)。BSD好像后来也跟进了吧。(man vfork能看到这些信息)

另外,在我的机器上运行你的代码:
$ time ./benchmark p
Process...

real 0m1.164s
user 0m0.012s
sys 0m0.228s

​$time ./benchmark t
Thread...

real 0m0.224s
user 0m0.160s
sys 0m0.076s
然后我对你的代码做了一个小修改,在循环里不进行pthread_join/wait,而是分别新开一个循环来join/wait,结果为:
​$ time ./benchmark p
Process...

real 0m0.344s
user 0m0.004s
sys 0m0.336s

$ ​time ./benchmark t
Thread...

real 0m0.118s
user 0m0.212s
sys 0m0.132s


Cheng,
Best Regards

Zhang Cheng

unread,
Nov 28, 2014, 10:04:46 PM11/28/14
to USTC LUG

2014-11-29 10:31 GMT+08:00 Zhang Cheng <steph...@gmail.com>:
另外,在我的机器上运行你的代码:
$ time ./benchmark p
Process...

real 0m1.164s
user 0m0.012s
sys 0m0.228s

​$time ./benchmark t
Thread...

real 0m0.224s
user 0m0.160s
sys 0m0.076s
然后我对你的代码做了一个小修改,在循环里不进行pthread_join/wait,而是分别新开一个循环来join/wait,结果为:
​$ time ./benchmark p
Process...

real 0m0.344s
user 0m0.004s
sys 0m0.336s

$ ​time ./benchmark t
Thread...

real 0m0.118s
user 0m0.212s
sys 0m0.132s

​对于这两个代码的差异,我的理解是这样的:

前者,pthread是顺序执行,顺序回收(并且是一个回收了再执行下一个),后者,pthread是乱序执行,顺序回收(且乱序执行时是并发执行),后者比前者快,应该是快在并发执行上。
前者,fork是顺序执行,顺序回收(同上),后者,fork是乱序执行,乱序回收(且乱序执行时是并发执行),后者比前者快,一方面在并发执行上,一方面在乱序回收的速度不受子进程调度顺序的影响。​

​我没有找到简单的方法来测试pthread乱序回收的效果,所以没有测试。不过从第一组结果和第二组结果的比较来看,第一组测试的数据中,有大量的时间是消耗在调度上,而不是创建、销毁上的。我相信,第二组的时间里,应该还是有不少时间是花在调度上的。
同时,从使用场景看,这里创建、调度、销毁10000个进程,也就花了不到1s的时间,这么小的开销,3倍5倍也没那么可怕,而且实际的应用场景下估计很少会有这样的情况。(我见过的都属于高性能服务方面的,比如某mongodb实例有上万个线程,varnish有上千个线程等等)。不过在高性能的场景下,我确实很少见到用进程的,我觉得主要因素可能不是进程调度的性能差(当然也是原因之一),可能主要的麻烦是进程/线程间通信的效率问题。一般来说进程间通信的开销会比线程要大。​


--
Cheng,
Best Regards

Siliang Cao

unread,
Nov 28, 2014, 10:32:57 PM11/28/14
to ustc_lug
http://www.ibm.com/developerworks/cn/linux/l-threading.html
这个链接说明了为什么IBM和redhat的人为什么要给Linux加入原生线程特性,包括原生线程和copy-on-write进程的区别。


--

Bojie Li

unread,
Nov 29, 2014, 1:15:34 AM11/29/14
to USTC_LUG
2014-11-29 4:44 GMT+08:00 Yan Wang <gra...@gmail.com>:
非常感谢你的详细回复!现在有些明白了~在创建过程中进程的开销还是比较大的。从相关内核代码来看,新进程复制的数据结构的开销的确是几倍于新建线程。只是现在开始不解为什么之前那么多人都持linux下应该用多进程的观点。。是我的感受有误还是真的有这样的观点。。?

你在什么地方看到的 “Linux 下应该用多进程而非多线程“?前面的 StackOverflow 没有表达这个意思,只是说两个不同 CPU 上并行运行的线程同时修改一块共享内存,对 CPU 缓存的影响很大,但前面 Guo, Jiahua 已经指出这跟多线程还是多进程无关,进程之间也可以用 shared memory。

我觉得对相同任务的性能而言,多进程不会比多线程有性能优势。选择多进程而非多线程的原因可能是简化编程和增强模块之间的隔离性。

多个并发任务之间的通信一般有两种方式:共享内存和消息传递。共享内存的范式初看起来更简单直接,但要保证一致性往往需要加锁,一方面很难保证加锁方式的安全性(即不会出现 race condition)和活跃性(即不会发生死锁);另一方面锁是阻塞的,忙等则浪费 CPU,任务切换则有额外开销。消息传递的范式则写起来比较费脑子,不过 Scala、Erlang、Go 等为高并发而生的语言都采用消息传递的范式,因为它更自然地匹配了计算机内部的事件驱动模型。

================
顺便说一个多线程编程中的经典问题:false sharing。我们都知道 CPU 非对齐访问要访问两次总线,效率很低,因此编译器都知道把整数按照4字节对齐。但在多核多线程处理中,还有一种以 64 字节为单位的对齐。在我用的 Intel 处理器中 CPU 高速缓存是以 64 字节的行(cache line)为单位的,只要其中的一个字节缓存不命中,就要从内存中读取整行;只要其中的一个字节被写入,就要通知其他处理器把相同地址的这一行清出高速缓存(缓存失效)。

如果我们有一个 16 线程的计算密集程序,每个线程 i = 0..15 用到一个 4 字节全局变量 ThreadGlobal[i],则每个线程写入全局变量时,其他处理器的全局变量缓存都要失效。本来各线程的全局变量互相不影响,可以分别缓存,但 CPU 缓存行的设计破坏了缓存,这就是 false sharing。

避免 false sharing 的方法很简单:让每个线程私有的全局变量按 64 字节(或处理器架构上的缓存行宽度)对齐。遗憾的是,编译器并不能自动做到这一点,需要把线程全局变量封装在结构体中,自己添加 padding。例如
struct ThreadGlobal {
    unsigned int data;
    unsigned int padding[15];
}; // total 64 bytes

struct ThreadGlobal _TG[NUM_THREADS];

Yan Wang

unread,
Nov 29, 2014, 10:06:09 AM11/29/14
to ustc...@googlegroups.com
2014-11-28 22:04 GMT-05:00 Zhang Cheng <steph...@gmail.com>:
同时,从使用场景看,这里创建、调度、销毁10000个进程,也就花了不到1s的时间,这么小的开销,3倍5倍也没那么可怕,而且实际的应用场景下估计很少会有这样的情况。(我见过的都属于高性能服务方面的,比如某mongodb实例有上万个线程,varnish有上千个线程等等)。不过在高性能的场景下,我确实很少见到用进程的,我觉得主要因素可能不是进程调度的性能差(当然也是原因之一),可能主要的麻烦是进程/线程间通信的效率问题。一般来说进程间通信的开销会比线程要大。​

​这个我比较赞成(也是上一个回复里面提到的),可能现在的开销已经小到不是性能瓶颈了。然后做决策就非常简单了,如果是单机多核程序,为了通信方便就用线程,如果是多机并行就只能用多进程了。​
 


--
Cheng,
Best Regards

Yan Wang

unread,
Nov 29, 2014, 10:15:00 AM11/29/14
to ustc...@googlegroups.com
2014-11-28 22:04 GMT-05:00 Zhang Cheng <steph...@gmail.com>:
前者,pthread是顺序执行,顺序回收(并且是一个回收了再执行下一个),后者,pthread是乱序执行,顺序回收(且乱序执行时是并发执行),后者比前者快,应该是快在并发执行上。
前者,fork是顺序执行,顺序回收(同上),后者,fork是乱序执行,乱序回收(且乱序执行时是并发执行),后者比前者快,一方面在并发执行上,一方面在乱序回收的速度不受子进程调度顺序的影响。​

​我没有找到简单的方法来测试pthread乱序回收的效果,所以没有测试。不过从第一组结果和第二组结果的比较来看,第一组测试的数据中,有大量的时间是消耗在调度上,而不是创建、销毁上的。我相信,第二组的时间里,应该还是有不少时间是花在调度上的。

​我不是太理解为什么第一组测试中大量的时间消耗在调度上。。我的理解是如果新建了进程,但是这个进程没有立马进入CPU而是被挂起。但如果是新建线程的话,因为​
当前被执行的进程还是这个进程,只有两个线程竞争这个执行权限,所以新的线程很快就被调度上然后退出了。是这个意思吗?(我的理解存疑,因为调度的最小单位不是进程而是线程?)

--
Cheng,
Best Regards

Yan Wang

unread,
Nov 29, 2014, 10:21:46 AM11/29/14
to ustc...@googlegroups.com
2014-11-28 22:32 GMT-05:00 Siliang Cao <silia...@gmail.com>:
http://www.ibm.com/developerworks/cn/linux/l-threading.html
这个链接说明了为什么IBM和redhat的人为什么要给Linux加入原生线程特性,包括原生线程和copy-on-write进程的区别
这个文档很有意思。谢谢!所以说,早期的linux其实是没有多线程这个东西的,(结合Zhang Cheng的评论),要并行就只能vfork()或者copy-on-write,用多进程的方式来弄。然后后来加入了一些多线程机制(根据文档,每个“线程”还是有自己的pid),然后加入了符合POSIX的原生线程。

也许这也部分解释了Bojie Li的问题:我是从哪里得到这样的印象的——因为linux的历史原因,原生线程在03年发布的kernel v2.6才被引入。可能一些文档没有来得及更新。

Yan Wang

unread,
Nov 29, 2014, 10:27:23 AM11/29/14
to ustc...@googlegroups.com
2014-11-29 1:15 GMT-05:00 Bojie Li <boj...@gmail.com>:
2014-11-29 4:44 GMT+08:00 Yan Wang <gra...@gmail.com>:
非常感谢你的详细回复!现在有些明白了~在创建过程中进程的开销还是比较大的。从相关内核代码来看,新进程复制的数据结构的开销的确是几倍于新建线程。只是现在开始不解为什么之前那么多人都持linux下应该用多进程的观点。。是我的感受有误还是真的有这样的观点。。?

你在什么地方看到的 “Linux 下应该用多进程而非多线程“?前面的 StackOverflow 没有表达这个意思,只是说两个不同 CPU 上并行运行的线程同时修改一块共享内存,对 CPU 缓存的影响很大,但前面 Guo, Jiahua 已经指出这跟多线程还是多进程无关,进程之间也可以用 shared memory。

 
​最初的印象来自于The Art of UNIX Programming: http://www.catb.org/esr/writings/taoup/html/ch03s01.html#id2892171
第三章提到

In the Unix experience, inexpensive process-spawning and easy inter-process communication (IPC) makes a whole ecology of small tools, pipes, and filters possible. 
...
​If an operating system makes spawning new processes expensive and/or process control is difficult and inflexible, you'll usually see all of the following consequences:
​...
  • Multithreading is extensively used for tasks that Unix would handle with multiple communicating lightweight processes.
​所以会觉得*nix下面 管道啊,多进程啊是很便宜的操作(我不觉得多进程会比多线程会快),又因为UNIX的设计哲学是不同的可执行程序干不同的事,所以要多用多进程。​
​但我现在反应过来这个和科学领域的并行高性能计算是没有必然联系的。。尤其这本书还提到了,programmers' time is much more expensive than machines' time...​
 
我觉得对相同任务的性能而言,多进程不会比多线程有性能优势。选择多进程而非多线程的原因可能是简化编程和增强模块之间的隔离性。

​这个赞同。​
 
 
多个并发任务之间的通信一般有两种方式:共享内存和消息传递。共享内存的范式初看起来更简单直接,但要保证一致性往往需要加锁,一方面很难保证加锁方式的安全性(即不会出现 race condition)和活跃性(即不会发生死锁);另一方面锁是阻塞的,忙等则浪费 CPU,任务切换则有额外开销。消息传递的范式则写起来比较费脑子,不过 Scala、Erlang、Go 等为高并发而生的语言都采用消息传递的范式,因为它更自然地匹配了计算机内部的事件驱动模型。

​这个很有启发。​谢谢!
 

================
顺便说一个多线程编程中的经典问题:false sharing。我们都知道 CPU 非对齐访问要访问两次总线,效率很低,因此编译器都知道把整数按照4字节对齐。但在多核多线程处理中,还有一种以 64 字节为单位的对齐。在我用的 Intel 处理器中 CPU 高速缓存是以 64 字节的行(cache line)为单位的,只要其中的一个字节缓存不命中,就要从内存中读取整行;只要其中的一个字节被写入,就要通知其他处理器把相同地址的这一行清出高速缓存(缓存失效)。

如果我们有一个 16 线程的计算密集程序,每个线程 i = 0..15 用到一个 4 字节全局变量 ThreadGlobal[i],则每个线程写入全局变量时,其他处理器的全局变量缓存都要失效。本来各线程的全局变量互相不影响,可以分别缓存,但 CPU 缓存行的设计破坏了缓存,这就是 false sharing。

避免 false sharing 的方法很简单:让每个线程私有的全局变量按 64 字节(或处理器架构上的缓存行宽度)对齐。遗憾的是,编译器并不能自动做到这一点,需要把线程全局变量封装在结构体中,自己添加 padding。例如
struct ThreadGlobal {
    unsigned int data;
    unsigned int padding[15];
}; // total 64 bytes

struct ThreadGlobal _TG[NUM_THREADS];

​原来还有这样的事情。。感觉这种深入到编译器和缓存级别的性能优化已经做到丧心病狂的地步了​
 
​。。。​

Bojie Li

unread,
Nov 30, 2014, 10:28:07 PM11/30/14
to USTC_LUG

The Art Of Unix Programming 里的很多话是要反着看的……我第一次读这本书时也被它洗脑了,不过现在认为,UNIX 跟 Windows 的区别里,在不同的应用场景里各有各的好处,没有绝对的优劣之分。这书确实是 UNIX 特性很好的总结,不过它的观点不能全盘接受,因为它考虑的都是 hacker 如何使用计算机(小工具拼接起来做大事),没有考虑最终用户的体验,也没有考虑高并发、并行计算之类机器的时间比程序员的时间更重要的地方。

比如现在流行的 fastcgi 取代 cgi,node.js 之类单个持续运行的 daemon 取代 php 之类每个请求一个进程,就是反 UNIX 哲学的,目的是提高性能和编程的灵活性。还有 SOAP、CORBA、.NET、RESTful 之类基于对象的进程间通信框架,就是打了 UNIX “一切基于文本流”的脸,现在恐怕很少有人写程序还自定义一套对象序列化的格式。

--

Yan Wang

unread,
Nov 30, 2014, 10:42:41 PM11/30/14
to ustc...@googlegroups.com
同意~比如Powershell很多时候因为对象化的管道,用起来就很爽~

Roy Zhang

unread,
Dec 1, 2014, 12:26:18 AM12/1/14
to ustc...@googlegroups.com
On 12/01/2014 11:28 AM, Bojie Li wrote:
> The Art Of Unix Programming 里的很多话是要反着看的……我第一次读这本书时也
> 被它洗脑了,不过现在认为,UNIX 跟 Windows 的区别里,在不同的应用场景里各
> 有各的好处,没有绝对的优劣之分。这书确实是 UNIX 特性很好的总结,不过它的
> 观点不能全盘接受,因为它考虑的都是 hacker 如何使用计算机(小工具拼接起来
> 做大事),没有考虑最终用户的体验,也没有考虑高并发、并行计算之类机器的时
> 间比程序员的时间更重要的地方。
>
> 比如现在流行的 fastcgi 取代 cgi,node.js 之类单个持续运行的 daemon 取代
> php 之类每个请求一个进程,就是反 UNIX 哲学的,目的是提高性能和编程的灵活
> 性。还有 SOAP、CORBA、.NET、RESTful 之类基于对象的进程间通信框架,就是打
> 了 UNIX “一切基于文本流”的脸,现在恐怕很少有人写程序还自定义一套对象序列
> 化的格式。
>

所以现在看《The Art Of Unix Programming》,用现在的话说,就多是“高端黑”了?

Thomas Copper

unread,
Dec 1, 2014, 3:04:39 AM12/1/14
to ustc_lug
其实KISS原则也好,小工具拼接做大事也好,最大的优势是降低了修改、维护、增加功能的成本。现在大工具在开发过程中不也是讲究模块化么。

要是一个大工具,出了一点点bug就要检查几乎所有代码,要修改很多处零零散散的地方,这怎么也说不上方便吧。

各个不相关的功能相互独立,尽量降低模块之间的耦合,我倒是觉得这是UNIX的KISS原则最好的概括。

而在“机器的时间比程序员的时间更重要的地方”,这里本来就应该解决通信问题,然后继续使用上面的原则。

Zhang Cheng

unread,
Dec 2, 2014, 1:13:57 AM12/2/14
to USTC LUG
2014-12-01 11:28 GMT+08:00 Bojie Li <boj...@gmail.com>:

The Art Of Unix Programming 里的很多话是要反着看的……我第一次读这本书时也被它洗脑了,不过现在认为,UNIX 跟 Windows 的区别里,在不同的应用场景里各有各的好处,没有绝对的优劣之分。这书确实是 UNIX 特性很好的总结,不过它的观点不能全盘接受,因为它考虑的都是 hacker 如何使用计算机(小工具拼接起来做大事),没有考虑最终用户的体验,也没有考虑高并发、并行计算之类机器的时间比程序员的时间更重要的地方。

比如现在流行的 fastcgi 取代 cgi,node.js 之类单个持续运行的 daemon 取代 php 之类每个请求一个进程,就是反 UNIX 哲学的,目的是提高性能和编程的灵活性。还有 SOAP、CORBA、.NET、RESTful 之类基于对象的进程间通信框架,就是打了 UNIX “一切基于文本流”的脸,现在恐怕很少有人写程序还自定义一套对象序列化的格式。


​我觉得“文本管道”跟年代也有关系。在早期的时候,计算机需要处理的事情比较简单,基本上都是处理文本,所以文本管道也就是很自然的事情。而且文本管道的好处是,“文本”仅仅是内容载体,而不约束格式,格式则可以自由约定,这样,模块独立更新就非常方便,比如有个 producer | consumer,突然有一天producer输出的信息里要增加一列,那么只要consumer考虑到这种前向发展的可能性,自动忽略stdin中多出的列,那么consumer可以不做修改而继续使用,但是如果传递对象的话,对象的格式(或者说类)变了,很可能consumer就挂掉了,而这里的consumer可能不止一种程序,而是100种程序。

另一方面,开源社区的开发模式是十分分散的,模块的开发者之间沟通很少或者几乎不沟通,这时候使用文本作为管道的规范也是好事。不像微软这样的公司,他可以定义一种对象管道的规范,并且推行这种规范,而开源社区一旦要搞对象管道,可以预见一下子会冒出许多中不同的对象管道的规范,然后开发者也不知道支持哪个好了。

我个人觉得,在开放的开源社区里不太可能出现被广泛接受的对象管道的规范。除非有某个公司主导大量的常用工具的开发,然后制定一套规范,并让这些工具都互相支持。但实际情况下,没有这样的公司。比如redhat如果这时候提出一套对象管道的规范,想让ls、wget、grep这类平时常用的工具都支持它,太难了。



--
Cheng,
Best Regards

Zhang Cheng

unread,
Dec 2, 2014, 1:23:21 AM12/2/14
to USTC LUG

2014-12-01 16:04 GMT+08:00 Thomas Copper <univers...@gmail.com>:
其实KISS原则也好,小工具拼接做大事也好,最大的优势是降低了修改、维护、增加功能的成本。现在大工具在开发过程中不也是讲究模块化么。

要是一个大工具,出了一点点bug就要检查几乎所有代码,要修改很多处零零散散的地方,这怎么也说不上方便吧。

各个不相关的功能相互独立,尽量降低模块之间的耦合,我倒是觉得这是UNIX的KISS原则最好的概括。

​有好处的时候,往往也意味着有一些坏处,得到的同时通常伴有失去。

比如Debian,Debian是无法做到像gentoo那样极致的性能优化的,因为Debian的目的是universal os,他的目标是尽可能多的支持各种设备。debian这两年才刚刚放弃支持i386,最低支持i486。可见,其实用debian时,常用CPU里面大量的优化指令都没有被使用到。

KISS是有很多的好处,但我觉得KISS出现的根本原因是开源社区只能这么玩。增强各模块之间的耦合性,往往可以获得更好的性能(或者应该反过来说,要获得更好的性能,许多时候不得不增加模块之间的耦合性),然而,增加耦合性,就需要开发者之间更多的沟通,而这种沟通成本,对于开源社区来说太高了。除了沟通之外,还需要在开发者之间达成一致的目标和想法,这也是几乎不可能的。所以,KISS适合开放的、自由的开源社区,而开源社区也只能玩KISS。



--
Cheng,
Best Regards
Reply all
Reply to author
Forward
0 new messages