Python
Python3

Python3.5で実装されたasync/awaitを使って軽量スレッドの性能ベンチマーク

More than 3 years have passed since last update.

Python3.5でasync/awaitが追加されていたのでメモリ消費量とコンテキストスイッチのコストの観点でベンチマークを取ってみました。


async/await構文とは

非同期処理やノンブロッキングI/O処理を良い感じに書ける非同期処理のパラダイムにおける最先鋭の構文です。C#に実装されたあと、C++,VB,Node.jsに実装されついにPythonにもやってきた!という感じです。特徴はいままでThreadingで頑張って書いてた非同期処理が、より簡潔により強力に書けるようになります。軽量スレッドとはマイクロスレッド、ファイバーとも呼ばれるもので、「C10K問題」(クライアント1万台問題)と言われるI/O待ちによってクライアント数が多いとハードウェアの性能が生かしきれない問題の解決策の1つです。I/O待ちの際に高速にコンテキストスイッチして他のクライアントを捌くことでハードウェアの性能を限界まで引き出すプログラムを書くことが出来るようになります。つまり使いこなすとnginxのように高速軽量に動作するサーバを書くことが出来る様になります。


ベンチマーク結果

ベンチマークの結果はPythonのasync/awaitを利用したコルーチンは起動が早く、メモリ消費量が少なく、コンテキストスイッチも早かったです。まさに軽量スレッドと言える実装になっているのではないでしょうか。Erlamgの軽量スレッドはすごいErlang本によると消費メモリ4KByteで起動にマイクロ秒単位で起動するみたいです。Python版はメモリ消費3.44KByte,起動に60マイクロ秒とErlangと比肩する速度が出ています。

項目

コルーチン10万個の起動と終了時間
6.081sec

コルーチン1個あたりの起動と終了時間
0.0608ms

コンテキストスイッチ100万回の実行時間
51.435sec

コンテキストスイッチ1回あたりの実行時間
51.435μs

コルーチン10万個起動時の消費メモリ
146.86MByte

コルーチン1個あたりの消費メモリ
1.50KByte

コルーチン10万個起動してコンテキストスイッチ100万回実施時の消費メモリ
336.30MByte

コンテキストスイッチ100万回実施時の1個あたりの消費メモリ
3.44KByte

※消費メモリについてオーバーヘッドを考慮していないので、不正確な値になっています。ご注意ください。


Pythonのasync/awaitが向いてる処理

ネットワークI/OやファイルI/Oを複数非同期に行う処理


async/awaitを試す


普通のFizzBuzzを書く


普通のFizzBuzz

def fizzbuzz(i):

if i == 15:
return 'FizzBuzz'
if i % 5 == 0:
return 'Buzz'
if i % 3 == 0:
return 'Fizz'
return str(i)

for x in range(1, 10):
print(fizzbuzz(x))



async/awaitでコルーチン1個のFizzBuzz

コルーチンで実行しているだけなので結果は同じになります。

スクリーンショット 2015-11-03 18.59.47.png


コルーチン1個

import asyncio

async def main():
await task_fizzbuzz()

async def task_fizzbuzz():
for x in range(1, 10):
print(fizzbuzz(x))
return None

loop = asyncio.get_event_loop()
# コルーチンでmain関数を呼び出し
loop.run_until_complete(main())
loop.close()



async/awaitでコルーチン10万個のFizzBuzzを非同期実行

コルーチンを10万個生成して同時実行します。1つのコルーチンが実行終了すると、コンテキストスイッチして次のコルーチンが実行されます。

スクリーンショット 2015-11-03 18.52.02.png


コルーチン10万同時実行

# -*- coding: utf-8 -*-

# from asyncio import Queue
# from queue import Queue
import asyncio

async def task_fizzbuzz(prefix):
for x in range(1, 10):
# await asyncio.sleep(1)
print(prefix + "{}:".format(str(x)) + fizzbuzz(x))
return None

loop = asyncio.get_event_loop()
# コルーチン10万個生成
tasks = asyncio.wait([task_fizzbuzz(str(i) + ":") for i in range(1, 100000)])
loop.run_until_complete(tasks)
loop.close()



コルーチン10万個実行結果

....

71798:7:7
71798:8:8
71798:9:Fizz
84034:1:1
84034:2:2
84034:3:Fizz
84034:4:4
84034:5:Buzz
84034:6:Fizz
84034:7:7
84034:8:8
84034:9:Fizz
17235:1:1
17235:2:2
....



async/awaitでコルーチン10万個のFizzBuzzをsleep付きで非同期実行

fizzbuzzロジック中にsleepしたとき上手くコンテキストスイッチするかのテストです。


sleep付きのコルーチン10万同時実行

# -*- coding: utf-8 -*-

# from asyncio import Queue
# from queue import Queue
import asyncio

async def task_fizzbuzz(prefix):
for x in range(1, 10):
await asyncio.sleep(1) # 新しく追加したコード
print(prefix + "{}:".format(str(x)) + fizzbuzz(x))
return None

