Объектно-ориентированная таблица переходов, определенная для нескольких классов

Я занимаюсь рефакторингом эмулятора NMOS6502 на несколько классов. Мне было интересно, существует ли «объектно-ориентированный» способ определения таблицы переходов функций. По сути, я определил отдельные классы инструкций для классификации групп связанных операций процессора, таких как «CStackInstHandler» или «CArithmeticInstHandler», которые будут иметь ссылку на объект процессора. Каждый класс инструкций является производным от абстрактного класса инструкций. Каждый производный класс инструкций имеет набор функций, которые будут использовать общедоступный интерфейс объекта процессора для изменения состояния процессора, например:

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

Проблема в том, что во время выполнения обработчик инструкций, а также соответствующая функция-член, определенная для этого обработчика, должны быть определены с использованием кода операции.

Итак, у нас есть - код операции считывается из памяти, таблица используется процессором для сопоставления кода операции с типом обработчика инструкции, а затем тот же самый код операции используется обработчиком инструкции для выбора правильной функции. Каждая инструкция переопределяет функцию «выполнить», например:

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...*/

Я получаю два поиска, один из которых является избыточным. Один для выбора обработчика, а другой для выбора функции обработчика. Я чувствую, что это неправильный способ выполнения этой задачи, но я не уверен в альтернативе, которая не просто переходит в группы функций, не являющихся членами.

Мне интересно, есть ли у кого-нибудь понимание этой проблемы. В основном это сводится к желанию реорганизовать класс на более мелкие кусочки (класс процессора с функциями-членами инструкций, реорганизованными в класс процессора и классы инструкций), но все компоненты настолько взаимосвязаны, что мне приходится повторяться. Вводится резервирование.

Необъектно-ориентированное решение состояло бы в том, чтобы эти инструкции были функциями, не являющимися членами, которые принимают ссылку на процессор. Затем будет определена таблица переходов функций, инструкции будут искать и индексировать по коду операции и выполнять.

Это действительно не кажется практичным с объектами. Я мог бы сделать все инструкции статическими или что-то в этом роде, но это, похоже, упускает суть.

Любое понимание или информация о даже косвенно связанных проблемах были бы очень полезны.

Спасибо.


person jonnywalkerr    schedule 01.06.2015    source источник
comment
Влияет ли getAddress на isCarry?   -  person kfsone    schedule 01.06.2015
comment
Это не. getAddress просто возвращает текущий счетчик программ, а isCarry просто проверяет, установлен ли бит переноса в данный момент времени.   -  person jonnywalkerr    schedule 01.06.2015
comment
Так зачем помещать его вне if?   -  person kfsone    schedule 01.06.2015
comment
Я вижу, откуда вы пришли. Вы правы, это должно быть внутри оператора if. Кроме того, getAddress() возвращает эффективный адрес, а не компьютер.   -  person jonnywalkerr    schedule 01.06.2015
comment
Вероятно, существует хорошее объектно-ориентированное решение с использованием функторов/лямбда-выражений, но позвольте мне добавить это заранее: не бойтесь использовать генератор кода или разумное использование препроцессора. Наборы инструкций, как правило, довольно плотные, поэтому вы не будете тратить время на фиксированный массив, чтобы сделать эти поиски очень дешевыми, но поддерживать такие вещи - боль.   -  person kfsone    schedule 01.06.2015
comment
Если вы хотите придерживаться этой категоризации инструкций, почему бы не позволить обработчикам регистрироваться в объекте ЦП (что сводилось бы к чему-то вроде таблицы поиска в объекте ЦП, заполненной адресами всех методов обработки кода операции)? Затем, чтобы выполнить код операции, вы должны сделать _cpu->execute(opcode).   -  person Michael    schedule 01.06.2015
comment
Возможно, глупый вопрос: если это просто вопрос дизайна, почему бы просто не вызывать каждый execute? Классы, которые не обрабатывают данный код операции, знают, что они его не обрабатывают и просто ничего не будут делать.   -  person Tommy    schedule 02.06.2015


Ответы (3)


Я собираюсь продвигать свой комментарий к ответу: объектно-ориентированное решение, как вы говорите, дает дочерним классам полную ответственность за решение, на какие коды операций они реагируют.

Я бы предположил, что самый простой способ сделать это — не пытаться построить двухэтапный switch, а просто направить каждый код операции каждому дочернему элементу и позволить дочернему элементу либо внести свой вклад, либо нет. Это минимально жизнеспособное решение.

Если вам нужна оптимизация, то проще всего будет переформулировать:

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

To:

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

Таким образом, каждый execute возвращает, действительно ли он обработал этот код операции.

