Luaのコルーチンをいろんな方法で作って実行してみる in C++(ついでに時間も測る)

More than 3 years have passed since last update.

現在、C++, Lua, luabindを使ってゲームなど作って遊んでいます。Luaのコルーチンをよく使うのですが、コルーチンの作成は結構遅い処理だというのをどこかで読んで、気になっていました。

コルーチンの作成方法・実行方法は何通りかあるので、それらの普段使っていない方法も試してみるついでに時間も測ってみました。


ソースコード


coro.lua

local a = 0

function coro_func()
for i=1, 100 do
a = a + math.random()
coroutine.yield(0)
end
return 1 --コルーチンが終了したら1を返す仕様
end

function run_coroutine_lua()
local coro = coroutine.create(coro_func)
repeat
ret1, ret2 = coroutine.resume(coro)
until ret2 == 1
end

function print_a()
print(a)
a = 0
end



luathread.cpp

#include <iostream>

#include <chrono>
#include <lua.hpp>
#include <luabind/luabind.hpp>
#include <luabind/object.hpp>
using namespace std;

const int STACKSIZE = 5100;
const int N = 5000; // 繰り返し回数

lua_State* init_lua()
{
lua_State* L = lua_open();
luaL_openlibs(L);
lua_checkstack(L, STACKSIZE);
luaL_dofile(L, "coro.lua");
return L;
}

// 時間測る用
void print_elapsed(chrono::system_clock::time_point start)
{
cout << chrono::duration_cast<chrono::milliseconds>
(chrono::system_clock::now() - start).count() << ",";
}

int main()
{
lua_State* L = init_lua();
lua_State* thread;

// ◆パターン1:Lua側でコルーチンを作って実行する
auto start = chrono::system_clock::now();
for (int i = 0; i < N; i++) {
luabind::call_function<void>(L, "run_coroutine_lua");
}
print_elapsed(start);

// ◆パターン2:C++側でコルーチンを作って実行する
start = chrono::system_clock::now();
thread = lua_newthread(L);
for (int i = 0; i < N; i++) {
lua_getglobal(thread, "coro_func");
int ret = 0;
while (ret == 0) {
lua_resume(thread, 0);
ret = lua_tointeger(thread, -1);
}
}
print_elapsed(start);

// ◆パターン3:C++側でコルーチンを作って実行する(luabind)
start = chrono::system_clock::now();
thread = lua_newthread(L);
for (int i = 0; i < N; i++) {
int ret = luabind::resume_function<int>(thread, "coro_func");
while (ret != 1) {
ret = luabind::resume<int>(thread);
}
}
print_elapsed(start);

// ◆パターン4:C++側でコルーチンを作って実行する
//(lua_newthreadを毎回呼ぶ、遅そうだが…)
start = chrono::system_clock::now();
for (int i = 0; i < N; i++) {
thread = lua_newthread(L);
int ret = luabind::resume_function<int>(thread, "coro_func");
while (ret != 1) {
ret = luabind::resume<int>(thread);
}
}
print_elapsed(start);

cout << "\n";
lua_close(L);
return 0;
}



ソースコードの内容について

コルーチンが終了するまでresumeする処理をN回繰り返してその時間を測っています。コルーチンの中身は適当です。


・パターン1:Lua側でコルーチンを作って実行する

毎回coroutine.createでコルーチンを作って実行しています。めっちゃ遅そうです。


・パターン2:C++側でコルーチンを作って実行する(1)

lua_newthreadでスレッドを作ってlua_resumeで実行しています。

コルーチンが終了した後、新たにlua_newthreadを呼ぶ必要は実はないんですね。既に終了したスレッドをlua_resumeに渡せばまた最初から実行できます。


・パターン3:C++側でコルーチンを作って実行する(2)

私が普段使っている、luabindによるやり方です。スタックの状態とかあまり考えなくていいので楽です。


・パターン4:C++側でコルーチンを作って実行する(3)

パターン1のような感じで、lua_newthreadを毎回呼んでみました。


処理時間の測定結果

上のプログラムを100回実行して処理時間のグラフ描いてみました。

予想通りパターン1(Lua)は他に比べて遅いです。しかしパターン2~4はそこまで差がありません。

lua_newthreadよりもluabindの影響のほうが大きいように見えます。

graph.png


実行環境


  • Core i5-4570 3.2GHz

  • RAM 8GB

  • Windows 7 64bit SP1

  • Visual Studio Community 2015 Update 1

  • Lua 5.1.5 (x64)

  • luabind 0.9


参考にしたサイト


この記事書いてて気になったこと

・処理速度的には毎回lua_newthread呼んでも別に良い気がする

  →スタック上限にすぐ達しそう

  →今まではスレッド1000個くらいスタックに積んで使い回す感じで足りてたので気にしてなかった

  →本当に大量に使いたい場合はスレッドをスタックからレジストリに移したりする必要とかがあるかも(調べてない)

こちらの方が書いていらっしゃる「コルーチンの利用」の記事にあるような、渡された関数を無限に実行するだけのコルーチンを大量に作っておいて使い回す方法も試してみたかった(面倒なので多分やらない…)