Property bindings in Qt 6
November 30, 2020 by Lars Knoll | Comments
Qt 6 is coming with many new features under the hood. One of the most exciting features that we have added is to bring the concept of bindings from QML and Qt Quick back into the heart of Qt and allow using it from C++.
Bindings in Qt 5
Let’s start by recapping how property bindings work in Qt 5. There, binding support was limited to Qt Quick. Here’s a very simple example:
import QtQuick 2.15
Rectangle {
height: width
border.width: height/10
}
What this does is set up two bindings on a Rectangle object. The first binding ensures that the Rectangle will always be square. The second binding sets the border width to 10% of the height. The QML engine in Qt then ensures that those relations will be kept and automatically adjusts both the height and the border width whenever the width of the Rectangle gets changed.
This mechanism of bindings is what enables writing UI definitions in Qt Quick in a mostly declarative fashion. Binding expressions (the right-hand side of a binding) can be arbitrarily complex and contain references to properties of other objects or even call other methods.
We’ve seen during the lifetime of Qt 5 that bindings make code much more expressive and remove a lot of glue code that would need to be written otherwise. So, with Qt 6, our goal was to allow using this mechanism also as a C++ developer.
Let’s have a look at how one would like to express the same relations in C++. Below is how we wanted such a Rectangle to be written as a C++ class:
class Rectangle {
public:
Property<int> width;
Property<int> height;
Property<int> border;
Rectangle() {
height.setBinding(width);
border.setBinding([this]() {
return height / 10;
});
}
};
This defines a Rectangle class with 3 properties: width, height and border. The constructor then sets up two bindings, one binding the height to the width, the other one binding the border to 10% of the height.
The question for us when we set out to do Qt 6 was whether we could implement this in an efficient and performant way.
Goals for the binding system
Apart from a good and easy to use syntax, the system needed to fulfil a few other requirements.
- Performance The system needs to be fast, there should be as little overhead as possible when evaluating bindings
- No overhead The system should not add noticeable runtime overhead when it is not being used
- Memory efficient The system should have low memory overhead
- Integrate with existing property system in QObject Qt has a pre-existing property system for QObject based classes, and the new system should integrate/extend that system
Let’s have a look at how the new system is implemented and how we achieved the goals above.
Trivial implementation
Let’s start off by looking at the most trivial way to implement a QProperty class that supports the functionality we're looking for:
template <typename T>
class QProperty
{
std::function<T()> binding = nullptr;
T data;
public:
T value() const {
if (binding) return binding();
return data;
}
void setValue(const T &newValue) {
if (binding) binding = nullptr;
data = newValue;
}
void setBinding(std::function<T> b) { binding = b; }
};
The implementation above is probably the simplest way to implement a QProperty class that supports bindings. It basically contains property data and a function pointer to a binding that can potentially be null. Whenever a binding is set on the property, the property getter will always execute the binding if one is set to retrieve the value.
This implementation has however a couple of serious drawbacks that make it unsuitable to be used as is. The most obvious one is that performance can be extremely bad, especially if bindings are depending on other properties that have bindings themselves. Evaluating those bindings every time a getter is called can cause serious performance problems. Worse, this could lead to application crashes or deadlocks in case a binding is somehow referring back to itself.
Immediate and deferred binding evaluation
So we do need a design that is a bit more advanced. There are basically two possible ways to avoid calculating the value of a binding with every setter call. Both involve caching the resulting value in data. In addition, we then also need to remember which properties a bindings depends upon.
Immediate binding evaluation is what Qt Quick does in Qt 5. It means that whenever a property is being changed, we immediately trigger a re-evaluation of all bindings that depend upon this property. The disadvantage of this system is that it can lead to unnecessary evaluations of binding expressions. An example would be a property area that is being bound to width*height. If both width and height get assigned new values, area would get calculated twice, even though only the second result would ever get used.
For Qt 6 we thus use deferred binding evaluation. This implies that we recursively flag all bindings that depend upon a property as dirty. The property getter then checks that dirty flag and if it is true, re-evaluates the binding expression, then stores the result in data and clears the dirty flag.
This is a simplified view of how QProperty now looks:
template <typename T>
class QProperty
{
T val;
QPropertyBindingData d;
public:
T value() const {
if (d.hasBinding())
d.evaluateIfDirty(this);
d.registerWithCurrenlyEvaluatingBinding();
return this->val;
}
void setValue(const T &t) {
d.removeBinding();
if (this->val == t)
return;
this->val = t;
notify();
}
};
What happens here is that the getter checks if we have a binding, and re-evaluates it if it is dirty. After that, as a second step, it registers itself with any binding that might be currently evaluating. This ensures that we know all bindings that depend upon this property. setValue() is rather similar to before. We shortcut the setter if old and new values are the same to avoid binding re-evaluations in this case. If a new value gets set, we call notify() which in turn marks all bindings depending on this property as dirty.
There are a lot more details that we needed to work through. The dependency registration uses for example thread-local storage to know about the currently evaluating binding. If you want to know all the details, have a look at the implementation of QProperty in Qt 6.
Notifications and change handlers
In addition to setting up bindings, QProperty also allows the registration of change handlers for the property. Using the onValueChanged() or subscribe() methods of QProperty, one can register a callback, that will get called whenever the underlying value of the property has changed.
The callbacks will get called when either the value of the property has been changed by calling the setter, or if the binding for the property has been marked as dirty because one of it’s dependencies has changed.
Binding support in QObjects property system
Looking back at the goals outlined above, you might have noticed, that the implementation of QProperty doesn’t solve all goals for the binding engine in Qt 6. It does perform very well (see performance numbers further below), and it only adds a small overhead when bindings aren’t being used. That overhead is mainly one check whether we have a binding and a TLS lookup for the currently evaluating binding in the getter and a fast check for dependencies in the setter.
But it does come with a non-neglectable additional 4 to 8 bytes of memory overhead per property, and it also doesn’t integrate with the existing property system in QObject. Let’s have a look at how those have been solved next.
While QProperty as it is right now can be used standalone and in any class, we wanted to have something that integrates seamlessly and in a compatible fashion with the existing property system in QObject. That system is build around QObject based properties simply having a setter and a getter as public members in the class definition. How this was backed by data was somewhat irrelevant.
To support data bindings for those properties, we needed to see how we could adjust the ideas from QProperty to fit in here.
What we ended up with was a simple extension of the public API of a QObject implementing a property.
class MyObject : public QObject
{
Q_PROPERTY(int x GET x SET setX BINDABLE bindableX)
// the line below was “int xData;” in Qt 5
Q_OBJECT_BINDABLE_PROPERTY(MyObject, int, xData)
public:
int x() { return xData; }
void setX(int x) { xData = x; }
QBindable<int> bindableX() { return &xData; }
};
The parts marked in red are new in Qt 6. As you can see there are relatively few changes required to make a property bindable in Qt 6. The simple “int xData;” for storing the data is replaced by a macro that implements the bindings logic, ie. some of the things that QProperty does as a standalone class. In addition, we added a new bindableX() method that returns a QBindable<int>, and told the meta-object system about it in the Q_PROPERTY macro.
QBindable<T> is an lightweight interface, that offers the additional functionality that is also available in QProperty. It allows setting and retrieving of bindings and to register notifications. Setting up a binding on the x property of MyObject is for example achieved by calling:
myObject->bindableX().setBinding([otherObject]() {
return otherObject->x() + otherObject->width();
}
Using those macros and the fact that we know this is being used in QObject has a couple of advantages. Unlike QProperty, Q_OBJECT_BINDABLE_PROPERTY does not add any memory overhead. The size of the the object implemented by the macro is the same as the size of the data being stored. This is achieved by moving the binding data into a common data structure (allocated on demand) for the whole QObject instance.
It makes looking up a binding slightly slower, but we on the other hand can avoid the TLS lookup for the currently executing binding because we have that on-demand data structure in QObject. This also means that we can reduce our runtime overhead when bindings aren’t being used to a pointer lookup and comparison for both the setter and the getter.
Let’s have a quick look at how this is implemented. To allow using bindings in QObject properties, the Q_OBJECT_BINDABLE_PROPERTY macro above exands to two things. First, it defines a static member function inside the object:
static constexpr size_t _qt_property_cData_offset()
{
return offsetof(MyObject, xData);
}
This method allows is then being used as a template parameter to the QObjectBindableProperty instance defined in the next line:
QObjectBindableProperty<MyObject, int, MyObject::_qt_property_cData_offset> xData;
What this achieves is that we now have a method to calculate the this pointer of the QObject that owns the property data from the this pointer of the property data. This is something we in turn use to retrieve a QBindingStorage pointer from QObject. That pointer could be null, in which case we have the fast path where no bindings are being used with this object. Otherwise, we lookup the QPropertyBindingData that QProperty has built-in in the QBindingStorage. Once we have retrieved a pointer to a valid the binding data, QObjectBindableProperty basically does the same operations as QProperty.
Backwards compatibility
Properties that have been implemented as in Qt 5 using a changedSignal() as notification will continue to work as before. This means, that they can be used together with bindings inside Qt Quick, but not from C++. They will however also continue to use immediate binding evaluation.
To get the full benefit of the new system, you should consider adding the binding support to your own properties. This will make them bindable from C++ and will start using deferred binding evaluation in most cases. Adding binding support to an existing property of a QObject is 100% backwards compatible.
Most properties in Qt 6 itself still have not been ported to support the new binding engine neither. We are planning for this to happen for Qt 6.1 and 6.2.
Benchmark data
Let’s first have a look at the performance of property reads and writes when bindings are not being used. This is important, since we do not want larger regressions in existing code. To test, we look at an integer property. This tests the worst case, as reading and writing an integer is as fast as it can get, and the results will thus show any added overhead most clearly.
Read | Write | |
Old style property | 3,8ns | 7,2ns |
QObjectBindableProperty (no notification) | 4,3ns | 4,5ns |
QObjectBindableProperty (with changed signal) | 4,3ns | 8,2ns |
QProperty | 9,1ns | 5,4ns |
The table shows results, testing a couple of cases. The first is a property implemented the Qt 5 way, with a getter, setter and a changed signal. The next two lines use Q_OBJECT_BINDABLE_PROPERTY to make the property bindable. In one case, we did not add a Qt 5 style changed signal (as the new system doesn’t rely upon them), the other case still emits a changed signal for backwards compatibility. The final line shows how QProperty performs.
As you can see, we are around 10% slower for the getter (but note that the getter for the old-style property expands to a function call that contains three instructions). The setters are 40% faster for the most common case where the property doesn't have a changed signal. QProperty is slightly slower, as it needs to do a TLS lookup.
For QString based properties the difference would be much less pronounced, so we can conclude that we managed to add support for bindings without significant overhead in the case where they aren’t being used.
Lets now have a look at how bindings are performing. To do that we use a simple direct binding of one integer property to another one. We have two test cases, one where we continuously set the first property and then read the value of the second. In a second case, we only write to the first property, but never read the second one. Each of those cases, we split up into two sub cases, one where we read and write the value through QObjects generic property interface (setProperty() and property()) and one where we use the C++ setter and getter.
We then run those test cases for both old style properties as well as the new properties with direct binding support.
Let's start with a binding that is defined in QML and evaluated as in Qt 5.
Access method | write & read | write only | write & read | write only |
setProperty/property | setter/getter | |||
Old style property | 370ns | 240ns | 130ns | 130ns |
QObjectBindableProperty (no notification) | 370ns | 110ns | 120ns | 14ns |
QObjectBindableProperty (with changed signal) | 410ns | 120ns | 140ns | 25ns |
QProperty | 440ns | 130ns | 130ns | 10ns |
While QML in Qt 5 has some shortcuts for a few selected properties, some properties might end up going through the generic property interface of QObject. The numbers in the first line of the table reflect the worst and best case performance we can get in Qt 5.15.
The other lines show the performance we can get in Qt 6. What you can see is that the case where each write is followed by a read take about the same time as in Qt 5. This is expected, as we need to do about the same amount of work. But in all cases, where there are several writes, before we need the value of the property again, the new system beats the old one by a good margin.
Let's have a look at what happens when we setup the binding in C++. As this is not possible with the old property system, we emulate it there by connecting a lambda to the changed signal that sets the new value. It should be noted that this is however no replacement for bindings, as it simply doesn't scale to more complex binding expressions and would require a huge amount of manual setup to catch all dependencies.
Access method | write & read | write only | write & read | write only |
setProperty/property | setter/getter | |||
Old style property | 230ns | 120ns | 29ns | 30ns |
QObjectBindableProperty (no notification) | 250ns | 100ns | 35ns | 12ns |
QObjectBindableProperty (with changed signal) | 280ns | 120ns | 51ns | 22ns |
QProperty | 300ns | 120ns | 48ns | 9ns |
The two leftmost columns are mainly for reference and to compare with the table above. In C++, one would almost never access the properties through the generic, string based property API. Instead, the two rightmost columns reflects typical usage in C++.
What can be seen is that the binding system performs pretty much as good as a direct signal/slot connection between two old style properties. Given that it is much more flexible and automatically captures all dependencies (that would need to be manually declared using signals/slots), these are great numbers.
You can also see that a C++ based binding using setters and getters is around 3-10 times faster than a binding defined in QML in Qt 5.15. Moving forward, we plan to make use of that fact by exploring ways to compile binding expressions defined in QML to C++ and then to assembly.
Conclusions
The binding engine in Qt 5 was what made Qt Quick so successful. With Qt 6, we have now moved that engine from Qt Quick all the way down into the heart of Qt and made it available for C++ developers as well.
While doing so, we managed to implement significant performance improvements over what we have in Qt 5, while avoiding overhead for those users that do not use the engine.
We are nevertheless not done with our work, as most of the properties in our own libraries still need to be ported over to the new system.
Blog Topics:
Comments
Subscribe to our newsletter
Subscribe Newsletter
Try Qt 6.8 Now!
Download the latest release here: www.qt.io/download.
Qt 6.8 release focuses on technology trends like spatial computing & XR, complex data visualization in 2D & 3D, and ARM-based development for desktop.
We're Hiring
Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.