loop = asyncio.get_event_loop()
# コルーチン10万個生成
tasks = asyncio.wait([task_fizzbuzz(str(i) + ":") for i in range(1, 100000)])
# 並列実行
loop.run_until_complete(tasks)
loop.close()



sleep付きのコルーチン10万個実行結果

....

75219:6:Fizz
8282:6:Fizz
57464:6:Fizz
75220:6:Fizz
8283:6:Fizz
57465:6:Fizz
75221:6:Fizz
8284:6:Fizz
57466:6:Fizz
75222:6:Fizz
8285:6:Fizz
57467:6:Fizz
75223:6:Fizz
....


コルーチン実行中にawait asyncio.sleep(1)でsleepすると他のコルーチンにスイッチするため同時実行しているような結果が得られました。

スクリーンショット 2015-11-03 19.25.56.png


ベンチマークを取る

検証が終わったので次の3つのプログラムでベンチマークを取っていきます。


  1. コルーチンを利用せずFizzBuzz 10回 x 10万回実行するプログラム

  2. コルーチンを10万個立ち上げてFizzBuzz 10回 x 10万回実行するプログラム

  3. コルーチンを10万個立ち上げてFizzBuzz1回毎にSleepを実行することでコンテキストスイッチを計100万回実行する、FizzBuzz 10回 x 10万回実行するプログラム

スクリーンショット 2015-11-03 19.46.41.png


1. コルーチンなし

# -*- coding: utf-8 -*-

import time

def fizzbuzz(i):
if i == 15:
return 'FizzBuzz'
if i % 5 == 0:
return 'Buzz'
if i % 3 == 0:
return 'Fizz'
return str(i)

COUNT = 100000
FIZZBUZZ_COUNT = 10

# 計測開始
ts = time.time()

# 10回FizzBuzz x 10万回実行
for x in range(COUNT):
for x in range(FIZZBUZZ_COUNT):
print(fizzbuzz(x))
# 他と条件を同じにするために10秒待つ
for x in range(FIZZBUZZ_COUNT):
time.sleep(1)

# 計測終了
te = time.time()

# 結果出力
print("{}sec".format(te-ts))


2.コルーチン10万個立ち上げ

# -*- coding: utf-8 -*-

import time
import asyncio

def fizzbuzz(i):
if i == 15:
return 'FizzBuzz'
if i % 5 == 0:
return 'Buzz'
if i % 3 == 0:
return 'Fizz'
return str(i)

COUNT = 100000
FIZZBUZZ_COUNT = 10

# 計測開始
ts = time.time()

# 10回FizzBuzz x 10万回実行
async def task_fizzbuzz(prefix):
for x in range(FIZZBUZZ_COUNT):
# await asyncio.sleep(1)
print(prefix + "{}:".format(str(x)) + fizzbuzz(x))
return None

loop = asyncio.get_event_loop()
tasks = asyncio.wait([task_fizzbuzz(str(i) + ":") for i in range(COUNT)])
loop.run_until_complete(tasks)
loop.close()

# 他と条件を同じにするために10秒待つ
for x in range(FIZZBUZZ_COUNT):
time.sleep(1)

# 計測終了
te = time.time()

# 結果出力
print("{}sec".format(te-ts))


3.コルーチン10万個立ち上げコンテキストスイッチ100万回

# -*- coding: utf-8 -*-

import time
import asyncio

def fizzbuzz(i):
if i == 15:
return 'FizzBuzz'
if i % 5 == 0:
return 'Buzz'
if i % 3 == 0:
return 'Fizz'
return str(i)

COUNT = 100000
FIZZBUZZ_COUNT = 10

# 計測開始
ts = time.time()

# 10回FizzBuzz x 10万回実行
async def task_fizzbuzz(prefix):
for x in range(FIZZBUZZ_COUNT):
await asyncio.sleep(1)
print(prefix + "{}:".format(str(x)) + fizzbuzz(x))
return None

loop = asyncio.get_event_loop()
tasks = asyncio.wait([task_fizzbuzz(str(i) + ":") for i in range(COUNT)])
loop.run_until_complete(tasks)
loop.close()

# 計測終了
te = time.time()

# 結果出力
print("{}sec".format(te-ts))


ベンチマーク結果

各3回しかやってないよ。コンテキストスイッチ時間掛かりすぎぃ(›´ω`‹ ) 

結果を単純にするために書いてないけどベンチマーク2もコンテキストスイッチ10万回発生してそう。

スクリーンショット 2015-11-03 19.58.15.png

スクリーンショット 2015-11-03 19.50.43.png


参考

PEP 0492 -- Coroutines with async and await syntax

18.5.3. タスクとコルーチン

18.5.9. Develop with asyncio

Python3.5の新機能

How to use async/await in Python 3.5?

Async および Await を使用した非同期プログラミング (C# および Visual Basic)

AsyncIO vs Gevent? : Python - Reddit