StoreFunctions and Allocator Struct Wrapper
This section discusses both of these because they were part of a change to add two additional type args, TStoreFunctions and TAllocator, to TsavoriteKV as well as the various sessions and *Context (e.g. BasicContext). The purpose of both of these is to provide better performance by inlining calls. StoreFunctions also provides better logical design for the location of the operations that are store-level rather than session-level, as described below.
From the caller point of view, we have two new type parameters on TsavoriteKV<TKey, TValue, TStoreFunctions, TAllocator>. The TStoreFunctions and TAllocator are also on *.Context (e.g. BasicContext) as well. C# allows the 'using' alias only as the first lines of a namespace declaration, and the alias is file-local and recognized by subsequent 'using' aliases, so the "Api" aliases such as BasicGarnetApi in multiple files are much longer now.
TsavoriteKV constructor has been changed to take 3 parameters:
KVSettings<TKey, TValue>. This replaces the previous long list of parameters.LogSettings,ReadCacheSettings, andCheckpointSettingshave become internal classes, used only byTsavoriteKV(created fromTsavoriteKVSettings) when instantiating the Allocators (e.g. the newAllocatorSettingshas aLogSettingsmember).SerializerSettingshas been removed in favor of methods onIStoreFunctions.- An instance of
TStoreFunctions. This is usually obtained by a call to a staticStoreFunctionsfactory method to create it, passing the individual components to be contained. - A factory
Func<>for theTAllocatorinstantiation.
These are described in more detail below.
StoreFunctions overview
StoreFunctions refers to the set of callback functions that reside at the TsavoriteKV level, analogous to ISessionFunctions at the session level. Similar to ISessionFunctions, there is an IStoreFunctions. However, the ISessionFunctions implementation can be either a struct or a class' Tsavorite provides the SessionFunctionsBase class, which may be inherited from, as a utility. Type parameters implemented by classes, however, do not generate inlined code.
Because IStoreFunctions is intended to provide maximum inlining, Tsavorite does not provide a StoreFunctionsBase. Instead, Tsavorite provides a StoreFunctions struct implementation, with optional implementations passed in, for:
- Key Comparison (previously passed as an
ITsavoriteKeyComparerinterface, which is not inlined) - Key and Value Serializers. Due to limitations on type arguments, these must be passed as
Func<>which creates the implementation instance, and because serialization is an expensive operation, we stay with theIObjectSerializer<TKey>andIObjectSerializer<TValue>interfaces rather than clutter theIStoreFunctions<TKey, TValue>interface with the Key and Value Serializer type args. - Record disposal (previously on
ISessionFunctionsas multiple methods, and now only a single method with a "reason" parameter). - Checkpoint completion callback (previously on
ISessionFunctions).
Of course, because TsavoriteKV has the TStoreFunctions type parameter, this can be any type implemented by the caller, including a class instance (which would be slower).
Allocator Wrapper overview
As with StoreFunctions, the Allocator Wrapper is intended to provide maximal inlining. As noted above, type parameters implemented by classes do not generate inlined code; the JITted code is general, for a single IntPtr-sized reference. For structs, however, the JITter generates code specific to that specific struct type, in part because the size can vary (e.g. when pushed on the stack as a parameter).
There is a hack that allows a type parameter implemented by a class to be inlined: the generic type must be for a struct that wraps the class type and makes calls on that class type in a non-generic way. This is the approach the Allocator Wrapper takes:
- The
BlittableAllocator,GenericAllocator, andSpanByteAllocatorclasses are now the wrapper structs, withKey,Value, andTStoreFunctionstype args. These implement theIAllocatorinterface. - There are new
BlittableAllocatorImpl,GenericAllocatorImpl, andSpanByteAllocatorImplclasses that implement most of the functionality as previously, including inheriting fromAllocatorBase. These also haveKey,Value, andTStoreFunctionstype args; theTAllocatoris not needed as a type arg because it is known to be theXxxAllocatorWrapper struct. The wrapper structs contain an instance of theXxxAllocatorImplclass. AllocatorBaseitself now contains a_wrapperfield that is a struct-wrapper instance (which contains the instance pointer of the fully-derived allocator class) that is constrained to theIAllocatorinterface.AllocatorBaseitself is templated onTStoreFunctionsandTAllocator.
The new Allocator definition supports two interfaces:
IAllocatorCallbacks, which is inherited byIAllocator. This contains the derived-Allocator methods called byAllocatorBasethat we want to inline rather then virtcall. The struct wrapperAllocatorBase._wrapperimplementsIAllocatorCallbacks, so the call on_wrapperinlines the call toIAllocatorCallbacks, which then calls down to the derived*AllocatorImplclass implementation.IAllocator : IAllocatorCallbacks. This is all inlined calls on the Allocator, includingIAllocatorCallbacks.- It turns out not to be possible to keep
IAllocatorCalbacksas a separate type arg becauseIAllocatormust propagate, butIAllocatorCallbacksremains as a separate interface (instead of combining it all intoIAllocator) as the organization may be useful.
- It turns out not to be possible to keep
There are still a number of abstract AllocatorBase methods, for which inlining of the method call is not important due to the time for the overall call. These are mostly IO and Scan/Iteration methods.
Within TsavoriteKV, we have:
hlogremains, but is now of typeTAllocator(the wrapper struct).hlogBaseis new; it is theAllocatorBase. All the calls on the allocator that we don’t need to inline (or are not virtual, such as *Address) are now called on hlogBase.- It might be cleaner to rename these to
allocatorandallocatorBase.
- It might be cleaner to rename these to
There is a new AllocatorSettings class as well that is created by TsavoriteKV when instantiating the allocator. Allocator instantiation is done by a factory Func<AllocatorSettings, TStoreFunctions> rather that being passed in as an object, because:
- The caller would have to know more internal stuff such as the epoch, whether to create readcache, and so on.
- We create temporary
TsavoriteKVs, such as when Scanning or Compacting, so there is no way to pass these instances in.