実は既に結構挫折気味。

やっぱり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 = 0
    self.X = 0
    self.Y = 0
    self.PC = 0
    #  .
    #  .
    #  .

見たいなクラスにするとして、足し算するときなどは必ず self.A = (self.A + x) & 0xff みたいにして8bitに収めないといけない。ここで

def _create_n_bit_property(name, mask):
  rname = "_"+name
  result = {
    name:property(lambda self: getattr(self, rname),
                  lambda self,v : setattr(self, rname, v & mask),
                  )
  }
  return result

class 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 = 0
    self._X = 0
    self._Y = 0
    self._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);
    else
            res = 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 += count
  self.passed_clocks -= clocks

なんてのになるわけで、ここが激しくループするわけで。ここはガリガリにちょっとでも節約できるものは節約。

def step_execute(self, clocks):
  read = self.memory.read
  get_method_by_opecode = self.get_method_by_opecode
  while (self.passed_clocks < clocks):
    old = self._PC; self._PC += 1; self._PC &= 0xffff
    opecode = read(old)
    method = get_method_by_opecode(opecode)
    count = method()
    count = count != None and count or CYCLES[opecode]
    self.passed_clocks += count
  self.passed_clocks -= clocks

javascriptで . (ドット)演算が遅いとかいうのは最近(?)よく言われていることで、それはPythonにも当然当てはまる。なのでループ前にだせるものはローカルに出しておく。(これが簡単にできるのがRubyよりもPythonがいい部分だよなあ)

あと、実行の部分。ここはCなら関数テーブルかswitch(コンパイラによるけど大差ないと思う)になるんだけど、あいにくPythonにはswitchがないので関数テーブル的なものか if opecode == 0x01: ... elif opecode == 0x02:... elif... かになる。

関数テーブル的なものは

def ope_0x01(self):
  #code
def ope_0x02(self):
  #code

self.__getattribute__("ope_"+hex(opecode))()

となる。両方やってみたところ、大差はなかったので、関数テーブル的なほうに。

結局

そんな感じでいじってみてかつpsycoを入れて1Frame:0.15程度(PPUやAPUは中身がないので、最後までつくったらもっと遅くなる)。ほかにもいじれそうなところはあって、そこをいじれば0.1は切れそうな感じがしてます。

けど、そこまでやるとPythonである意味がないのも確か。ぶっちゃけ、エミュレータは確実にCが向いている。Javaで書かれているエミュレータもレジスタの値をセットするたびに A & 0xff みたいなことをやっていて、どーもめんどくさい。

