StoreFunctions 和分配器结构包装器
本节讨论这两者,因为它们是添加两个额外类型参数 TStoreFunctions 和 TAllocator 到 TsavoriteKV 以及各种会话和 *Context(例如 BasicContext)的更改的一部分。两者的目的是通过内联调用提供更好的性能。StoreFunctions 还为存储级别而非会话级别的操作提供了更好的逻辑设计,如下所述。
从调用者的角度来看,我们在 TsavoriteKV<TKey, TValue, TStoreFunctions, TAllocator> 上有两个新的类型参数。TStoreFunctions 和 TAllocator 也存在于 *.Context(例如 BasicContext)上。C# 只允许 'using' 别名作为命名空间声明的第一行,并且该别名是文件本地的,并被后续的 'using' 别名识别,因此多个文件中的“Api”别名(例如 BasicGarnetApi)现在变得更长。
TsavoriteKV 构造函数已更改为接受 3 个参数
KVSettings<TKey, TValue>。这取代了之前一长串的参数。LogSettings、ReadCacheSettings和CheckpointSettings已成为内部类,仅由TsavoriteKV(从TsavoriteKVSettings创建)在实例化分配器时使用(例如,新的AllocatorSettings包含一个LogSettings成员)。SerializerSettings已被移除,取而代之的是IStoreFunctions上的方法。TStoreFunctions的实例。这通常通过调用静态StoreFunctions工厂方法来创建,并传入要包含的各个组件。- 用于
TAllocator实例化的工厂Func<>。
这些将在下面更详细地描述。
StoreFunctions 概述
StoreFunctions 指的是位于 TsavoriteKV 级别的回调函数集,类似于会话级别的 ISessionFunctions。类似于 ISessionFunctions,有一个 IStoreFunctions。然而,ISessionFunctions 的实现可以是结构体或类;Tsavorite 提供 SessionFunctionsBase 类作为实用工具,可以从中继承。但是,由类实现的类型参数不会生成内联代码。
因为 IStoreFunctions 旨在提供最大程度的内联,所以 Tsavorite 不提供 StoreFunctionsBase。相反,Tsavorite 提供了一个 StoreFunctions 结构体实现,并传入可选的实现,用于
- 键比较(以前作为
ITsavoriteKeyComparer接口传入,该接口未内联) - 键和值序列化器。由于类型参数的限制,这些必须作为
Func<>传入,该Func<>创建实现实例,并且由于序列化是昂贵的操作,我们保留IObjectSerializer<TKey>和IObjectSerializer<TValue>接口,而不是用键和值序列化器类型参数来混淆IStoreFunctions<TKey, TValue>接口。 - 记录处置(以前在
ISessionFunctions上作为多个方法,现在只有一个带有“原因”参数的方法)。 - 检查点完成回调(以前在
ISessionFunctions上)。
当然,由于 TsavoriteKV 具有 TStoreFunctions 类型参数,因此这可以是调用者实现的任何类型,包括类实例(这会更慢)。
分配器包装器概述
与 StoreFunctions 一样,分配器包装器旨在提供最大程度的内联。如上所述,由类实现的类型参数不会生成内联代码;JITted 代码是通用的,用于单个 IntPtr 大小的引用。然而,对于结构体,JITter 会生成特定于该特定结构体类型的代码,部分原因是大小可能不同(例如,作为参数压入堆栈时)。
有一个技巧允许由类实现的类型参数内联:泛型类型必须是包装类类型并以非泛型方式调用该类类型的结构体。这是分配器包装器采用的方法
BlittableAllocator、GenericAllocator和SpanByteAllocator类现在是包装结构体,带有Key、Value和TStoreFunctions类型参数。这些实现了IAllocator接口。- 有新的
BlittableAllocatorImpl、GenericAllocatorImpl和SpanByteAllocatorImpl类,它们像以前一样实现了大部分功能,包括继承自AllocatorBase。这些也具有Key、Value和TStoreFunctions类型参数;TAllocator不需要作为类型参数,因为它已知是XxxAllocator包装结构体。包装结构体包含XxxAllocatorImpl类的一个实例。 AllocatorBase本身现在包含一个_wrapper字段,它是一个结构体包装器实例(包含完全派生的分配器类的实例指针),该实例受IAllocator接口的约束。AllocatorBase本身是根据TStoreFunctions和TAllocator模板化的。
新的分配器定义支持两个接口
IAllocatorCallbacks,由IAllocator继承。这包含由AllocatorBase调用的派生分配器方法,我们希望将其内联而不是虚调用。结构体包装器AllocatorBase._wrapper实现了IAllocatorCallbacks,因此对_wrapper的调用内联了对IAllocatorCallbacks的调用,然后该调用会进一步调用到派生的*AllocatorImpl类实现。IAllocator : IAllocatorCallbacks。这是分配器上的所有内联调用,包括IAllocatorCallbacks。- 事实证明,不可能将
IAllocatorCallbacks作为一个单独的类型参数保留,因为IAllocator必须传播,但IAllocatorCallbacks仍然是一个单独的接口(而不是将其全部组合到IAllocator中),因为这种组织可能很有用。
- 事实证明,不可能将
仍然有许多抽象的 AllocatorBase 方法,由于整个调用的时间,方法的内联并不重要。这些主要是 IO 和扫描/迭代方法。
在 TsavoriteKV 中,我们有
hlog仍然存在,但现在是TAllocator类型(包装结构体)。hlogBase是新的;它是AllocatorBase。所有我们不需要内联(或不是虚拟的,例如 *Address)的分配器上的调用现在都在 hlogBase 上调用。- 将这些重命名为
allocator和allocatorBase可能会更清晰。
- 将这些重命名为
还有一个新的 AllocatorSettings 类,由 TsavoriteKV 在实例化分配器时创建。分配器实例化通过工厂 Func<AllocatorSettings, TStoreFunctions> 完成,而不是作为对象传入,因为
- 调用者必须知道更多内部信息,例如纪元、是否创建读取缓存等等。
- 我们创建临时的
TsavoriteKV,例如在扫描或压缩时,因此无法传入这些实例。