WinRT’s RuntimeClass system allows a very attractive “all the methods are visible on this object you have” developer experience. Intellisense and other code-authoring helpers merge all the methods and their overloads into a single object that the developer “dots through” at runtime. While this is conceptually good, the runtime implication of dealing with multiple interfaces comes with a high binary and wall-clock-execution time cost.
Consider
the Windows.Data.Json.JsonObject
type.
It is a composite of multiple interfaces – IJsonObject
(the default),
IInspectable
(inherited), IJsonValue
, IMap<String, IJsonValue>
,
IIterable<IKeyValuePair<String, IJsonValue>>
, IIterable<IKeyValuePair<String,
IJsonValue», and IStringable. Using any method outside of JsonObject
(IJsonObject) requires a QueryInterface to the target interface. Consider this
normal-looking code:
ValueSet v;
v.Insert("Kittens", box_value(true));
v.Insert("Puppies", box_value<uint32_t>(15));
v.Insert("Hamster", box_value(L"Trundle"));
Every call to “Insert” hides a QueryInterface
from IValueSet
(a marker
interface with no other methods) to IMap<String, Object>
. ValueSet
is a
platform-provided type so there is no hope of the compiler figuring out that the
QueryInterface
calls can be merged.
An easy way to enhance the performance is a one-time conversion so that only one QueryInterface is performed:
ValueSet v;
IMap<hstring, IInspectable> m { v }; // QI from IValueSet -> IMap<K,V>
m.Insert("Kittens", box_value(true));
m.Insert("Puppies", box_value<uint32_t>(15));
m.Insert("Hamster", box_value("Trundle"));
The downside of this operation is that it makes the code look less “good” –
rather than just using ValueSet::Insert
as is projected by C++/WinRT and C#
and Rust, the developer manually inserts the conversion.
PropertySet
, ValueSet
, StringMap
, etc)IClosable
, IStringable
Related to the above “many query interfaces” issue, invoking static methods in
sequence in a method generates many individual calls to the static factory cache
for your protection. A common example is lurking in the above example for
ValueSet. Each call to box_value<>
is eventually a call to
winrt::Windows::Foundation::PropertyValue::Create...
static methods to
instantiate the reference wrapper. Each static method call looks for the statics
value in the module statics cache, acquiring it if not present. Consider this:
ValueSet v;
IMap<hstring, IInspectable> m { v };
m.Insert("Kittens", PropertyValue::CreateBoolean(true));
m.Insert("Puppies", PropertyValue::CreateUInt32(15));
m.Insert("Hamster", PropertyValue::CreateString("Trundle"));
Instead, consider manually fetching the static interface for the type once and using it repeatedly:
ValueSet v;
IMap<hstring, IInspectable> m { v };
auto s = get_activation_factory<winrt::PropertyValue, winrt::IPropertyValueStatics>();
m.Insert("Kittens", s.CreateBoolean(true));
m.Insert("Puppies", s.CreateUInt32(15));
m.Insert("Hamster", s.CreateString("Trundle"));
While this model reduces the idiomatic elegance of C++/WinRT’s projection, it greatly reduces binary footprint. Instead of each “.Create()” being a fetch of the property cache, an addref of it, a call, and a releae, it’s a single call per line with the fetch-and-release applied once in the sequence.
C++/WinRT does cache the real factory interface itself, into a global map of
class-to-factory-interface. Fetching from that cache still requires a lookup, a
lock, a potential factory-cache-miss lookup with call to
RoGetActivationFactory
, an AddRef
, then an eventual Release
of the
interface at the sequence-point.
// Wasteful
auto v1 = JsonValue::CreateString(...)
auto v2 = JsonValue::CreateObject(...)
// Less wasteful
auto jv = get_activation_factory<winrt::JsonValue, winrt::IJsonValueStatics>();
auto v1 = jv.CreateString(...);
auto v2 = jv.CreateObject(...);
This issue can often be avoided by using C++ types within a component boundary, moving “convert to value set” to the ABI boundary with another component.
Some objects have a generic property bag available hanging off them, encouraging an access pattern like (in C++/WinRT):
foo.Properties().Insert("Puppies", box_value(true));
auto k = unbox_value_or<uint32_t>(foo.Properties().TryLookup("Kittens"), 0);
foo.Properties().Insert("Kittens", box_value(k + 1));
Each .Properties()
is a vtable call, and potentially also a QueryInterface
(see above.) Instead, fetch the property set once, then operate on it:
winrt::IMap<winrt::hstring, winrt::IInspectable> p { foo.Properties() };
p.Insert("Puppies", box_value(true));
auto k = unbox_value_or<uint32_t>(p.TryLookup("Kittens"), 0);
p.Insert("Kittens", box_value(k + 1));
Similarly, a common pattern is to repeated check the property value during computation, especially during iteration of a collection of some kind. Instead, fetch the property value once and use it. WinRT design guidelines encourage properties to not “silently change” on instances unless set by the client or through a method call. Consider this pattern:
if (IsNoisyAnimal(k.Name()) {
SetSummary(k.Name() + " are noisy");
} else if (IsCuteAnimal(k.Name())) {
SetSummary(k.Name() + " are cute");
} else {
SetSummary(k.Name() + " are something else");
}
This causes many calls to IWhateverTypeKIs::Name. A worse example is iteration on a collection:
MyType FindAnotherOfMyTypeKind(MyType const& toFind) {
for (auto const& m : toFind.ChildTypes()) {
if (m.Kind() == toFind.Kind()) {
return m;
}
}
return nullptr;
}
Each pass through the loop calls toFind.Kind()
. Instead, save the value off
and use it repeatedly:
// Ex #1
auto kName = k.Name();
if (IsNoisyAnimal(kName) {
SetSummary(kName + " are noisy");
} else if (IsCuteAnimal(kName)) {
SetSummary(kName + " are cute");
} else {
SetSummary(kName + " are something else");
}
// Ex #2
MyType FindAnotherOfMyTypeKind(MyType const& toFind) {
auto toFindKind = toFind.Kind();
for (auto const& m : toFind.ChildTypes()) {
if (m.Kind() == toFindKind) {
return m;
}
}
return nullptr;
}