0

NMOS6502 エミュレーターを複数のクラスにリファクタリングしている最中です。関数ジャンプ テーブルを定義する「オブジェクト指向」の方法があるかどうか疑問に思っていました。基本的に、CPU オブジェクトへの参照を持つ「CStackInstHandler」や「CArithmeticInstHandler」など、関連する CPU 操作のグループを分類するために個別の命令クラスを定義しました。各命令クラスは、抽象命令クラスから派生します。各派生命令クラスには、cpu オブジェクトのパブリック インターフェイスを使用して cpu の状態を変更する一連の関数があります。

uint8_t opcode = _memory->readMem(_cpu->getProgramCounter());
AInstructionHandler* _handler = _cpu->getInstHandler(opcode);
_handler->setCpu(&cpu);
_handler->setMemory(&memory);
_handler->execute(opcode);    

問題は、実行時に、命令ハンドラーとそのハンドラーに定義された適切なメンバー関数をオペコードを使用して決定する必要があることです。

したがって、オペコードはメモリから読み取られ、CPU はテーブルを使用してオペコードを命令ハンドラー タイプにマップし、命令ハンドラーは同じオペコードを使用して正しい関数を選択します。各命令は、「実行」機能をオーバーライドします。例:

void CBranchInstHandler::execute() {
    switch(_opcode) {
        case 0x90:
            this->BCC();
            break;
        case 0xb0:
            this->BCS();
            break;
        case 0xf0:
            this->BEQ();
            break;
        case 0x30:
            this->BMI();
            break;
        case 0xd0:
            this->BNE();
            break;
        case 0x10:
            this->BPL();
            break;
        default:
            break;
    }

}

void CBranchInstHandler::BCC() {
    uint16_t address = this->getAddress();
    if(!_cpu->isCarry()) {
        uint16_t pc = _cpu->getPC();
        pc += address;
        _cpu->setPC(pc);
    }
}

/*more instruction specific functions...*/

最終的に 2 つのルックアップが発生しますが、そのうちの 1 つは冗長です。1 つはハンドラーを選択し、もう 1 つはハンドラー関数を選択します。これはこのタスクを達成するための間違った方法のように感じますが、非メンバー関数のグループに委譲するだけではない代替手段については確信が持てません。

誰かがこの問題について洞察を持っているかどうか疑問に思っています。基本的には、クラスをより小さなバイト (cpu クラスと命令クラスにリファクタリングされた命令メンバー関数を持つ cpu クラス) にリファクタリングしたいということですが、すべてのコンポーネントが非常に相互に関連しているため、同じことを繰り返す必要があります。冗長性が導入されます。

非オブジェクト指向の解決策は、これらの命令を cpu 参照を受け入れる非メンバー関数にすることです。次に、関数ジャンプ テーブルが定義され、命令が検索され、オペコードによってインデックス付けされて実行されます。

これは、オブジェクトでは実際には実用的ではないようです。すべての命令を静的にすることもできますが、これはポイントを逃しているようです。

接線的に関連する問題であっても、洞察や情報は非常に役立ちます。

ありがとう。

4

3 に答える 3

1

私のコメントを答えに昇格させます。オブジェクト指向のソリューションは、あなたが言うように、子クラスに、応答するオペコードを決定する完全な責任を与えることです。

これを行う最も簡単な方法は、2 段階を構築しようとするのではなく、switchすべてのオペコードをすべての子にルーティングし、子に貢献するかどうかを任せることです。それが実行可能な最小の解決策です。

最適化が必要な場合、最も簡単な方法は次のように再定式化することです。

void CBranchInstHandler::execute() {
    switch(_opcode) {
        case 0x90:
            this->BCC();
            break;
            ... etc ...
    }
}

に:

FuncPtr CBranchInstHandler::execute() {
    switch(_opcode) {
        case 0x90:
            return BCC;
            ... etc ...
    }
    return NULL;
}

したがって、それぞれexecuteが実際にそのオペコードを処理したかどうかを返します。

親クラス内では、オペコードから関数ポインターまでのテーブルを単純に保持できます。配列で十分です。テーブルには、最初はNULLずっと s が含まれています。

オペコードを実行するときは、テーブルでハンドラーを検索します。ハンドラーが存在する場合は、それを呼び出して先に進みます。そうでない場合executeは、ハンドラーが返されるまですべての子を順番に呼び出し、それらをテーブルに入れてから呼び出します。したがって、実行時にジャストインタイムでビルドします。各オペコードの最初の実行には少し時間がかかりますが、その後はジャンプ テーブルに相当するものになります。

その利点は、子が処理するものに関する情報を構文的に実際の処理と密接に関連付けることができるため、コードのオーバーヘッドとエラーの可能性が減少することです。

于 2015-06-02T20:54:33.730 に答える
1

クラスメンバー関数/メソッドへのポインターを使用できます。

void (CBranchHandlerBase::*)();

for store を使用して、指定された に対して呼び出す必要があるメソッドへのポインターを格納します_opcode

map<uint8_t, void (CBranchHandlerBase::*)()> handlers;
handlers[0x90] = &BCC;
handlers[0xb0] = &BCS;
...

上記のコードは、ハンドラーの基本クラス内の初期化セクション/メソッドで提供する必要があります。もちろん、このアプローチを機能させるには、BCC、BCS などを純粋仮想メソッドとして宣言する必要があります。

次に、スイッチの代わりに:

void CBranchHandlerBase::execute() {
    (this->*handlers[_opcode])();
}

execute は基本クラスで定義されていることに注意してください (各 Handler は execute メソッドと同じ機能を持つため、仮想である必要はありません)。

編集:マップは実際にはサイズのベクトルまたは配列に置き換えることができます:2^(8*sizeof(uint8_t))効率上の理由から

于 2015-06-01T14:18:27.300 に答える