Pythonのプロファイル結果をGUIで解析する
久しぶりのエキスパートPythonプログラミング補足記事です。今回はプロファイリングの補足です。
Pythonは標準ライブラリにプロファイラ(profile/cProfileモジュールのProfileクラス)を持っていて、 その結果を格納しているpstats.Statsクラスにはプロファイル結果を解析するのに必要な機能(実行時間でのソートや、呼び出し元/呼び出し先の表示など)が一通りそろっています。これらのクラスについてはエキスパートPythonプログラミングで使い方を解説してあります。
コマンドラインで作業するときにはIPythonからStatsオブジェクトを直接操作してプロファイル結果を調べられるのですが、やはりプロファイル結果はグラフィカルに表示してくれた方が見やすいものです。特に「エキスパートPythonプログラミング」の中でも言及されているKCacheGrindというGUIツールは非常に強力で、一度使うと手放せません。
KCacheGrindが対応しているcallgrind形式は、元々はValgrindのツールの一つであるCallgrindのものでしたが、 最近はプロファイル結果を保存するフォーマットとして広く用いられています。callgrind形式ファイルのブラウザとしてはKCacheGrind以外にも、Windowsで動くWinCacheGrind、PHP製でブラウザ上で表示するwebgrind 等がありますし、PHPのxdebugに含まれるプロファイラもcallgrind形式でプロファイル結果を保存します。
弊社でも日常的にcallgrind形式のプロファイル結果を共有していて、そのために社内のWebツールまで用意しています。(紹介記事: PHP Xdebug のProfileの手軽な共有ツールを作ったよ)
しかし、Statsクラスはcallgrind形式には対応しておらず、プロファイル結果を保存するときのファイルフォーマットは独自フォーマット(Statsの内部変数をmarshal形式で保存したもの)になっています。このままでは、Pythonのプロファイル結果をKCacheGrindで表示することができません。 そこで、Statsオブジェクトをcallgrind形式で保存するためにlsprofcalltree.pyを利用します。
lsprofcalltree.pyは、コマンドラインツールとして $ lsprofcalltree.py スクリプトファイル という使い方もできますし、アプリケーションに組み込んで使うこともできます。例えばBazaarでは $ bzr --lsprof-file callgrind.out statusのように実行するとプロファイル結果をcallgrind形式で出力するようになっています。
プロファイル結果を見やすくビジュアライズできる環境が整うと、アプリケーションのチューニングが楽しくなります。 KCacheGrind等のGUIツールとcallgrind形式のファイルを出力するプロファイラは、Pythonプログラマに限らずみんな常備しておくといいと思います。
Windows実行形式のMachineタイプを判別する方法
64 ビット環境の場合は所定のバイナリを実行してタスクマネージャで確認すれば、32 ビットプロセスならプロセスイメージ名の末尾に「*32」の符丁が表示されるためそれで見分けることもできますが、わざわざそのためにプロセスを起動するというのも何だか妙な話だし、また、バイナリが DLL の場合にはその方法は使えません。好ましいのは「バイナリの内容をチェックして判別する」というもっとも単純なやり方だと思いました。また、そういうツールがあれば 32 ビット環境でも 64 ビット環境でも所定のバイナリを手軽に識別できるはずです。
Windows に標準でそういう機能がないかとざっと見渡したところではどうも見当たらず、ネット上にもそれらしいツールが見つからなかったため簡単なコードを書いてみることにしました。 だいぶ前にこのブログに Windows 実行形式のヘッダ構成を書いたことがあり、その中のどこかにそういう情報があった記憶がありました。あらためて探してみるとこれでした。
MSDN: IMAGE_FILE_HEADER Structure
|Members | Machine | IMAGE_FILE_MACHINE_I386 (0x014c) x86 | IMAGE_FILE_MACHINE_IA64 (0x0200) Intel IPF | IMAGE_FILE_MACHINE_AMD64 (0x8664) x64そのまんまですね^^;
これらの情報をもとに C 言語で短い Windows プログラムを書きました。 引数で渡されたファイルを読んで実行形式であれば Machine タイプを表示する内容です。 ビルドずみのバイナリ (x86 & x64)
筆者は SendTo フォルダにショートカットをコピーし右クリックメニューから呼び出して使っています。
//
// 引数で渡された Windows 実行形式ファイルの Machine タイプを判別する
//
#if !defined(UNICODE)
#define UNICODE
#define _UNICODE
#endif
#include <windows.h>
#include <stdio.h>
#include <sys/stat.h>
#pragma comment(lib, "user32.lib")
// IMAGE_FILE_HEADER から Machine タイプを取得
int MyCheckMachineType(LPCWSTR pszFileName)
{
FILE *fp;
struct _stat st;
IMAGE_DOS_HEADER idh;
IMAGE_NT_HEADERS inh;
if (_wstat(pszFileName, &st) < 0 ||
_wfopen_s(&fp, pszFileName, L"rb") != 0) {
return -1;
}
if (fread(&idh, sizeof(IMAGE_DOS_HEADER), 1, fp) < 1 ||
idh.e_magic != IMAGE_DOS_SIGNATURE || // "MZ"
idh.e_lfanew <= 0 || idh.e_lfanew >= st.st_size) {
fclose(fp);
return -2;
}
if (fseek(fp, idh.e_lfanew, SEEK_SET) != 0 ||
fread(&inh, sizeof(IMAGE_NT_HEADERS), 1, fp) < 1 ||
inh.Signature != IMAGE_NT_SIGNATURE) { // "PE\0\0"
fclose(fp);
return -3;
}
fclose(fp);
switch (inh.FileHeader.Machine) {
case IMAGE_FILE_MACHINE_I386:
return 0;
case IMAGE_FILE_MACHINE_AMD64:
return 1;
case IMAGE_FILE_MACHINE_IA64:
return 2;
}
return 3; // Unknown
}
// 現プロセスが WOW64 下で実行中のプロセスか
BOOL MyIsWow64Process()
{
typedef BOOL (WINAPI *DEC_ISWOW64PROCESS)(HANDLE, BOOL*);
BOOL bWow64Process = FALSE;
DEC_ISWOW64PROCESS pIsWow64Process = (DEC_ISWOW64PROCESS)
GetProcAddress(GetModuleHandle(L"Kernel32.dll"),"IsWow64Process");
if (!pIsWow64Process ||
!pIsWow64Process(GetCurrentProcess(), &bWow64Process)) {
return FALSE;
}
return bWow64Process;
}
int APIENTRY wWinMain(HINSTANCE hInst, HINSTANCE hPrevInst,
LPWSTR lpCmdLine, int nCmdShow)
{
int sts;
WCHAR *pName;
WCHAR *m[] = { L"x86", L"x64", L"IA64", L"Unknown"};
WCHAR *e[] = { L"broken?", L"not executable", L"open error"};
#ifdef WIN32
if (MyIsWow64Process()) {
MessageBox(NULL,
L"WOW64 によるリダイレクトの影響を避けるために\n" \
L"Windows x64 環境では x64 版を使用して下さい",
L"Warning", MB_OK);
}
#endif
if (__argc < 2) {
return 0;
}
sts = MyCheckMachineType(__wargv[1]);
pName = wcsrchr(__wargv[1], L'\\') + 1;
if (sts >= 0 && sts <= 3) {
MessageBox(NULL, m[sts], pName, MB_ICONINFORMATION|MB_TOPMOST);
} else {
MessageBox(NULL, e[sts+3], pName, MB_TOPMOST);
}
return 0;
}
たとえばこのプログラムでこのプログラム自身の x86, x64 バイナリをチェックすると図のように表示されます。
(tanabe)
Twisted vs Tornado vs Go で非同期Webサーバー対決
昨日の takada-at の記事で「サーバー側では単純に100ms待ってからレスポンスを返すだけのページを用意しておき、」とあったのですが、今日はそのサーバー側の話をします。
もともとこのサーバーを作った動機は、takada-at が作成中の負荷試験システムがちゃんと並列に負荷をかけられるかどうかを検証するためでした。 すぐにレスポンスを返してしまうと、負荷試験スクリプトがきちんと並列に負荷をかけられなくても PV/sec が出てしまいます。 そこで、 epoll を使って高速に並列接続を扱えるTwistedフレームワークを使って、100msの遅延をしつつ数千PV/secに耐えるWebサーバーを作ってみました。 さらに、同じく epoll を使っている Tornado や Go にも興味があったので、こちらでも同じものを作成し、パフォーマンスを比較してみました。
コード
まずは、コードを見てみましょう。
Twisted Python:
from twisted.internet import epollreactor
epollreactor.install()
from twisted.web import server, resource
from twisted.internet import reactor
class Lazy(resource.Resource):
isLeaf = True
def __init__(self, delay=0.1):
self._delay = delay
def render_GET(self, request):
def after():
request.write('Hello, World!')
request.finish()
reactor.callLater(self._delay, after)
return server.NOT_DONE_YET
if __name__ == '__main__':
import sys
delay = 0.1
try:
delay = float(sys.argv[1]) / 1000
except Exception:
pass
site = server.Site(Lazy(delay))
reactor.listenTCP(8000, site)
reactor.run()
次は、Tornadoです。
from tornado import httpserver, ioloop, web
from time import time
DELAY = 0.1
class MainHandler(web.RequestHandler):
@web.asynchronous
def get(self):
def _hello():
self.write("Hello, World!")
self.finish()
ioloop.IOLoop.instance().add_timeout(time() + DELAY, _hello)
application = web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
import sys
try:
DELAY = float(sys.argv[1]) / 1000
except Exception:
pass
http_server = httpserver.HTTPServer(application)
http_server.listen(8001)
ioloop.IOLoop.instance().start()
最後に、Goです。
package main
import (
"flag"
"http"
"io"
"log"
"time"
)
var addr = flag.String("addr", ":9000", "http service address")
var delay = flag.Int64("delay", 100, "response delay[ms]")
func main() {
flag.Int64Var(delay, "d", 100, "response delay[ms]")
flag.Parse()
http.Handle("/", http.HandlerFunc(hello))
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Exit("ListenAndServe:", err)
}
}
func hello(c *http.Conn, req *http.Request) {
time.Sleep(int64(1000000) * int64(*delay))
io.WriteString(c, "Hello, World!\n")
}
TwistedやTornadoは、ループに後で呼び出す関数を登録していていて明らかにイベントドリブンフレームワークという感じがします。 それに対してGoは Sleep() を使っていて、一見普通のスレッドべースの並列処理に見えます。 しかし、Goはネイティブスレッドではなくgoroutineという軽量なユーザーランドスレッドを利用しており、そのスケジューラは(Linuxの場合)epollを利用しています。通常のスレッドを使うよりも効率的に動作するはずです。
ベンチマーク
それでは、この3つにそれぞれ負荷をかけてみましょう。といっても、100msの遅延だと負荷をかけるのが大変なので、10msの遅延で実行し、ab -c100 -n10000で実験してみます。
Twisted: Requests per second: 2620.31 [#/sec] (mean) Time per request: 38.163 [ms] (mean) Time per request: 0.382 [ms] (mean, across all concurrent requests) Percentage of the requests served within a certain time (ms) 50% 31 66% 32 75% 33 80% 37 90% 38 95% 38 98% 40 99% 44 100% 3082 (longest request) Tornado: Requests per second: 5299.04 [#/sec] (mean) Time per request: 18.871 [ms] (mean) Time per request: 0.189 [ms] (mean, across all concurrent requests) Percentage of the requests served within a certain time (ms) 50% 19 66% 19 75% 19 80% 19 90% 19 95% 19 98% 21 99% 22 100% 33 (longest request) Go: Requests per second: 4712.63 [#/sec] (mean) Time per request: 21.220 [ms] (mean) Time per request: 0.212 [ms] (mean, across all concurrent requests) Percentage of the requests served within a certain time (ms) 50% 19 66% 20 75% 20 80% 21 90% 32 95% 40 98% 42 99% 42 100% 48 (longest request)
TornadoやGoに比べると、Twistedが残念な結果になっています。 これは、Twistedが比較的汎用的で高機能なフレームワークということもありますが、遅延実行する関数をリストに格納していて、実行する関数があるかどうかをリストを全部チェックしているのが大きそうです。(Tornadoはソート済みリストを利用していてます)
Goも、コンパイラ型なのにTornadoに負けてるのが残念ですが、epollから直接イベントドリブンしているTornadoに比べると goroutine という抽象化レイヤを1枚はさんでいるので仕方ないかもしれません。
さらに高速化
まずはTornadoのJITによる高速化に挑戦してみます。PyPy-1.2が一番よさそうですが、セットアップが面倒なのでお手軽なpsycoを使ってみました。もとのソースコードに、次の2行を追加します。
import psyco psyco.full()
結果はこうなりました。
Requests per second: 7503.16 [#/sec] (mean) Time per request: 13.328 [ms] (mean) Time per request: 0.133 [ms] (mean, across all concurrent requests) Percentage of the requests served within a certain time (ms) 50% 13 66% 13 75% 14 80% 14 90% 14 95% 14 98% 15 99% 15 100% 23 (longest request)
5299req/sec => 7503req/sec で、20%以上高速化できました。topで見ている限り、メモリ使用量(RES)も1MB以下です。Tornado+psycoは非常に優秀ですね。
次に、Goでマルチスレッド化を試してみます。goroutineは複数のネイティブスレッドを利用して並列に動作することができます。しかし、現時点ではまだスケジューラがよくないらしく、デフォルトではマルチスレッドを使わないようになっています。環境変数 GOMAXPROCS に並列数を指定して実験してみました。
GOMAXPROCS=6 の場合: Requests per second: 5760.33 [#/sec] (mean) Time per request: 17.360 [ms] (mean) Time per request: 0.174 [ms] (mean, across all concurrent requests) Percentage of the requests served within a certain time (ms) 50% 16 66% 17 75% 17 80% 18 90% 28 95% 33 98% 37 99% 40 100% 53 (longest request)
実験マシンはCore2 Duoで2コアしか載っていない(Hyper Threadingもなし)なのですが、2を指定してもあまり性能が上がらず、そこから1つずつ数字を上げていったところ6までは少しずつ性能が向上していきました。しかも、topを見ている限りCPU使用率は最大の200%には全く届かず120%以下しか利用できていません。 CPUを原因は判りませんが、今回のようなイベントループをヘビーに使う用途にはスケジューラが最適化されておらず、もっとサーバー側でCPUを利用する計算を行う処理をしないとマルチコアを使いきれないのかもしれません。
Goは今回のケースではTornadoに負けてしまいましたが、IO多重化をイベントドリブンよりシンプルに記述でき、コンパイル型言語なので複雑な計算をPythonより圧倒的に高速に計算できそうなので、非常に期待しています。
並列1000コネクションに耐える! Ruby のイベント駆動ライブラリ Rev と EventMachine の HTTPクライアント
Rubyのイベント駆動型ネットワークプログラミングフレームワーク Rev と EventMachine で HTTPクライアントを動かしてみました。
イベント駆動型ネットワークプログラミングフレームワークとは何か説明しだすと難しいですが、一言で言うと、以下のようになります。
# ふつうのフロー駆動型プログラム
Net::HTTP.start(host, port){|http|
res = http.get(path) #この処理が終わってから
}
puts "done" #この次の処理が実行される
# イベント駆動型プログラム client = Rev::HttpClient::connect(host, port) client.get(path) #この処理が終わってないのに puts "not done" #すぐこの行が実行されてしまう一見するとプログラムの複雑さが増えるだけのように思えますが、処理をブロックしないということは無駄がないということでもあります。これによって、
- 複数のファイルディスクリプタを同時に開き
- 読み込み可能なものから処理していく
イベント駆動ライブラリのサンプルでは、サーバーを書くことが多いですが、クライアントの同時接続数の限界に挑戦したかったので、今回は HTTPクライアントを書いてみます。
Ruby のイベント駆動ライブラリとして Ruby Revと EventMachine の2つを使ってレスポンスタイムを計測しました。どちらも独自の HTTPクライアントをライブラリに組み込んでいます。
EventMachine
http://rubyeventmachine.com/
Rev
http://rubyforge.org/projects/rev/
Rev も EventMachine も Python の Twisted や Tornado、Perl の AnyEvent のように、ノンブロッキング I/O を利用することで並行性が高く高速なネットワークプログラムを組むためのフレームワークです。
どちらのライブラリもまだあまり資料がありませんが、内部の実装を見つつサンプルコードを作成してみました。
結論から言うと、Revはとても高速です。
サーバー側では単純に100ms待ってからレスポンスを返すだけのページを用意しておき、クライアントは並列に接続してレスポンスタイムの平均を取りました。要するに、並列数を増やしてもレスポンスタイムが100ms に近ければ近いほど、高速なクライアントということになります。
以下
- Rubyのスレッド + net/http ライブラリ
- Rev/HttpClient
- EventMachine/HttpClient2
なお、試験に利用したサーバーは、Python の Tornado で書かれており、十分に高速です。
(EventMachine にはなぜか HTTPクライアントが2つありますが、HttpClient という名前の方はまともに動作しなかったので試験対象から外しました)。
10コネクション
target: http://hornet.klab.org:8000/
concurrency: 10
net/http + thread
avg: 140.5132
EventMachine/HttpClient2
avg: 101.3227
Rev/HttpClient
avg: 101.4915
スレッドの方はすでに多少の遅延が派生してます。
100コネクション
target: http://hornet.klab.org:8000/
concurrency: 100
net/http + thread
avg: 166.59175
EventMachine/HttpClient2
avg: 110.23086
Rev/HttpClient
avg: 103.69137
EventMachine, Rev はほぼ問題の無い処理時間です。
500コネクション
target: http://hornet.klab.org:8000/
concurrency: 500
net/http + thread
avg: 2720.349032
EventMachine/HttpClient2
avg: 132.847788
Rev/HttpClient
avg: 113.132662
EventMachine に多少の遅延が発生するようになりました。スレッドはほとんど使いものにならない遅さです。
1000コネクション
target: http://hornet.klab.org:8000/
concurrency: 1000
net/http + thread
avg: 10968.612545
EventMachine/HttpClient2
avg: 136.243228
Rev/HttpClient
avg: 122.174841
EventMachine もかなり高速ですが、1000を少し越えたあたりからまともに動作しなくなります。
一方 Rev は1000コネクション程度でも問題なく動作するようです。
なお注意事項として、この試験をする前に、ファイルディスクリプタの制限を増やしておく必要があります。
デフォルトでは1024以上のファイルディスクリプタを扱うことができず、ソケットの数にも制限がくわえられてしまいます。
Linux では、/etc/security/limits.conf を編集することでユーザーのファイルディスクリプタの上限を増やすことができます。
さらに、TIME_WAIT 状態のコネクションを使い回せるようにtcp_tw_reuse=1 を設定しておくとよいでしょう。
/etc/sysctl.conf に net.ipv4.tcp_tw_reuse = 1 を設定することで、TIME_OUT 状態のコネクションを再利用し、TCPコネクションの上限を引き上げることができます。
以下に今回使用したテストコードを掲載します。
イベント駆動を駆使してるため多少読みにくいですが、どのパターンも 1秒待ってからHTTPリクエストを並列に送信し、レスポンスの時間を計測しています。
(takada-at) 続きを読む
Windows用フリーウェア「HookDate」をバージョンアップしました
新しいバージョン 1.0.2.0 での改訂内容は以下の通りです。
- 64ビット Windows 環境 (x64) への対応
- exe ファイルへのショートカットファイルを HookDate.exe にドラッグ&ドロップできるように
- HookDateRegister.exe を追加しインストール手順を簡素化
以下のテスト環境において動作を確認しています。
- Windows 2000 Professional SP4 (x86)
- Windows XP Home SP3 (x86)
- Windows XP Professional SP3 (x86)
- Windows Vista Business SP2 (x86)
- Windows 7 Professional (x86)
- Windows XP Professional SP2 (x64) *仮想環境
- Windows Vista Business SP2 (x64)
- Windows 7 Professional (x64)
本バージョンからインストール・アップデート方法を変更しています。
HookDate のページの記事をご参照の上、同ページからダウンロードしてご利用下さい。
(tanabe)
Pythonの内包表記はなぜ速い?
「エキスパートPythonプログラミング」の発売が、Amazonや一部の書店で始まりました。
エキスパートPythonプログラミング著者:Tarek Ziade
販売元:アスキー・メディアワークス
発売日:2010-05-28
クチコミを見る
今回は、「エキスパートPythonプログラミング」の2章から、リスト内包表記について補足します。 本書で、リスト内方表記が速い理由について、次のような訳注を書きました。
訳注:リストに要素を append() する場合、インタプリタは「リストから append 属性を取り出してそれを関数として呼び出す」という処理をしなければなりません。 それに対して、リスト内包表記を使うと、インタプリタに直接「リストに要素を追加する」という処理をさせることができます。インタプリタが解釈する命令数が減る、属性の取り出しが不要になる、関数呼び出しが不要になる、という3つの理由で、リスト内包表記を使うと速くなります。
今回はこの訳注の内容について、詳しく見ていきましょう。いきなりですが、内包表記を使う関数と使わない関数を用意して、実行速度とバイトコードを比較してみます。
In [1]: def sample_loop(n):
...: L = []
...: for i in xrange(n):
...: L.append(i)
...: return L
...:
In [2]: def sample_comprehension(n):
...: return [i for i in xrange(n)]
...:
In [3]: %timeit sample_loop(10000)
1000 loops, best of 3: 1.03 ms per loop
In [4]: %timeit sample_comprehension(10000)
1000 loops, best of 3: 497 us per loop
In [5]: from dis import dis
In [6]: dis(sample_loop)
2 0 BUILD_LIST 0
3 STORE_FAST 1 (L)
3 6 SETUP_LOOP 33 (to 42)
9 LOAD_GLOBAL 0 (xrange)
12 LOAD_FAST 0 (n)
15 CALL_FUNCTION 1
18 GET_ITER
>> 19 FOR_ITER 19 (to 41)
22 STORE_FAST 2 (i)
4 25 LOAD_FAST 1 (L)
28 LOAD_ATTR 1 (append)
31 LOAD_FAST 2 (i)
34 CALL_FUNCTION 1
37 POP_TOP
38 JUMP_ABSOLUTE 19
>> 41 POP_BLOCK
5 >> 42 LOAD_FAST 1 (L)
45 RETURN_VALUE
In [7]: dis(sample_comprehension)
2 0 BUILD_LIST 0
3 DUP_TOP
4 STORE_FAST 1 (_[1])
7 LOAD_GLOBAL 0 (xrange)
10 LOAD_FAST 0 (n)
13 CALL_FUNCTION 1
16 GET_ITER
>> 17 FOR_ITER 13 (to 33)
20 STORE_FAST 2 (i)
23 LOAD_FAST 1 (_[1])
26 LOAD_FAST 2 (i)
29 LIST_APPEND
30 JUMP_ABSOLUTE 17
>> 33 DELETE_FAST 1 (_[1])
36 RETURN_VALUE
実行速度は、リスト内方表記の方が倍以上速いですね。 バイトコードについては詳しくは解説しませんが、 sample_loopは "19 FOR_ITER" から "38 JUMP_ABSOLUTE" までの8命令、sample_comprehensionは "17 FOR_ITER" から "30 JUMP_ABSOLUTE" までの6命令がループになっています。 インタプリタはこれらの命令を読み込んで実行というループを行っているので、まずは命令数の削減が高速化の理由の1つになります。 さらに、ループの中の処理について見ていきましょう。
1. append属性の取り出し
sample_loop のバイトコードで、 "23 LOAD_ATTR" という部分が、 "L" という変数からロードしたオブジェクトから "append" という属性を取り出しています。 属性の参照がどれくらい重い処理なのか、 属性の参照はそれなりに「重い」処理です。試しに、 append 属性の参照をループの外に追い出してみましょう。
In [19]: def sample_loop2(n): ....: L = [] ....: append = L.append ....: for i in xrange(n): ....: append(i) ....: return L ....: In [20]: %timeit sample_loop2(10000) 1000 loops, best of 3: 549 us per loop
リスト内方表記を利用した場合に比べて1割程度しか遅くありません。実は、元のコードのオーバーヘッドの大半は、append属性の参照にあったという事になります。
ちなみに、バイトコードは載せていませんが、LOAD_ATTRが無くなった分命令数も8命令から7命令に削減されています。
2. 関数の呼び出し
リスト内包表記が速い残り1割の理由は、 "29 LIST_APPEND" という命令にあります。
内包表記を使わない場合は、 "13 CALL_FUNCTION" によって、 append を「Pythonの関数として実行」する必要があります。この「Pythonの関数として実行」というのは、 スタックから引数リストを作成する必要があったり、呼び出される関数の方で引数のチェックをしていたり、関数の戻り値の処理("37 POP_BACK" など)が必要になります。
それに対して、 "LIST_APPEND" という命令はリストオブジェクトに要素を追加する専用の命令なので、内部ではリストへの要素の追加のみが行われるようになります。
まとめ
リスト内方表記を使うと速くなる仕組みを、バイトコードレベルで解説しました。もっと下まで知りたい方は、Pythonのソースコードの ceval.c というファイルから、 CALL_FUNCTION と LIST_APPEND の処理をしている部分を追ってみてください。
また、速くなる理由の大半は、属性の参照コストにありました。このコストは、内包表記を使わなくても、メソッドをループの手前でローカル変数に代入するというテクニックでも回避できます。このテクニックはリストのappendメソッド以外にも任意の属性に利用できるので、ボトルネックになるかもしれないループを書くときには積極的に利用しましょう。
Apache MPMをめぐる冒険 〜eventとpreforkを比べてみるよ〜
Apache 2.3からMPMの切替が実行時(起動時)に設定ファイルから動的に選択できるようになる点について、以前当DSASブログ内の記事で紹介しました。このMPMの切替によってどのようなメリットを得られるのでしょうか。実際にこれを動かしてみたときのCPU使用率とネットワークI/Oの変化を見ながら、それぞれのMPMモジュールの性能・特性を比較してみたいと思います。
まずは実験です。以下のような環境を用意しました。クライアント側については、ab(Apache Bench)によって単一のURLをひたすらダウンロードする単純なものです。しかも静的ファイルなので(中身はダミー)、純粋にApacheの転送能力のみの比較になります。サーバには、CPUはAtom D510、メインメモリ4GBを用意しました。クライアントとサーバの間はGbEで繋がっており、同一セグメント(中間ノード無し)となっております。
対象となるダウンロードファイルは1kB、10kB、100kBの3種類とし、それぞれabにおけるリクエスト数の総数を下表のようにしました。
| ファイル | リクエスト数(-nオプション) | 同時接続数(-cオプション) |
|---|---|---|
| 1k | 1000000 | 100 |
| 10k | 1000000 | 100 |
| 100k | 100000 | 100 |
サーバ側ではpreforkとeventを切り替えて設定、こちらもそれぞれ下表のように設定しました。なお、MinSpareThreads/MaxSpareThreadsはMaxClientsと同値としました。プロセス/スレッド数の調整、生成および破棄によって生じる性能差を比較対象から外すためです。
| MPM | MaxClients | ThreadsPerChild |
|---|---|---|
| prefork | 64 | - |
| event | 64 | 16 |
Atom D510はLinuxカーネル上では4コアとして認識されます(デュアルコア、HyperThreading有)。これに合わせてevent MPMでは、MaxClientsの全体としてはpreforkと同値にしつつ、4コアを使いきるようにプロセス数を4としてスレッドはその中で分散するように設定しました。
さて、以上の環境でもって実験した結果、次のようなデータとなりました。一つずつ見て行きましょう。まずはGangliaで収集したCPU使用率とロードアベレージのグラフです。03:15の前と後でevent-1kとprefork-1k、続いて03:30の地点でevent-10k、03:35にprefork-10k、03:40にevent-100k、03:45の前にprefork-100kとなっています。
1kについては、ロードアベレージのグラフをご覧下さい。03:15の前後で差が出来ているのが分かるかと思います。1kという大変小さなファイルへのHTTPリクエストなので、preforkではリクエストが届くと同時にほぼ一気に全てのプロセスが活性化してロードアベレージおよび実行中プロセス数が急上昇しています。実行中プロセス数は、MaxClientsの上限値=64にほぼ張り付いています。一方で、event-1kの方ではあまり大きな上昇になっていません。これはそもそも処理の分散をプロセスではなくスレッドによって賄っているため、同時リクエスト数の上昇がプロセス数の上昇としては現れないようになっていることを表しています。
このように、1kファイルのテストではpreforkとeventそれぞれの特性をよく表していると言って良いのではないでしょうか。64プロセスを事前に立ち上げて用意しておくことにより、バースト時には全てのプロセスが一気に動こうとするprefork MPMと、プロセス数は抑制しつつ、スレッドで捌くことによって過負荷状態を避けるevent MPMと、はっきりと違いが現れています。
これが10kから100kへと移ってくると、次第にネットワーク転送の方が律速となってきて、実行中プロセス数の極端な上がりは無くなります。おそらくサーバの性能やネットワーク環境にもよるのでしょうが、preforkにおける負荷集中時の実行中プロセス数の急騰は、小サイズのファイルによって引き起こされる可能性が高いということになります。
次にabの結果です。それぞれ秒間リクエスト処理数、および全体転送バイト数です。
- 1k Download
- prefork: 4913.44 reqs/sec - 6093.82 KB/sec
- event: 4561.75 reqs/sec - 5657.64 KB/sec
- 10k Download
- prefork: 3651.18 reqs/sec - 37396.49 KB/sec
- event: 3836.81 reqs/sec - 39297.67 KB/sec
- 100k Download
- prefork: 848.33 reqs/sec - 85042.92 KB/sec
- event: 853.21 reqs/sec - 85536.30 KB/sec
ネットワークI/Oとしての性能差はあまりないようです。10kと100kではeventが勝っており、ファイルが大きくなるにつれeventの方が(若干)優勢であるという仮説が立てられそうな気もしますが、決して優位な差とは言えず、より多くのデータをとって検証を行う必要があります。
ただ、1kにおいてpreforkの方が若干勝っているのは、実行中プロセス数がバーストする分少しでも多くのリクエストを捌くことが(結果として)できているということ、eventは若干CPUリソースを使い切れないでいることの傍証になるかと思います。CPU利用率のグラフを見ると、prefork-1kでは100%振り切っているのに対してevent-1kではCPU時間を98%程度までしか使いきっていませんね。
以上のように、MPMそれぞれにおいてどのような性能・特性の変化が出るかを見てみました。しかしこれはWebサービスを行う上で、極限られた一部分のみに基づいた比較になっています。JavaやPHPなど各種動的サイトではどうなのか、GbE直結じゃなくもっと高遅延低帯域のネットワーク越しではどうなのか、見るべき箇所は山ほどあります。次回以降更にさまざまなテストケースを設定しつつ、調査を進めて行こうと思います。
Windows仮想プリンタプログラムを作ってみる
システムは本物のプリンタだと信じているのに実はそれはソフトウェアへのインターフェイスにすぎず、印刷ジョブを渡したら最後、データは隅から隅までなめまわされ好きなように処理されてしまう。ということは、その気になればあんなことやこんなこともできてしまうはず・・・。 あらためて考えてみるとなかなか面白い話なので、仮想プリンタのしくみを調べて何かプログラムを書いてみたいと思いました。
手はじめに、定番の題材として所定のドキュメントを PDF や画像に変換しファイル出力する仮想プリンタを作ってみることにしました。 ひとつの仮想プリンタを自作のコードで構築し、データ出力部分を著名なオープンソースソフトウェアの Ghostscript と RedMon、ImageMagick で処理する内容です。
作成したプログラムとそのソースコードをフリーソフトウェアとして公開します。 こういった具体的な開発情報は案外見あたりませんので興味のあるかたはご覧下さい。
|
ダウンロード:MyVirtualPrinter ver 1.0.0.2
※ダウンロードできないときはこちらから |
今回はこの MyVirtualPrinter を切り口に、Windows での印刷処理の概要と仮想プリンタをプログラムで構成する方法について説明します。
続きを読む
「エキスパートPythonプログラミング」を翻訳しました
昨年の12月から翻訳していた、「エキスパートPythonプログラミング」(原題「Expert Python Programming」) という本が、5/31に発売になります。

エキスパートPythonプログラミング
著者:Tarek Ziade
販売元:アスキー・メディアワークス
発売日:2010-05-28
クチコミを見る
まだAmazonには目次が載っていませんが、 http://dl.dropbox.com/u/284189/epp/index.html で翻訳中時点の目次を公開しています(出版されるものと若干違いがあります)。
キャッチコピーにもありますが、この本はPythonの入門書ではなく、 すでにある程度Pythonのプログラムを書ける人がこれから活躍していく上で必要な 知識を網羅的に習得するための本です。 コンテキストマネージャーやメタクラスなどの高度なイディオム、 パッケージングとデプロイ、テストドリブンプログラミングや開発プロセス、 有用なドキュメントの整備、最適化などなど、開発しているアプリケーションの ジャンルを問わない、全てのPythonプログラマに有用な知識を提供しています。
私は、12章(プロファイリング)、13章(最適化)、14章(デザインパターン)までの翻訳と、 追加章(Unicode)の執筆を行いました。 また、2章(クラス以下のレイヤのイディオム)、3章(クラスレイヤのイディオム)についても、 全コードをテストしたり、サンプルコードをPython 3に対応させたり、特に力を入れて査読しています。
もともと原著が非常に良い本なのですが、翻訳版では原著が出版されてから2年の間の変化に対応していることと、 翻訳者が翻訳だけでなく技術的な面の検証もていねいに行なって多くのミスを修正したので、 翻訳版は原著より良いものになっていると自信を持って言えます。
Pythonの世界で活躍されている方、これから活躍しようと思っておられる方は、 ぜひ読んでみてください。 また、この本にはPython以外の言語にも共通する基本的な考え方が多く書かれていますし、 この本で紹介されている Mercurial, Sphinx, Buildout, Buildbot などのツールは Python以外の言語のプロジェクトでも有用ですので、Pythonが第一言語で無い方にも おすすめします。
ところで、翻訳中に「もっと詳しく説明したい」、「他の手段も紹介したい」、など、 書きたかったものの本文の内容からそれるとか、ページ数を増やしてまで説明する事では 無いなどの理由で、泣く泣く削った文章もあります。 このまま捨ておくのももったいないので、再編集して、このBlogで紹介して行きたいと思います。 「エキスパートPythonプログラミング」を補完する記事としてお読みください。
起動不能なPCからインポートずみ証明書を救出せよ!
1. ことの始まり
先日、遠くの知人から「手元の PC で Windows XP を起動できなくなった」と連絡がありました。 最初のロゴ表示が終わったところでフリーズする状態で、セーフモードでの起動もできないそうです。
彼は言います。
「HDD を取り出し別の PC につないで最新版のデータファイルを全部拾い上げたので、普段使ってるソフトの数を考えればもとの環境の復旧に時間をかけるよりも新しい環境へデータを移行する方が早そうだ」
幸い HDD に物理的損傷はなかったようです。
よかったね今後は HDD のまるごとバックアップもこまめにやるべし、と答えると、「実はひとつだけ問題が残っている」とのことでした。
話はここから始まります。
続きを読む


