実は既に結構挫折気味。
やっぱりPythonではちょっと厳しいかもしれない。
とりあえず一番ややこしいPPU周りの情報を調べて、ちょろちょろ書き始めたあたりでいったんCPU部分のパフォーマンスを調べてみました。
かなり厳しいものがあります。CPU部分のコードはPython的な書き方で書いてたんですが、これじゃ話にならない。まず、アクセサなんてものはつかっちゃいけないのだ。
以下環境はOS:WinXP,CPU:Athlon64 3000+,Memory:1G,Python2.4です。
CPUのレジスタ関連の実装
NESのCPUである6502のレジスタはPCは16bit、それ以外は8bit。8bitの値なんてものはCならunsigned charで一発なんだけど、Pythonにはそんなものない。足し算したらどんどん大きくなるし、引き算したらどんどん小さくなる。ので
class Py6502(object):def __init__(self):self.A = 0self.X = 0self.Y = 0self.PC = 0# .# .# .
見たいなクラスにするとして、足し算するときなどは必ずself.A = (self.A + x) & 0xffみたいにして8bitに収めないといけない。ここで
def _create_n_bit_property(name, mask):rname = "_"+nameresult = {name:property(lambda self: getattr(self, rname),lambda self,v : setattr(self, rname, v & mask),)}return resultclass ForceNbitType(type):def __new__(cls, class_name, class_bases, classdict):names = classdict.get("__16bit__")for name in names:classdict.update(_create_n_bit_property(name, 0xffff))names = classdict.get("__8bit__")for name in names:classdict.update(_create_n_bit_property(name, 0xff))cls = type.__new__(cls, class_name, class_bases, classdict)return cls
のようなmetaclassをつくって
class Py6502(object):__metaclass__ = ForceNbitType__8bit__ : "A", "X", "Y"__16bit__ : "PC",def __init__(self):self._A = 0self._X = 0self._Y = 0self._PC = 0# .# .# .
とすればself.A += 10とかしてもちゃんと8bitに収まる。
非常に上手くかけるんですが、こんなのやってらんない。遅すぎる。1Frame分(28000サイクル程度)の実行に0.5秒(笑
getattr
getattrは遅い。getattrの真価は第3引数でdefault値が指定できること(だと思う)。今回のように確実に属性が存在することが分かっているならself.__getattribute__(name)を使うと結構違ってくる。ちなみにPythonのソースではこんな感じ。
static PyObject *slot_tp_getattro(PyObject *self, PyObject *name){static PyObject *getattribute_str = NULL;return call_method(self, "__getattribute__", &getattribute_str,"(O)", name);}static PyObject *slot_tp_getattr_hook(PyObject *self, PyObject *name){PyTypeObject *tp = self->ob_type;PyObject *getattr, *getattribute, *res;static PyObject *getattribute_str = NULL;static PyObject *getattr_str = NULL;if (getattr_str == NULL) {getattr_str = PyString_InternFromString("__getattr__");if (getattr_str == NULL)return NULL;}if (getattribute_str == NULL) {getattribute_str =PyString_InternFromString("__getattribute__");if (getattribute_str == NULL)return NULL;}getattr = _PyType_Lookup(tp, getattr_str);if (getattr == NULL) {/* No __getattr__ hook: use a simpler dispatcher */tp->tp_getattro = slot_tp_getattro;return slot_tp_getattro(self, name);}getattribute = _PyType_Lookup(tp, getattribute_str);if (getattribute == NULL ||(getattribute->ob_type == &PyWrapperDescr_Type &&((PyWrapperDescrObject *)getattribute)->d_wrapped ==(void *)PyObject_GenericGetAttr))res = PyObject_GenericGetAttr(self, name);elseres = PyObject_CallFunction(getattribute, "OO", self, name);if (res == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) {PyErr_Clear();res = PyObject_CallFunction(getattr, "OO", self, name);}return res;}
上が__getattribute__、下がgetattr。ま、こんなことやったって関数呼び出しのオーバーヘッドが一番ツライのでgetterは使わず、setterは必要があるときだけ使うように。これで結構はやくなる。
実行ループ
CPUのエミュレータなんてのはだいたい同じようなパターンで割り込みを除いて簡単に書くと
def step_execute(self, clocks):while (self.passed_clocks < clocks):opecode = read()# opecodeを取得# 実行count = CLOCK[opecode] # 実行に必要なクロック数を取得self.passed_clocks += countself.passed_clocks -= clocks
なんてのになるわけで、ここが激しくループするわけで。ここはガリガリにちょっとでも節約できるものは節約。
def step_execute(self, clocks):read = self.memory.readget_method_by_opecode = self.get_method_by_opecodewhile (self.passed_clocks < clocks):old = self._PC; self._PC += 1; self._PC &= 0xffffopecode = read(old)method = get_method_by_opecode(opecode)count = method()count = count != None and count or CYCLES[opecode]self.passed_clocks += countself.passed_clocks -= clocks
javascriptで.(ドット)演算が遅いとかいうのは最近(?)よく言われていることで、それはPythonにも当然当てはまる。なのでループ前にだせるものはローカルに出しておく。(これが簡単にできるのがRubyよりもPythonがいい部分だよなあ)
あと、実行の部分。ここはCなら関数テーブルかswitch(コンパイラによるけど大差ないと思う)になるんだけど、あいにくPythonにはswitchがないので関数テーブル的なものかif opecode == 0x01: ... elif opecode == 0x02:... elif...かになる。
関数テーブル的なものは
def ope_0x01(self):#codedef ope_0x02(self):#codeself.__getattribute__("ope_"+hex(opecode))()
となる。両方やってみたところ、大差はなかったので、関数テーブル的なほうに。
結局
そんな感じでいじってみてかつpsycoを入れて1Frame:0.15程度(PPUやAPUは中身がないので、最後までつくったらもっと遅くなる)。ほかにもいじれそうなところはあって、そこをいじれば0.1は切れそうな感じがしてます。
けど、そこまでやるとPythonである意味がないのも確か。ぶっちゃけ、エミュレータは確実にCが向いている。Javaで書かれているエミュレータもレジスタの値をセットするたびにA & 0xffみたいなことをやっていて、どーもめんどくさい。
じゃぁLLでエミュレーターを書く意味ってなんだ、というと・・・うーん(笑 自己満足以外なにもないでしょうねえ。というわけで自己満足のために、今後もヒマができれば、ちょっとづつ書き進めてみようかなあ。