-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use std::vector
in Signal
#1016
Conversation
This makes updates easier, as only one of each method will need to be changed. The other overloads just wrap the parameters into a `Delegate`, and then pass it to the main overload.
Currently we are using `std::set`, which has implicit existence checks for the `insert` and `delete` operations. However, `std::set` requires the stored items to be "less than" comparable. The custom `Delegate` class has a custom `operator<`, while `std::function` does not. If we use `std::vector`, we won't need a custom `operator<`. We will however need to do manual existence checks when adding or removing delegates from the collection. As an aside, for small collections, it's faster to iterate through a `std::vector` than other more complex collections. The benefits of using `std::set` for fast lookup only become apparent when the collection size grows. As most `Signal` objects typically have at most one connection, we'd be better off using `std::vector` for faster operations.
A `std::vector` will be implicitly initialized.
As a bit of an aside, I did try some speed testing, just out of curiosity. I played with a few parameters in a very ad hoc manner. Here's some sample test code to compare speed: namespace {
class MockHandler {
public:
MOCK_CONST_METHOD0(MockMethod, void());
MOCK_CONST_METHOD0(MockMethod2, void());
MOCK_CONST_METHOD0(MockMethod3, void());
MOCK_CONST_METHOD0(MockMethod4, void());
MOCK_CONST_METHOD0(MockMethod5, void());
MOCK_CONST_METHOD0(MockMethod6, void());
MOCK_CONST_METHOD0(MockMethod7, void());
MOCK_CONST_METHOD0(MockMethod8, void());
MOCK_CONST_METHOD0(MockMethod9, void());
MOCK_CONST_METHOD0(MockMethod10, void());
};
}
// ...
TEST(Signal, Timing) {
MockHandler handler{};
auto delegate = NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod);
for (int i = 0; i < 100000; ++i) {
NAS2D::Signal<> signal;
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod2));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod3));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod4));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod5));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod6));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod7));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod8));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod9));
// signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod10));
for (int j = 0; j < 100; ++j) {
signal.connect(delegate);
// signal.disconnect(delegate);
}
}
} Without extra connected methods (that are unique, since duplicates are ignored), the Doing a similar test with TEST(Signal, Timing) {
struct MockHandler {
void MockMethod() {}
void MockMethod2() {}
void MockMethod3() {}
void MockMethod4() {}
void MockMethod5() {}
void MockMethod6() {}
void MockMethod7() {}
void MockMethod8() {}
void MockMethod9() {}
void MockMethod10() {}
};
MockHandler handler{};
auto delegate = NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod);
for (int i = 0; i < 100000; ++i) {
NAS2D::Signal<> signal;
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod2));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod3));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod4));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod5));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod6));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod7));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod8));
signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod9));
// signal.connect(NAS2D::MakeDelegate(&handler, &MockHandler::MockMethod10));
signal.connect(delegate);
for (int j = 0; j < 100; ++j) {
signal.emit();
}
signal.disconnect(delegate);
}
} |
Additionally, deletion from, and insertion to, the end of a vector is very fast via the
This makes insertion, traversal, and deletion using a |
That's a valid point. I don't believe insertion order was preserved by We could avoid re-seating costs by overwriting the deleted element with the last entry in the Of course, the change here wasn't originally to optimize the Of course, if we don't check for duplicates in |
Reference: #1015
There are two main reasons for this change:
std::function
internally rather than the customDelegate
classUsing a
std::set
requires the stored object to be "less than" comparable. The customDelegate
class has anoperator<
. That being a bit odd aside, thestd::function
class does not haveoperator<
. That means switching tostd::function
would either require a customoperator<
function, or we could change to a different collection class that doesn't requireoperator<
for the stored elements.In terms of speed, a
std::vector
is about the fastest collection class you can likely get for small amounts of data. It's simple, and caches well, so pretty much all operations will be fast for small collections. Things likestd::set
may haveinsert
anddelete
speed advantages for larger collections, due to better asymptotic complexity, but the more complex algorithms and lower cache locality mean it'll be slower for small collections. MostSignal
objects only have a single listener. That far favours using a simpler collection, such asstd::vector
. Further, the main operation will generally beemit
, rather thanconnect
ordisconnect
. That means collection traversal speed matters more thaninsert
anddelete
speed. In that case, thestd::vector
will definitely perform better, due to simpler iteration and better cache locality.