避免分配但不允许值类型的默认零值
Avoiding allocations but without allowing default zero values for value types
C# 中的值类型不能有无参数构造函数,因为在创建不带参数的实例时 CLR 的默认行为是将所有位清零。
假设我有一个用例,我希望有一个类型在基础值上强制执行一些不变量,而这些不变量不允许默认的全零状态。这迫使我使用引用类型。现在假设我赚了很多,我的意思是很多这种类型的实例。性能至关重要,这些分配加起来会给 GC 带来很大压力。我非常想避免这些分配,首先想到的是使用值类型。但是,唉,我不能,因为默认值是无效的。
假设该类型满足您希望从值类型中获得的所有其他条件,即它确实表示单个值,具有值语义,最多占用 16 位。唯一的问题是全零值是这种类型的无效状态。
我将如何实现我的性能目标,这些目标在概述的场景中受到 GC 压力的可验证约束,同时又不牺牲我的类型约定的不变量?
编辑:
一个非常简单的例子,假设我持有一个 64 位的序列以及第一个非零位的索引。
public <struct/class> BitSequence64
{
private long _bits;
private int _firstNonZero;
public IEnumerable<byte> Bytes => ...
// A bunch of helper properties.
public BitSequence64(long value)
{
// Set the _firstNonZero, etc.
...
}
// Methods that allow you to twiddle the bits but maintaining
// the invariant of always having at least one non-zero.
}
所以显然将 _bits
设置为全零是没有意义的,特别是因为 _firstNonZero
然后会指向第一位,这不是非零。有很多这些单独的序列,我非常希望这种类型的依赖者能够安全地使用它,而无需在每次传递给面向 [=30] 的 public 时验证它不是 default
值=].
我成功使用的一项技术是改变内部状态的解释方式,以便当零无效或non-zero 需要默认值。
具体如何实现取决于类型的内部状态。一般来说,至少有一个成员字段会存储一个与消费者看到的不一样的值。相反,类型的 public 接口将解释进入和离开类型的差异。
了解内部状态中类型的自然属性(数学或其他)可以帮助您确定如何做到这一点。
首先要做的是 select 类型的 合理默认值 。很明显,自然的default(T)
已经被判定为不合理了,所以还需要select编一些别的东西。例如,这可能是有效范围内的最小值。不过,无论它是什么,它都会告知输入在存储之前需要如何调整,以及内部值在 returning 之前需要如何调整(通过逆运算)。
入门示例
此技术的一个非常人为和基本的示例是以下 Year
包装器类型。
Caution: DO NOT use this example as-is; it's for demonstration purposes only.
public readonly struct Year
{
private const int Delta = 2000;
private readonly int _value;
public Year(int value)
{
_value = value - Delta;
}
public int Value => _value + Delta;
}
这里默认是Delta
常量,用来调整内部状态。在 default(Year)
中,_value
将是 0
,但 Value
属性 将是 return 2000
。类似地,new Year(2000)
将在输入时将 2000
转换为 0
,并在输出时转换回 2000
。另一种思考方式是 _value
表示与默认值的偏移量。
当您围绕这个内部表示构建功能时,重要的是要记住只有构造函数和 Value
属性 应该访问支持字段。其他一切,甚至是私有成员,都应该使用 Value
属性 来确保一致性。同样,创建新实例应该使用构造函数并传递 consumer-facing 值。在其他任何地方使用支持字段会引起潜在的错误,因此最好避免这样做。单元测试对于确保一致性至关重要。
题型
BitSequence64
的情况由于那个特定的不变量而有点诡异。在评论中,它看起来像默认值 1
—— 只设置了位 0 —— 可能是该类型的合理默认值。从现在开始,我会假设它是。
这可以通过将实际值与 1
进行异或来实现。这非常好,因为与 1 的异或运算是它自己的逆运算。
现在,default(BitSequence64)
是有效的,因为它代表的值与同样有效的 new BitSequence64(1L)
所代表的值相同。
public struct BitSequence64
{
private const long DefaultBit = 1L;
private long _value;
private int _firstNonZero;
public BitSequence64(long value)
{
if (value == 0)
throw new ArgumentException("At least one bit must be set.", nameof(value));
_value = value ^ DefaultBit;
_firstNonZero = GetFirstNonZero(_value);
}
public long Value => _value ^ DefaultBit;
public int FirstNonZero => _firstNonZero;
// Note that this property uses the post-adjustment, consumer-facing value.
public IEnumerable<byte> Bytes => BitConverter.GetBytes(Value);
private static int GetFirstNonZero(long value)
{
// TODO: Incorporate your implementation here.
throw new NotImplementedException();
}
// And, of course, let's not forget the members that do bit-twiddling
// while maintaining the invariants.
// ...
}
默认值 1
很方便,但是如果您需要默认值 0x8000_0000_0000_0000
(MSB 设置)怎么办? _firstNonZero
默认为零时将不再正确。
在内部状态下很容易解释这一点。我们可以重新定义 _firstNonZero
为距第 31 位“向下”的距离,而不是距第 0 位“向上”的距离。除了通过最高有效位对值进行异或运算之外,修改构造函数和 FirstNonZero
属性 以对 _firstNonZero
.
执行转换
C# 中的值类型不能有无参数构造函数,因为在创建不带参数的实例时 CLR 的默认行为是将所有位清零。
假设我有一个用例,我希望有一个类型在基础值上强制执行一些不变量,而这些不变量不允许默认的全零状态。这迫使我使用引用类型。现在假设我赚了很多,我的意思是很多这种类型的实例。性能至关重要,这些分配加起来会给 GC 带来很大压力。我非常想避免这些分配,首先想到的是使用值类型。但是,唉,我不能,因为默认值是无效的。
假设该类型满足您希望从值类型中获得的所有其他条件,即它确实表示单个值,具有值语义,最多占用 16 位。唯一的问题是全零值是这种类型的无效状态。
我将如何实现我的性能目标,这些目标在概述的场景中受到 GC 压力的可验证约束,同时又不牺牲我的类型约定的不变量?
编辑:
一个非常简单的例子,假设我持有一个 64 位的序列以及第一个非零位的索引。
public <struct/class> BitSequence64
{
private long _bits;
private int _firstNonZero;
public IEnumerable<byte> Bytes => ...
// A bunch of helper properties.
public BitSequence64(long value)
{
// Set the _firstNonZero, etc.
...
}
// Methods that allow you to twiddle the bits but maintaining
// the invariant of always having at least one non-zero.
}
所以显然将 _bits
设置为全零是没有意义的,特别是因为 _firstNonZero
然后会指向第一位,这不是非零。有很多这些单独的序列,我非常希望这种类型的依赖者能够安全地使用它,而无需在每次传递给面向 [=30] 的 public 时验证它不是 default
值=].
我成功使用的一项技术是改变内部状态的解释方式,以便当零无效或non-zero 需要默认值。
具体如何实现取决于类型的内部状态。一般来说,至少有一个成员字段会存储一个与消费者看到的不一样的值。相反,类型的 public 接口将解释进入和离开类型的差异。
了解内部状态中类型的自然属性(数学或其他)可以帮助您确定如何做到这一点。
首先要做的是 select 类型的 合理默认值 。很明显,自然的default(T)
已经被判定为不合理了,所以还需要select编一些别的东西。例如,这可能是有效范围内的最小值。不过,无论它是什么,它都会告知输入在存储之前需要如何调整,以及内部值在 returning 之前需要如何调整(通过逆运算)。
入门示例
此技术的一个非常人为和基本的示例是以下 Year
包装器类型。
Caution: DO NOT use this example as-is; it's for demonstration purposes only.
public readonly struct Year
{
private const int Delta = 2000;
private readonly int _value;
public Year(int value)
{
_value = value - Delta;
}
public int Value => _value + Delta;
}
这里默认是Delta
常量,用来调整内部状态。在 default(Year)
中,_value
将是 0
,但 Value
属性 将是 return 2000
。类似地,new Year(2000)
将在输入时将 2000
转换为 0
,并在输出时转换回 2000
。另一种思考方式是 _value
表示与默认值的偏移量。
当您围绕这个内部表示构建功能时,重要的是要记住只有构造函数和 Value
属性 应该访问支持字段。其他一切,甚至是私有成员,都应该使用 Value
属性 来确保一致性。同样,创建新实例应该使用构造函数并传递 consumer-facing 值。在其他任何地方使用支持字段会引起潜在的错误,因此最好避免这样做。单元测试对于确保一致性至关重要。
题型
BitSequence64
的情况由于那个特定的不变量而有点诡异。在评论中,它看起来像默认值 1
—— 只设置了位 0 —— 可能是该类型的合理默认值。从现在开始,我会假设它是。
这可以通过将实际值与 1
进行异或来实现。这非常好,因为与 1 的异或运算是它自己的逆运算。
现在,default(BitSequence64)
是有效的,因为它代表的值与同样有效的 new BitSequence64(1L)
所代表的值相同。
public struct BitSequence64
{
private const long DefaultBit = 1L;
private long _value;
private int _firstNonZero;
public BitSequence64(long value)
{
if (value == 0)
throw new ArgumentException("At least one bit must be set.", nameof(value));
_value = value ^ DefaultBit;
_firstNonZero = GetFirstNonZero(_value);
}
public long Value => _value ^ DefaultBit;
public int FirstNonZero => _firstNonZero;
// Note that this property uses the post-adjustment, consumer-facing value.
public IEnumerable<byte> Bytes => BitConverter.GetBytes(Value);
private static int GetFirstNonZero(long value)
{
// TODO: Incorporate your implementation here.
throw new NotImplementedException();
}
// And, of course, let's not forget the members that do bit-twiddling
// while maintaining the invariants.
// ...
}
默认值 1
很方便,但是如果您需要默认值 0x8000_0000_0000_0000
(MSB 设置)怎么办? _firstNonZero
默认为零时将不再正确。
在内部状态下很容易解释这一点。我们可以重新定义 _firstNonZero
为距第 31 位“向下”的距离,而不是距第 0 位“向上”的距离。除了通过最高有效位对值进行异或运算之外,修改构造函数和 FirstNonZero
属性 以对 _firstNonZero
.