В родительском классе вы можете просто сохранить таблицу от кода операции до указателя на функцию. Подойдет массив. Изначально таблица будет содержать NULLs.

При выполнении кода операции найдите обработчик в таблице. Если обработчик есть, вызовите его и двигайтесь дальше. Если нет, то вызовите execute для каждого дочернего элемента по очереди, пока кто-то не вернет обработчик, затем поместите их в таблицу и затем вызовите ее. Таким образом, вы создадите его точно в срок, во время выполнения. Первый запуск каждого кода операции займет немного больше времени, но впоследствии у вас будет таблица переходов.

Преимущество этого заключается в том, что он позволяет тесно связать информацию о том, что обрабатывает дочерний элемент, с фактической обработкой этого синтаксически, уменьшая накладные расходы на код и вероятность ошибки.

person Tommy    schedule 02.06.2015
comment
Это замечательно. Только один вопрос - как мне сохранить указатель функции на член класса? Или методы, обрабатываемые обработчиками инструкций, должны быть не членами/или управляющий класс должен быть другом обработчиков инструкций? - person jonnywalkerr; 03.06.2015
comment
Я немного не в курсе последних достижений С++, но, безусловно, классический подход заключается в том, что BCC и т. д. являются методами статического класса, которые принимают _cpu (или, я думаю, просто cpu) в качестве аргумента. Я предположил, что ваши дочерние классы не имеют хранилища, потому что это кажется вероятным в контексте эмуляции 6502. - person Tommy; 03.06.2015

Насколько я понимаю, вы создаете класс для каждого типа инструкций (ветвь, арифметика, загрузка, сохранение и т. д.), а затем внутри них вы пишете функции-члены для отдельных инструкций - c.f. у вас есть «CBranchInstrHandler», который обрабатывает «ветвь при переносе», «ветвь на нуле» и т. д.?

Полностью объектно-ориентированный подход заключается в расширении вашего подкласса до отдельных инструкций.

class CBranchInstrHandler { virtual void execute() = 0; };
class CBranchOnCarryClear : public CBranchInstrHandler {
    void execute() override {
        ...;
    }
};
class CBranchOnCarrySet : public CBranchInstrHandler {
    void execute() override {
        ...;
    }
};

Теперь вы можете просмотреть свои инструкции одним выстрелом, но вам потребуется сопоставление всех этих элементов один к одному.

switch (opCode) {
    case 0x90: return .. CBranchOnCarryClear .. ;
    case 0xB0: return .. CBranchOnCarrySet .. ;
}

Многоточие здесь, потому что я не уверен, как вы получаете указатель на свой CBranchInstrHandler; Я предполагаю, что они статичны и что вы не new используете их каждую инструкцию.

Если они не содержат данных, вы можете вернуть их как объекты функций по значению:

struct Base { virtual void execute() { /* noop */ } };
struct Derived { void execute(override) { ... } };

Base getHandler(opcode_t opcode) {
    if (opcode == 1) { return Derived(); }
}

но я подозреваю, что вы, вероятно, захотите взять параметры и сохранить состояние, и в этом случае возврат по значению здесь может привести к нарезке.

Конечно, если вы используете С++ 11, вы можете использовать лямбда-выражения:

switch (opCode) {
    case 0x90: return [this] () {
        ... implement BCC execute here ...
    };
    case 0xB0: return [this] () {
        ... implement BCS execute here ...
    }
}
person kfsone    schedule 01.06.2015

Вы можете использовать указатель на функцию/метод класса:

void (CBranchHandlerBase::*)();

Использование для хранения указателей на методы, которые должны быть вызваны для данного _opcode.

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

Приведенный выше код должен быть предоставлен в разделе/методе инициализации внутри вашего базового класса для обработчиков. Конечно, BCC, BCS и т. д. должны быть объявлены как чисто виртуальные методы, чтобы подход работал.

Затем вместо вашего переключателя:

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

Обратите внимание, что выполнение определено в базовом классе (и оно не обязательно должно быть виртуальным! Поскольку каждый обработчик будет иметь одинаковую функциональность метода выполнения).

Редактировать. Карта фактически может быть заменена вектором или массивом размера: 2^(8*sizeof(uint8_t)) по соображениям эффективности.

person W.F.    schedule 01.06.2015
comment
Почему map? У него нет поиска по постоянному времени, не так ли? - person Michael; 01.06.2015
comment
std::array, вероятно, будет лучше в этом сценарии. - person kfsone; 01.06.2015
comment
Но я думаю, проблема в том, что вы указываете CBranchInstHandler, в то время как у него, похоже, разные типы инструкций, каждый из которых имеет разные обработчики, поэтому ему нужно будет использовать CBranchHandlerBase, что нарушает модель... - person kfsone; 01.06.2015