じゃぁLLでエミュレーターを書く意味ってなんだ、というと・・・うーん(笑 自己満足以外なにもないでしょうねえ。というわけで自己満足のために、今後もヒマができれば、ちょっとづつ書き進めてみようかなあ。


正月だというのにひたすらCPUの命令を実装・・・。ここはひたすら地道な作業が続く。やっぱりこういう単純作業は苦手だ・・・

実装の際には InfoNES6502 Emulation Package のソースコード、そして NES on FPGAのCPUのページ に大変お世話になりました。特にNES on FPGAのページは非常に分かりやすくまとめられていて助かりました。

実装方針は前にも書いたとおり「できるだけPythonっぽく」。着々とエミュレーターっぽくないソースが出来上がってきています(笑 エミュレータのソースコードというとそれはもう、C言語でマクロ全開、register指定、inline(もしくはstatic)で出力されるコードを極限まで速く、なんて感じですが全く逆です。分かりやすさ優先。とりあえず書いてみて、ダメそうなら諦めるか頑張ってチューニングするか考えよう。

今日までで、CPUの全命令は実装しました(したつもり)。ただ、Indexed Absolute AddressingのX,Y、そしてIndirect Indexed Addressingでページクロスしたとき1クロック余計に発生する、という件は今のところスルー。

メモリ周りもIO以外はだいたい作ったので、今日の時点で単純なコードなら実行可能に。早速アセンブラを書いてNESASMで.NESファイルを作成。それを読み込んで動かしてみました。とりあえず動くことは動く・・・が全命令が正しく実装されているかはまだ分からないです(笑

というわけで次はデバッグ環境を整えて、命令のテストをしないとなあ。       余談。いやー卒論という重圧がないのは非常にプログラミングがはかどる。別にギリギリなわけでもないのに「卒論完全に出来上がってないのにこんなことしてていいのかなーって感じちゃう」的なものがないし。


2007年になりましたね。月に1回更新するか怪しいこのブログを購読してくださってる方々、ありがとうございます(笑

無事卒論も提出し、気楽な学生気分を満喫しています。ちなみに、卒論は完璧にコンピュータは関係ないもので、まぁラットと戯れていました。生物、こと脳にかかわる話も面白いですよ。まったくもって大した研究ではないのですが、教授が今年ヨーロッパの学会で発表してくださるらしいです。とりあえず書けて何より。

さて、プログラミング関連ですが、卒論もようやく終わり4月までは暇な学生生活、まとまった時間がとれるようになったので、ちょろちょろ書き始めています。

書いているのは・・・なんと今更NESエミュレータ。

一から自分で解析してエミュレータを書くのは達人ワザですが、十分なハードウェアの情報がある場合、実はそんなに難しくなかったりします。

というわけで書いているんですが、ここで普通にC言語やアセンブラで書こうというような楽?はしてません!NESエミュレータをPythonで書いてみる、これがチャレンジング。ぶっちゃけPythonで速度がシビアに求められるものを書いたことがないので、チャレンジです。

書く上で、とりあえず

  • ctypesは使わない(Cの流儀をそのままもっていってもおもしろくない)
  • 当然Cで拡張モジュールは書かない

というポリシーで書き進めています。

しかしやっぱりキツイ。絶対Cの方が楽な気がするのは気のせいだろうか・・・。現在はだいたいCPU(6502)のコアが書き終わったので、メモリ周りを書き進めてます。とりあえず.NESファイルから読み込んでメモリに読み込み、PCをセットするあたりまで書きました。

まず、NESのヘッダ読み込むのもめんどくさい。Cならfreadで構造体にマップしておわり、なのにPythonだとそうは行かない。そしてなんといってもポインタがない。これ。メモリのミラーリングとかめんどくさいだろうな・・・(笑

そうこう書いてるうちに、Pythonでのバイナリの扱いが上手くなってきました(きたような気がします)。       今のところ以下のような関数が活躍しています。エディアンは今のところ決めうちです。

from struct import * 
from array import array

def unpack_byte(b):
  return unpack("<B", b)[0]

def zero_filled(n):
  n /= 2
  return pack('h'*n, *([0]*n))

def byte_array(v):
  return array('c', v)

class byte_ref(object):
  def __init__(self, p):
    self._value = p
  def get_value(self):
    return self._value
  def set_value(self, v):
    self.value[0:] = byte_array(v)
  value = property(get_value, set_value)

まず、PythonでRAMをエミュレートしようと思うと、可変なunsigned charな配列が当然必要です。

Pythonで data = open("name", "rb").read() とした場合、きちんとバイナリデータがdataに入って、 unpack_byte(data[0:1]) などとするとちゃんとデータを取り出せます。

しかし、この場合、Pythonの内部では*不変な型である*文字列型として扱われているので data[w_addr & 0x7ff] = 0x12 みたいなことは出来ません。つまりROMにはなりますが、RAMになれません。じゃあどうやってunsigned charの配列を表現するか、ということで array モジュールの登場なわけです。 data = byte_array(open("name", "rb").read()) という感じで使っています。生成方法がことなるだけで data[0] みたいなインデックスによるアクセスも出来るし、部分書き換えも可能です。

byte_refは・・・別になくてもいいかもしれませんが、byte配列に対する参照を表現するオブジェクトです。といっても注意して使わないと意味がないクラスで気持ちの問題かもしれません。

ここをctypesでポインタを使えば考えなくてもよかったんですが、あえてPythonなんだ、ということで(笑

 

   

近況はこんな感じです。

ついに就職が近づいてきました。仕事はプログラマではないので、プログラムを書く時間はあんまり取れないかもしれません。でもまぁ、日曜プログラマとして細々やっていけたらなあ、と思っています。