В моем эмуляторе процессора 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 ответ
Накладные расходы 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 и матрица клавиатуры имеют ссылки, идущие в обоих направлениях. Кроме того, указание функции в конструкторе не является запуском — функция представляет собой «соединение», которое не относится к реализациям микросхемы, а относится к коду более высокого уровня, который виртуально соединяет микросхемы вместе.
— Альнитак