{Cxx11-Signals}

First, a quick intro for for the uninitiated, signals in this context are structures that maintain a lists of callback functions with arbitrary arguments and assorted reentrant machinery to modify the callback lists and calling the callbacks. These allow customization of object behavior in response to signal emissions by the object (i.e. notifying the callbacks by means of invocations).

Over the years, I have rewritten each of GtkSignal, GSignal and Rapicorn::Signal at least once, but most of that is long a time ago, some more than a decade. With the advent of lambdas, template argument lists and std::function in C++11, it became time for me to dive into rewriting a signal system once again.

So for the task at hand, which is mainly to update the Rapicorn signal system to something that fits in nicely with C++11, I’ve settled on the most common signal system requirements:

For me, the result is pretty impressive. With C++11 a simple signal system that fullfils all of the above requirements can be implemented in less than 300 lines in a few hours, without the need to resort to any preprocessor magic, scripted code generation or libffi.

I say "simple", because over the years I’ve come to realize that many of the bells and whistles as implemented in GSignal or boost::signal2 don’t matter much in my practical day to day programming, such as the abilities to block specific signal handlers, automated tracking of signal handler argument lifetimes, emissions details, restarts, cancellations, cross-thread emissions, etc.

Beyond the simplicity that C++11 allows, it’s of course the performance that is most interesting. The old Rapicorn signal system (C++03) comes with its own set of callback wrappers named "slot" which support between 0 and 16 arguments, this is essentially mimicking std::function. The new C++11 std::function implementation in contrast is opaque to me, and supports an unlimited number of arguments, so I was especially curious to see the performance of a signal system based on it.

I wrote a simple benchmark that just measures the times for a large number of signal emissions with negligible time spent in the actual handler.

I.e. the signal handler just does a simple uint64_t addition and returns. While the scope of this benchmark is clearly very limited, it serves quite well to give an impression of the overhead associated with the emission of a signal system, which is the most common performance relevant aspect in practical use.

Without further ado, here are the results of the time spent per emission (less is better) and memory overhead for an unconnected signal (less is better):

Signal System Emit() in nanoseconds Static Overhead Dynamic Overhead

GLib GSignal

341.156931ns

0

0

Rapicorn::Signal, old

178.595930ns

64

0

boost::signal2

92.143549ns

24

400 (=265+7+8*16)

boost::signal

62.679386ns

40

392 (=296+6*16)

Simple::Signal, C++11

8.599794ns

8

0

Plain Callback

1.878826ns

-

-

Here, "Plain Callback" indicates the time spent on the actual workload, i.e. without any signal system overhead, all measured on an Intel Core i7 at 2.8GHz. Considering the workload, the performance of the C++11 Signals is probably close to ideal, I’m more than happy with its performance. I’m also severely impressed with the speed that std::function allows for, I was originally expecting it to be at least a magnitude larger.

The memory overhead gives accounts on a 64bit platform for a signal with 0 connections after its constructor has been called. The "static overhead" is what’s usually embedded in a C++ instance, the "dynamic overhead" is what the embedded signal allocates with operator new in its constructor (the size calculations correspond to effective heap usage, including malloc boundary marks).

The reason GLib’s GSignal has 0 static and 0 dynamic overhead is that it keeps track of signals and handlers in a hash table and sorted arrays, which only consume memory per (instance, signal, handler) triplet, i.e. instances without any signal handlers really have 0 overall memory impact.

Summary:

For the interested, the brief C++11 signal system implementation can be found here: simplesignal.cc
The API docs for the version that went into Rapicorn are available here: aidasignal.hh

PS: In retrospect I need to add, this day and age, the better trade-off for Glib could be one or two pointers consumed per instance and signal, if those allowed emission optimizations by a factor of 3 to 5. However, given its complexity and number of wrapping layers involved, this might be hard to accomplish.

Post comment via email

Comments:

2018-08-15 Leira Hua

Nice implementation~! I learned a lot from your code. Some one put your
code on GitHub, it is easier to read the code there.

I have a question, why did you used a handmade linked list/ring, rather
than using std::list? And why manually manage reference count, rather than
use std::shared_ptr? Is there any specific reason you made that decision?

I tried to replace the linked list with std::list, and reference count with
std::shared_ptr, in this pull request
https://github.com/larspensjo/SimpleSignal/pull/4. Surprisingly it ran with
even better performance on my Mac:

> ./test.list
Signal/Basic Tests: OK
Signal/CollectorVector: OK
Signal/CollectorUntil0: OK
Signal/CollectorWhile0: OK
Signal/Benchmark: Simple::Signal: OK
  Benchmark: Simple::Signal: 4.695005ns per emission (size=24): OK
Signal/Benchmark: callback loop: OK
  Benchmark: callback loop: 0.001000ns per round: OK

Compare to the original implementation:

> ./test.master
Signal/Basic Tests: OK
Signal/CollectorVector: OK
Signal/CollectorUntil0: OK
Signal/CollectorWhile0: OK
Signal/Benchmark: Simple::Signal: OK
  Benchmark: Simple::Signal: 22.417022ns per emission (size=8): OK
Signal/Benchmark: callback loop: OK
  Benchmark: callback loop: 0.014000ns per round: OK