Wheatserver与Gevent作为WSGI服务器的性能评测

Posts in category 性能评测与优化Wheatserver与Gevent作为WSGI服务器的性能评测[url=http://www.wzxue.com/wheatserver与gevent作为wsgi服务器的性能评测/]

三5th
2013

[/url]
Leave a Comment
Written by 麦子麦http://www.wzxue.com/category/性能优化/
Wheatserver是通用的C实现的通用服务器以及框架,更多的请参考Wheatserver
WSGI应用服务器众多,较为出色的有uWSGI,Gunicorn,gevent和Apache mod_wsgi。性能评测参考http://nichol.as/benchmark-of-python-web-servers

为了比较Wheatserver的性能并且为了省事,我选取了在该评测中性能最为出色的gevent实现的WSGI应用服务器,主要考查了内存消耗,简单App下相应速度,带数据库连接的相应速度和带搜索的服务访问能力。Http测试客户端使用Apache ab。

由于Wheatserver是多工作进程模式,而gevent是单进程单线程模式,所以为了避免Wheatserver多进程的干扰,我们同样只使用一个进程的Wheatserver。

同样是Hello world应用

?

[table=98%]
[tr][td]

1

2

3

4

[/td][td]
#!/usr/bin/python
def application(env, start_response):
start_response(‘200 OK’, [(‘Content-Type’, ‘text/html’)])
return [“hello world”]

[/td][/tr]
[/table]

Server Software: geventServer Hostname: 127.0.0.1Server Port: 8088Document Path: /Document Length: 18 bytesConcurrency Level: 50Time taken for tests: 0.056 secondsComplete requests: 100Failed requests: 0Write errors: 0Total transferred: 13800 bytesHTML transferred: 1800 bytesRequests per second: 1773.33 [#/sec] (mean)Time per request: 28.196 [ms] (mean)Time per request: 0.564 [ms] (mean, across all concurrent requests)Transfer rate: 238.98 [Kbytes/sec] receivedConnection Times (ms) min mean[+/-sd] median maxConnect: 0 1 1.2 1 7Processing: 2 21 10.3 21 50Waiting: 2 21 10.5 21 50Total: 3 23 10.7 23 50Percentage of the requests served within a certain time (ms) 50% 23 66% 30 75% 31 80% 32 90% 35 95% 39 98% 49 99% 50 100% 50 (longest request)Server Software: wheatserverServer Hostname: 127.0.0.1Server Port: 10828Document Path: /Document Length: 13 bytesConcurrency Level: 50Time taken for tests: 0.021 secondsComplete requests: 100Failed requests: 0Write errors: 0Total transferred: 15500 bytesHTML transferred: 1300 bytesRequests per second: 4704.55 [#/sec] (mean)Time per request: 10.628 [ms] (mean)Time per request: 0.213 [ms] (mean, across all concurrent requests)Transfer rate: 712.12 [Kbytes/sec] receivedConnection Times (ms) min mean[+/-sd] median maxConnect: 0 1 0.7 1 2Processing: 1 7 2.4 8 10Waiting: 1 7 2.4 8 10Total: 2 8 1.9 8 11Percentage of the requests served within a certain time (ms) 50% 8 66% 9 75% 9 80% 9 90% 9 95% 10 98% 10 99% 11 100% 11 (longest request)
简单App下,平均相应时间是gevent的二分之一,主要是没有IO操作,C实现的Wheatserver自然更快。
Server Software: wheatserverServer Hostname: 127.0.0.1Server Port: 10828Document Path: /Document Length: 11995 bytesConcurrency Level: 5Time taken for tests: 0.709 secondsComplete requests: 100Failed requests: 0Write errors: 0Total transferred: 1226700 bytesHTML transferred: 1199500 bytesRequests per second: 141.08 [#/sec] (mean)Time per request: 35.441 [ms] (mean)Time per request: 7.088 [ms] (mean, across all concurrent requests)Transfer rate: 1690.05 [Kbytes/sec] receivedConnection Times (ms) min mean[+/-sd] median maxConnect: 0 0 0.0 0 0Processing: 9 35 3.8 35 42Waiting: 9 34 3.8 35 42Total: 10 35 3.7 35 42Percentage of the requests served within a certain time (ms) 50% 35 66% 36 75% 36 80% 37 90% 38 95% 39 98% 41 99% 42 100% 42 (longest request)Server Software: geventServer Hostname: 127.0.0.1Server Port: 8088Document Path: /Document Length: 11995 bytesConcurrency Level: 5Time taken for tests: 0.779 secondsComplete requests: 100Failed requests: 0Write errors: 0Total transferred: 1224600 bytesHTML transferred: 1199500 bytesRequests per second: 128.39 [#/sec] (mean)Time per request: 38.944 [ms] (mean)Time per request: 7.789 [ms] (mean, across all concurrent requests)Transfer rate: 1535.43 [Kbytes/sec] receivedConnection Times (ms) min mean[+/-sd] median maxConnect: 0 0 0.0 0 0Processing: 8 38 5.3 38 51Waiting: 8 38 5.3 38 51Total: 9 38 5.3 38 51Percentage of the requests served within a certain time (ms) 50% 38 66% 39 75% 39 80% 40 90% 42 95% 48 98% 51 99% 51 100% 51 (longest request)
在带有数据库连接的访问中,我们可以发现两者相差不多,响应时间主要以IO为主,Wheatserver略微占优。
Server Software: wheatserverServer Hostname: 127.0.0.1Server Port: 10828Document Path: /result/love/Document Length: 15461 bytesConcurrency Level: 5Time taken for tests: 2.556 secondsComplete requests: 100Failed requests: 0Write errors: 0Total transferred: 1573300 bytesHTML transferred: 1546100 bytesRequests per second: 39.12 [#/sec] (mean)Time per request: 127.814 [ms] (mean)Time per request: 25.563 [ms] (mean, across all concurrent requests)Transfer rate: 601.04 [Kbytes/sec] receivedConnection Times (ms) min mean[+/-sd] median maxConnect: 0 0 0.0 0 1Processing: 24 125 16.2 124 158Waiting: 24 125 16.2 124 158Total: 25 125 16.2 124 158Percentage of the requests served within a certain time (ms) 50% 124 66% 125 75% 131 80% 135 90% 141 95% 150 98% 157 99% 158 100% 158 (longest request)Server Software: geventServer Hostname: 127.0.0.1Server Port: 8088Document Path: /result/love/Document Length: 15461 bytesConcurrency Level: 5Time taken for tests: 2.556 secondsComplete requests: 100Failed requests: 0Write errors: 0Total transferred: 1571200 bytesHTML transferred: 1546100 bytesRequests per second: 39.12 [#/sec] (mean)Time per request: 127.806 [ms] (mean)Time per request: 25.561 [ms] (mean, across all concurrent requests)Transfer rate: 600.28 [Kbytes/sec] receivedConnection Times (ms) min mean[+/-sd] median maxConnect: 0 0 0.0 0 0Processing: 25 125 15.8 125 157Waiting: 25 125 15.7 125 157Total: 26 125 15.8 125 157Percentage of the requests served within a certain time (ms) 50% 125 66% 126 75% 128 80% 129 90% 143 95% 146 98% 155 99% 157 100% 157 (longest request)
在带搜索服务的对比上,可以发现两者更为一致,基本不分伯仲。

最后看一下两者在内存上对比,gevent在同样App下保持37MB的内存,而Wheatserver同样以35MB差不多。值得注意的是Wheatserver的内存消耗量多是因为Python应用的运行和预分配内存机制的存在,在实际运行中,Wheatserver的内存量会比gevent略低。考虑到gevent依赖libevent,greenlet等库,内存量应该稍多于Wheatserver。但gevent以Python实现的服务器能达到如此低的内存也令人惊讶。

Wheatserver在作为Http服务器方面由于IO比重大,主要CPU时间在应用上,因此优势比之Gevent不大。在静态文件发送上,Wheatserver可以做到与Nginx媲美的响应速度和能力,并且是在Wheatserver并没有使用缓存的基础上。

gevent, wheatserver, 性能评测

底层性能优化–高速缓存[url=http://www.wzxue.com/底层性能优化-高速缓存/]

十一7th
2012

[/url]
Leave a Comment
Written by 麦子麦

高速缓存即使在C程序员中也很少接触,因为对高速缓存的显式操作是依赖于处理器架构的,不同架构导致很大的不同,并且硬件对高速缓存已经做了很大的优化,使得软件部分可以忽略高速缓存部分。但是这里,我们依然会介绍在x86和x86-64下对高速缓存部分做优化处理的方式。 在多核处理器众多的情况下,我们无法忽视多核并行情况的存在,但是单执行体运行是多核并行的基础。因此,对单核执行的缓存优化是多核并行的子集,我们可以分开考虑。从这里也可以看出,多核并行实质是增加了软件的复杂度。多数情况下,多核并行需要考虑更多的东西。

以下分析基于《What every programmer should know about memory》(http://lwn.net/Articles/250967/)系列,并且参考了其他众多blog。
一、忽略缓存写入
在平时的程序中,很多时候可能产生了数据但是短期内不适用。如一个写线程专门负责向内存写入数据节点,而高速缓存中一行大小是大于节点大小的,所以高速缓存中一行可以容纳多个节点。当它写入第一个节点到缓存,在写下一个节点时,缓存会刷新脏数据到内存并重新读取。这就导致了很大的性能问题。 我们可能通过写合并来实现提高性能,在X86和x86-64结构中,gcc提供了一些内置函数用于实现这一方法:

?

[table=98%]
[tr][td]

1

2

3

4

5

6

7

8

9

10

11

12

13

[/td][td]
#include <emmintrin.h>
void _mm_stream_si32(int *p, int a);
void _mm_stream_si128(int *p, __m128i a);
void _mm_stream_pd(double *p, __m128d a);

#include <xmmintrin.h>
void _mm_stream_pi(__m64 *p, __m64 a);
void _mm_stream_ps(float *p, __m128 a);

#include <ammintrin.h>
void _mm_stream_sd(double *p, __m128d a);
void _mm_stream_ss(float *p, __m128 a);
</ammintrin.h></xmmintrin.h></emmintrin.h>

[/td][/tr]
[/table]

值得注意的是,由于x86架构中内存屏障的存在,绕过缓存可能和缓存的顺序写差不多,而且通常会带来随机写的损失。
二、高速缓存访问优化L1 Data缓存访问优化
我们都知道在代码中应该尽可能提高空间和时间局部性来提高对缓存的利用,但是优化很大程度上取决于一行缓存的大小。 在L1d缓存中,如果在Linux上,我们可以通过系统内置命令得到L1 Data的行缓存大小: getconf LEVEL1_DCACHE_LINESIZE 或者通过/sys/devices/cpu/…来查看每个逻辑cpu的缓存大小。 如果是mac osx: sysctlbyname(“hw.cachelinesize”, &line_size, &sizeof_line_size, 0, 0); 通常情况下,不同层次的缓存的行大小是相同的。 比如大矩阵的计算,我们可以通过将大矩阵分为缓存行大小字节的小矩阵来进行局部计算,通常情况下可以提高4倍的速度。 另一种情形是结构数组:

?

[table=98%]
[tr][td]

1

2

3

4

5

[/td][td]
struct foo {
int a;
long fill[7];
int b;
};

[/td][/tr]
[/table]

在编译器内存对齐优化后,这个foo结构用了68个字节存储,而本身这个结构只有64字节,这时候访问一个结构我们就需要两行缓存。 我们可以通过gcc内置命令来指定对齐: struct strtype { …members… } __attribute((aligned(64))); 但是如果有大量的结构数组时,内存对其的结构访问速度是不对齐结构访问速度的3倍。因此,即使架构是支持不对齐访问,不对齐的结构也需要谨慎考虑。
L1 Instrument缓存访问
好的代码局部性主要有两方面: 1. 条件跳转被很好预测到 2. 减少代码量来取得循环展开和内联的平衡 3. 代码内存对齐 在条件跳转上,实际上我们可以通过对编译器做两种方案来提高分支预测的准确性。第一是先编译后运行会得到一份性能报告,编译器重新利用这份报告生成代码会极大提高分支预测准确度。第二是显式的指定分支判断。 gcc中支持__builtin_expect来指定分支。

?

[table=98%]
[tr][td]

1

2

3

[/td][td]
long __builtin_expect(long EXP, long C);
#define unlikely(expr) __builtin_expect(!!(expr), 0)
#define likely(expr) __builtin_expect(!!(expr), 1)

[/td][/tr]
[/table]

我们通过likely宏在条件跳转上显式的指定分支。 在代码量上,intel core2有一个特征:当一个循环少于18个指令,只需要4个解码指令,最多有4个分支指令,这个小循环会加快16倍的执行速度。 在代码内存对齐上跟数据对齐类似的因素,但是线性的读取指令会取到更好的效果。
L2和更高层缓存访问
这跟L1相比,很大的改变是缓存未击中的惩罚大大提高,而且通常L2以上缓存会被多个核共享,实际每个执行体的缓存大小会大大少于总缓存大小。
TLB访问
提高TLB的缓存命中率主要靠减少页数量和减少TLB查询深度,但是这些因素很大程度上无法控制。 能被程序员影响的只有通过mmap的MAP_FIXED选项来直接访问,但是这个动作也是危险的,很有可能是page fault。必须对所选的请求地址清楚。
预取
分为硬件预取和软件预取,硬件预取依赖于处理器架构,但好处是不需要对代码做改动。软件预取通过显式调用函数来预取。在x86和x86-64架构上可以采取以下函数:

?

[table=98%]
[tr][td]

1

2

3

4

5

6

7

8

9

[/td][td]
#include <xmmintrin.h>
enum _mm_hint
{
_MM_HINT_T0 = 3,
_MM_HINT_T1 = 2,
_MM_HINT_T2 = 1,
_MM_HINT_NTA = 0
};
void _mm_prefetch(void *p, enum _mm_hint h);</xmmintrin.h>

[/td][/tr]
[/table]

三、多线程优化并发问题
在多处理器架构下,多线程并行写入同一内存位置时,由于缓存一致性问题,会导致很大的性能问题,这种现象也称为False Sharing。多处理器缓存一致性通过MESI协议(en.wikipedia.org/wiki/MESI_protocol)实现,缓冲访问一致通过复杂的状态来实现。而x86和x86-64架构实现的是缩略版的MESI协议。在多核情况下,类似情况会有所好转,但是也会有一定的性能损耗。 简易的解决方案是通过将共享变量写入不同的缓存行来实现提高。但是如果变量较多会导致较大的空间使得缓存命中降低。 我们可以将共享全局变量放入不同的段中来减小数据占用。

?

[table=98%]
[tr][td]

1

2

3

4

[/td][td]
int foo = 1;
int bar attribute((section(“.data.ro”))) = 2;
int baz = 3;
int xyzzy attribute((section(“.data.ro”))) = 4;

[/td][/tr]
[/table]

而如果一个变量只被一个线程使用,我们可以使用线程变量:

?

[table=98%]
[tr][td]

1

2

3

4

[/td][td]
int foo = 1;
__thread int bar = 2;
int baz = 3;
__thread int xyzzy = 4;

[/td][/tr]
[/table]

线程变量使得不同线程有自己的同名变量,但是如果该变量只被一个线程使用,会造成一定的浪费,因为线程变量是在创造线程时产生,过多的线程变量会使得产生线程变慢并且带来数据空间浪费。
原子操作
目前x86和x86-64架构主要实现了CAS(compare and swap)和Bit Test原语。(http://en.wikipedia.org/wiki/Atomic_operation)
总线带宽考虑
多线程如果在多核和多处理器下并行,考虑以下情况:两个线程分别在两个处理器的一个核上运行,运行一段时间后被内核抢占然后调度运行。当两个线程重新恢复运行时,如果又被分配到不同的核上,导致原来的缓存失效使得数据迁移忙。 我们可以通过设置cpu和核的亲和度(affinity)来绑定线程到某个核上。

?

[table=98%]
[tr][td]

1

2

3

4

5

6

7

8

9

10

[/td][td]
#include <pthread.h>

int pthread_setaffinity_np(pthread_t th, size_t size,
const cpu_set_t *cpuset);
int pthread_getaffinity_np(pthread_t th, size_t size, cpu_set_t *cpuset);
int pthread_attr_setaffinity_np(pthread_attr_t *at,
size_t size, const cpu_set_t *cpuset);
int pthread_attr_getaffinity_np(pthread_attr_t *at, size_t size,
cpu_set_t *cpuset);
</pthread.h>

[/td][/tr]
[/table]

c/c++, 高速缓存

程序设计性能多层优化[url=http://www.wzxue.com/程序设计性能多层优化/]

九24th
2012

[/url]
Leave a Comment
Written by 麦子麦
高层设计:
选择合适的数据结构和算法,并且在要避免实现算法和编码时引入可能影响性能的问题。 如:在实现快速排序时,避免错误的编码使得时间复杂度变成O(n2)。正确的实现自己所需的数据结构和算法非常重要。
基本编程原则:[list=1]
[]编码时避免晦涩的优化:因为现代编译器会很好的产生高效的代码。如,临时变量的使用,有时候,程序员试图减少临时变量的使用来避免不必要的多余变量,但在现代编译器中,如GCC中,一些整数变量会直接用常量代替,多余的变量编译器也会自动省去,或者等到用到时再提取。不必要的优化,省略使得代码晦涩,难读。同时也没有增加程序性能。
[
]消除过多的函数调用: 考虑以下函数
[/list]
?

[table=98%]
[tr][td]

1

2

3

[/td][td]
for (int i=0; i < strlen(s); ++i) {
d[i] = s[i];
}

[/i][/i]
[/td][/tr]
[/table]

在这个循环中,s没有发生变化,因此strlen(s)是一个常数,完全可以提前到循环前,每次循环都要判断大大影响了程序的性能。有些同学可能认为编译器会识别到此类情况,自动将strlen(s)替换为常量。 但是,编译器优化的核心原则就是:避免一切可能改变程序运行的优化。在循环中,可能会有改变s的代码,因此编译器不会试图替代strlen(s),这是一个危险的行为。 还有使用内联函数来替代,不断的进入函数和退出函数会多出很多指令来。 3. 消除不必要的内存引用: 使用局部变量来替代引用内存中数据。考虑以下函数:

?

[table=98%]
[tr][td]

1

2

3

4

5

6

[/td][td]
void sum(int array, int length, int *sum) {
int i;
for (i=0; i < length; ++i) {
*sum += array[i];
}
}

[/i]
[/td][/tr]
[/table]

如果查看这个函数的汇编代码,会发现,每次循环都会先将sum赋值给寄存器变量,然后再对寄存器变量做和运算,再把寄存器变量赋给sum,如此的话,每次循环都会有不必要的两次赋值运算。直接使用

?

[table=98%]
[tr][td]

1

2

3

4

5

6

7

[/td][td]
void sum(int array, int length, int *sum) {
int i, s;
for (i=0; i < length; ++i) {
s += array[i];
}
*sum = s;
}

[/i]
[/td][/tr]
[/table]

可以避免循环中不必要的引用内存,大大加快程序运行时间。
底层优化:[list=1]
[*]解开循环来减少循环上限并且能提高性能: 在求和函数中,
[/list]
?

[table=98%]
[tr][td]

1

2

3

4

5

6

7

[/td][td]
for (int i=0; i < length; ++i) {
sum += array[i];
}
for (int i=0; i < length; i+=2) {
sum += array;[i]
sum += array[i+1];
}

[/i][/i]
[/td][/tr]
[/table]

以上两个循环对比,下面这个循环可以提高运行性能,主要是因为在现代处理器中,指令可以达到并行化和流水线化,一个汇编指令在处理器中都被分解为多条处理器指令,如load, store等,每个处理器中都具有多个运算单元,但是由于循环中需要不断比较i<length,处理器无法明确下一条指令,在运算单元求和时,其他处理器单元就会停止工作。在解开循环后,在多个时钟周期中,可以让其他部件也执行指令,也就是说,在运行一条指令的时间里,其实现代处理器完全可以几乎同时运行其他处理器指令(下面解释),如提取内存中的array[i+1]。解开循环可以大大加快运行时间。另外,解开更多的循环可以不断的提高性能。 2. 为了增加指令级别的并行处理能力,可以使用多个运算同时进行,并且采用不同的运算顺序来提高性能:

?

[table=98%]
[tr][td]

1

2

3

4

5

6

7

8

9

10

[/td][td]
for (int i=0; i < length; i+=2) {
sum += array[i];
sum += array[i+1];
}

for (int i=0; i < length; i+=2) {
sum1 += array;[i]
sum2 += array[i+1];
}
sum = sum1 + sum2;

[/i][/i]
[/td][/tr]
[/table]

在这两个代码段中,区别在于在同一个循环中,后者使用了两个变量。在指令级别上,在同时执行循环里指令时,由于存在数据依赖,必须在第一次求和结束后,拿到这个sum值,才能进行第二次求和。因此,两个变量可以使得多个运算单元同时执行求值。 采用不同的求值顺序在长表达式中也非常重要,这个话题需要深入到处理器指令的关键路径上。不了解的话可以采用实验的方式求解同一条表达式: a = (b * c ) * (d * e) * f a = ((b * c) * d) * e * f
[list=1]
[*]在处理条件分支时,采用函数式风格来帮助编译器使用条件数据移动。 在处理器碰到分支结构时,在判断完成前,需要执行下一条指令,这时候,处理器会随机执行下一个指令,有可能会判断出错。如果判断出错,处理器需要回到之前的状态,然后执行正确的指令。这里涉及到处理器中的Retirement Unit,它会保存执行分支指令的状态,并且使用(register name, value)的方式传递假如判断正确的事先已经完成的指令所产生的寄存器的状态变化结果。如果判断出错,只要清空之前保存的()。 因此,在碰到执行错误分支时,程序性能会下降,程序员需要帮助编译器来使用分支数据选择而不是分支指令选择。 考虑以下函数,排序两个数组,使得对于每个i, b >= a:

[/list]
这两个函数区别在于,第一个采用分支控制选择,第二个采用分支数据选择。分支控制选择在判断出错后会影响运行性能,而分支数据选择可以让编译器采取cmov式的汇编指令来避免判断出错。
[i][i]
?

[table=98%]
[tr][td]

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

[/td][td]
void minmax1(int a, intb, int n) {
int i;
for (i = 0; i < n; i++) {
if (a[i] > b[i]) {
int t = a;[i]
a[i] = b[i];
b = t;[i]
}
}
}
void minmax2(int a, intb, int n) {
int i;
for (i = 0; i < n; i++) {
int min = a[i] < b[i] ? a[i] : b[i];
int max = a < b ? b : a;[i][i][i]
a[i] = min;
b = max;[i]
}
}

[/i][/i][/i][/i][/i][/i][/i][/i][/i][/i][/i][/i][/i][/i][/i]
[/td][/tr]
[/table]

在整个优化中,你会发现函数式风格的重要性,因为在现代处理器的设计中,采用了out of order的指令执行,只要保证最终行为的一致,处理器执行指令时会采用同时流水线执行多条指令,来加快运行速度。这时候,数据状态的变化会严重影响处理器的并行化,在命令式编程语言中同样需要函数式的编程。 在处理器设计上,会发现无状态是最自然的方式,改变数据状态反而显得有点背离。函数式语言不改变数据状态这个特点会越来越突出,因为在处理器级别上,无状态数据是最适合的方式,使得处理器节省不必要的执行,在上面的分支结构选择和数据依赖上已经很好的体现了。

最后,最重要的优化原则是:在瓶颈上优化!

[/i][/i]