Абстрагирование логики IO / склейки устройства в эмуляторе ЦП

В моем эмуляторе процессора Motorola 6809 (написанном на C ++ 14) я работаю над абстракцией, чтобы представить проводку между эмулируемыми устройствами, такими как порты на 6522 VIA, или линии прерывания, соединяющие периферийное устройство с процессором.

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

Эта абстракция работает и позволяет создавать выразительные и мощные конструкции, подобные этой, которая выполняет «соединенное и» из трех линий IRQ (где одна из них имеет высокий уровень активности) и присоединяет ее к входу IRQ процессора и каждый раз, когда состояние этой строки проверяется составленные функции, генерирующие правильный результат на основе текущего состояния трех входов:

cpu.IRQ << (!fdc.DIRQ & via.IRQ & acia.IRQ);

и это, что генерирует выходной сигнал, который является четностью 8 других выходов.

OutputPin parity = out[0] ^ out[1] ^ out[2] ^ out[3] ^ out[4] ^ out[5] ^ out[6] ^ out[7];

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

Могу ли я что-нибудь сделать для повышения эффективности выполнения, или я слишком сильно надеюсь на оптимизатор компилятора?

Вот полная реализация в виде заголовка со встроенными функциями:

static inline bool default_true() {
        return true;
}

class OutputPin {

public:
        using Function = std::function<bool()>;

protected:
        Function        f = default_true;

public:
                        OutputPin() { }

                        OutputPin(const Function& f) : f(f) { }

        void            bind(const Function& _f) {
                                f = _f;
                        }

                        operator bool() const {
                                return f();
                        }

        OutputPin       operator !() const {
                                return OutputPin([&]() {
                                        return !f();
                                });
                        }
};

class InputPin {

protected:
        OutputPin       input;

public:
        void            attach(const OutputPin& _input) {
                                input = _input;
                        }

        operator        bool() const {
                                return input;
                        }
};

inline void operator<<(InputPin& in, const OutputPin& out)
{
        in.attach(out);
}

inline OutputPin operator&(const OutputPin& a, const OutputPin& b)
{
        return OutputPin([=]() {
                return (bool)a && (bool)b;
        });
}

inline OutputPin operator|(const OutputPin& a, const OutputPin& b)
{
        return OutputPin([=]() {
                return (bool)a || (bool)b;
        });
}

inline OutputPin operator^(const OutputPin& a, const OutputPin& b)
{
        return OutputPin([=]() {
                return (bool)a ^ (bool)b;
        });
}

и вот тривиальный тестовый пример для вышеупомянутого заголовка (но который генерирует 60 КБ выходных данных ассемблера!):

#include "device.h"

int main()
{
        OutputPin   a, b, c;
        InputPin    i;
    
        i << (!a & b & c);
    
        return i;
}

1 ответ
1

Накладные расходы std::function

Основная проблема в том, что вы используете std::function, и это идет с некоторыми накладными расходами. В частности, он будет выделять хранилище (используя new внутренне), поскольку ваши лямбды захватывают переменные. Поскольку выделение памяти может иметь побочные эффекты (они могут даже вызывать исключения, если память не может быть выделена), компилятор не может их оптимизировать.

Рассмотрите возможность сохранения состояния булавки как bool

Состояние выходного контакта просто true или же false. Вместо того, чтобы сохранять функцию, которая вычисляет состояние, рассмотрите возможность просто сохранения текущего состояния в bool. Мне кажется маловероятным, что постоянное обеспечение правильной установки состояния вывода менее эффективно, чем наличие функции, вычисляющей его всякий раз, когда вам нужно значение. An InputPin в таком случае не следует хранить копировать из OutputPin, но либо ссылка на OutputPin, или если вы хотите, чтобы выражение было присвоено InputPin, сохраните функцию, которая вычисляет входное значение. Как только вы это сделаете, все значительно упростится, и у компилятора не возникнет проблем с оптимизацией этого кода.

Вот пример:

#include <functional>

using OutputPin = bool;

template<typename Function>
class InputPin {
    Function f;

public:
    InputPin(const Function &f): f(f) {}

    operator bool() const {
        return f();
    }
};

int main()
{
        OutputPin   a(true), b(true), c(true);
        InputPin    i([&]{return !a & b & c;});
    
        return i;
}

  • Я понимаю, откуда вы, но это не позволяет использовать вариант в моем первом примере, где InputPin и OutputPin переменные фактически являются переменными-членами классов C ++, представляющих различные устройства микросхемы. Если бы я смог после создания, назначил InputPin функция вне реализации содержащего класса, которая, тем не менее, может работать,

    — Альнитак

  • также для дальнейшего уточнения — OutputPin переменные являются частью общедоступного интерфейса микросхемы и доступны только для чтения, но состояние самого контакта является частью его частного состояния. Представьте себе также что-то вроде 6522, где запись байта в ORB может изменить 8 выходных контактов одновременно.

    — Альнитак


  • Хм, ты еще мог бы сделать OutputPin а class с operator() чтобы получить его состояние, я думаю, а затем сделать его template как я сделал для InputPin в приведенном выше примере. Затем объявление InputPin в main() становится: InputPin i([&]{return !a() && b() && c();});. Но я думаю, что основная проблема с моим подходом заключается в том, что он не работает, если ваши устройства не подключены в DAG.

    — Г. Сон

  • Устройства не обязательно подключены в DAG — на самом деле в системе, которую я сейчас реализую, порт A VIA и матрица клавиатуры имеют ссылки, идущие в обоих направлениях. Кроме того, указание функции в конструкторе не является запуском — функция представляет собой «соединение», которое не относится к реализациям микросхемы, а относится к коду более высокого уровня, который виртуально соединяет микросхемы вместе.

    — Альнитак


Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *