跳到主要内容

StoreFunctions 和分配器结构包装器

本节讨论这两者,因为它们是添加两个额外类型参数 TStoreFunctionsTAllocatorTsavoriteKV 以及各种会话和 *Context(例如 BasicContext)的更改的一部分。两者的目的是通过内联调用提供更好的性能。StoreFunctions 还为存储级别而非会话级别的操作提供了更好的逻辑设计,如下所述。

从调用者的角度来看,我们在 TsavoriteKV<TKey, TValue, TStoreFunctions, TAllocator> 上有两个新的类型参数。TStoreFunctionsTAllocator 也存在于 *.Context(例如 BasicContext)上。C# 只允许 'using' 别名作为命名空间声明的第一行,并且该别名是文件本地的,并被后续的 'using' 别名识别,因此多个文件中的“Api”别名(例如 BasicGarnetApi)现在变得更长。

TsavoriteKV 构造函数已更改为接受 3 个参数

  • KVSettings<TKey, TValue>。这取代了之前一长串的参数。LogSettingsReadCacheSettingsCheckpointSettings 已成为内部类,仅由 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 会生成特定于该特定结构体类型的代码,部分原因是大小可能不同(例如,作为参数压入堆栈时)。

有一个技巧允许由类实现的类型参数内联:泛型类型必须是包装类类型并以非泛型方式调用该类类型的结构体。这是分配器包装器采用的方法

  • BlittableAllocatorGenericAllocatorSpanByteAllocator 类现在是包装结构体,带有 KeyValueTStoreFunctions 类型参数。这些实现了 IAllocator 接口。
  • 有新的 BlittableAllocatorImplGenericAllocatorImplSpanByteAllocatorImpl 类,它们像以前一样实现了大部分功能,包括继承自 AllocatorBase。这些也具有 KeyValueTStoreFunctions 类型参数;TAllocator 不需要作为类型参数,因为它已知是 XxxAllocator 包装结构体。包装结构体包含 XxxAllocatorImpl 类的一个实例。
  • AllocatorBase 本身现在包含一个 _wrapper 字段,它是一个结构体包装器实例(包含完全派生的分配器类的实例指针),该实例受 IAllocator 接口的约束。AllocatorBase 本身是根据 TStoreFunctionsTAllocator 模板化的。

新的分配器定义支持两个接口

  • 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 上调用。
    • 将这些重命名为 allocatorallocatorBase 可能会更清晰。

还有一个新的 AllocatorSettings 类,由 TsavoriteKV 在实例化分配器时创建。分配器实例化通过工厂 Func<AllocatorSettings, TStoreFunctions> 完成,而不是作为对象传入,因为

  • 调用者必须知道更多内部信息,例如纪元、是否创建读取缓存等等。
  • 我们创建临时的 TsavoriteKV,例如在扫描或压缩时,因此无法传入这些实例。