加速 .NET 类型






4.76/5 (19投票s)
密集的“大数据”处理和移动应用需要快速计算和紧凑的数据存储。设计新的快速、安全的 .NET 类型且开销小并非易事。本文描述了如何创建这种零开销、只带优点的类型。
- 下载示例 - 7.6 KB (请参见下面的“使用代码”章节)
引言
大型网络在线地图项目 www.GoMap.Az 的开发需要密集处理网络请求和地理数据。为了简化和加速计算,作者决定开发专门的数据类型。其中一种需要改进的代码是名为 SmartInt
的新型整型,它等效于 int
。本文简要解释了这种类型的设计和实现。许多其他类型也可以以类似的方式创建。
本文描述了
- 创建具有特定含义且在大小和速度上都没有缺点的全新类型
- 简化和丰富标准类型的使用
- 创建类似于
Nullable
类型但没有大小开销的结构 - 用于创建高可靠性代码的单元测试
- 性能测量
- 研究 .NET Framework 开源代码的好处
- 包含企业级代码的开源项目
本文不应被视为对 .NET 组件的改进或替代,也不应试图找出标准 .NET 的弱点或缺点。.NET 是一个为通用使用而开发的全球性框架。这意味着它紧凑,具有合理数量的函数/方法,对于许多用例来说足够快且足够消耗资源。SSTypes
反过来专门用于几种情况(主要目的是 string
解析和输入清理)。这种使用限制允许对其进行性能优化。SSTypesLT
项目正在开发中,我希望发布的信息会有用。除此之外,它还可以作为创建自定义类型的示例。
问题
密集的数据转换和计算需要快速易用的代码。所需的改进包括
- 加速从文本
string
到数值的转换是主要的改进要求。新的Parse
方法应快速地将输入值转换为SmartInt
且不抛出异常。 SmartInt
还应包含一个标志,表示存储NoValue
或BadValue
。换句话说,提供类似于Nullable<int>
的功能。- 新类型应在
System.Int32
(下文中为Int32
)适用的任何地方工作——在计算中、作为数组索引、作为结构和类中的成员。
零成本改进
改进的主要思想基于这样一个事实:从另一个类派生的类保留了父类的特性并对其进行了专门化。这意味着如果未添加新的成员或方法,它只会限制功能。因此,存在加速现有功能并保持占用空间不变的能力。
另一个事实是现代编译器优化代码的能力。这使我们能够添加不会给最终本地代码带来额外负担的语义。
C# 中主要有两类类型——引用类型和值类型。所有类都是引用类型。在类基础上创建新类型会增加面向对象编程的支持,这是一个不必要的负担。值类型不会增加这些额外的东西。
Int32
的源代码是绝佳的起点,并且现在可以获取(例如,通过 Google 搜索“System.Int32 source code”)。这些代码展示了 COM 世界中称为“包含”(如旧书《Programming Visual C++》,Microsoft Press,1998 中所述)的关系的使用。包含是二进制 COM 对象实现继承的一种方式。这种技术是一种继承父类特性并创建新类型的方法,该新类型可以在父类型使用的任何地方使用(请参阅下面的“非常规 OOP”章节)。让我们详细考虑这种技术。
创建 SmartInt 结构
SmartInt
结构的最简单代码如下
public struct SmartInt
{
private System.Int32 m_v;
public static readonly SmartInt BadValue = System.Int32.MinValue;
public static readonly SmartInt MaxValue = System.Int32.MaxValue;
public static readonly SmartInt MinValue = System.Int32.MinValue + 1;
// Constructs from int value
public SmartInt(System.Int32 value)
{
m_v = value;
}
// Constructs from int? value
public SmartInt(System.Int32? value)
{
if (value.HasValue)
m_v = value.Value;
else
m_v = BadValue.m_v;
}
// Checks validity of the value
public bool isBad()
{
return m_v == BadValue.m_v;
}
public static implicit operator SmartInt(System.Int32 value)
{
return new SmartInt(value);
}
public static implicit operator System.Int32(SmartInt value)
{
return value.m_v;
}
}
SmartInt 的完整代码可在 https://github.com/dgakh/SSTypes/blob/master/SSTypesLT/SSTypesLT/Native/SmartInt.cs 获取。
代码示例的特点包括
m_v
包含在SmartInt
中,形成了“Containment
”关系。- 构造函数允许从
int
和int?
(Nullable<int>
) 值构建 - 隐式运算符
SmartInt(System.Int32 value)
允许从System.Int32
到SmartInt
的隐式转换。 - 隐式运算符
System.Int32(SmartInt value)
允许从SmartInt
到System.Int32
的隐式转换。
结果是,以下代码可以编译和运行
SmartInt a = 35;
int b = a;
int c = b * 2;
a = c;
a = 3;
string h = "Hello, World !";
char ch = h[a]; // Use SmartInt as an array index
JIT 将以 SmartInt
完全类似于 int
的方式优化此代码。未观察到性能损失或大小开销(.NET Framework 4.0 及更高版本的测试证实了这一点)。
非常规面向对象编程
从现有 Int32
生成新结构 SmartInt
可以从 OOP 的角度来考虑。让我们回顾一下主要特性。
封装
SmartInt
封装了字段和方法,包括 public
/protected
级别的访问。与类的封装没有区别。
继承
SmartInt
具有其父类型 Int32
的特性。您可以看到重载的函数,例如 ToString
、Equals
、GetHashCode
和公开的 Int32
函数,例如 GetType
、格式化的 ToString
。
多态
多态性以值级别呈现,SmartInt
可以在其父类型 Int32
使用的任何地方使用,例如
int a = SmartInt.Parse(“38405”); // Parsed SmartInt value is assigned to int
SmartInt ind = 7;
SmartInt len = 5;
String hw = "Hello, World !";
Console.WriteLine(hw[ind]); // SmartInt value is used as index
String ww = hw.Substring(ind, len); // SmartInt values are used as function’s arguments
抽象
SmartInt
降低了 Int32
的抽象级别,但不够大。下面“语义”一章中提到的类型 Age
显示了更深层次的专门化和抽象的降低。
语义
基于 Int32
创建另一个结构可以引入具有不同含义的类型。
public struct Age
{
private System.Int32 m_v;
public static readonly SmartInt MaxValue = 150;
public static readonly SmartInt MinValue = 0;
public static implicit operator Age(System.Int32 value)
{
return new SmartInt(value);
}
public static implicit operator System.Int32(Age value)
{
return value.m_v;
}
}
尽管 SmartInt
和 Age
类型在二进制上相似(因为它们基于相同的 Int32
类型),但在 C# 语言级别,它们是不兼容的。
以下代码将无法编译
Age a = 7; // Ok – int can be converted to Age
// Compilation error – no operator for conversion from Age to SmartInt
SmartInt s1 = a;
// Ok – value will be assigned through conversion Age->int and int->SmartInt
SmartInt s2 = (int)a;
对于所有编译后的代码,JIT 优化将消除负担。
可空类型
在需要使用 NoValue
的情况下,Nullable
类型使用起来非常方便。但使值类型 Nullable
会增加其大小,因为它需要额外的存储空间来表示状态。通常,此额外空间的大小等于对齐方式(许多平台为 32 位)。这使得 Int32?
的总大小是 Int32
的两倍。
这种大小增加有两个缺点
- 增加了大小,尤其是对于数组
- 使缓存命中率变差
- 影响垃圾回收器(在寿命更长的代中分配)
SmartInt
具有类似于 Nullable<Int32>
的特性,但保持其原始大小等于 Int32
的大小。换句话说,Nullable<Int32>
的功能被添加到 Int32
中,而没有任何负担。
向 SmartInt
添加以下方法使其与 Int32?
(Nullable<Int32>
) 兼容
//Converts the value of System.Int32? to SmartInt.
public static implicit operator SmartInt(System.Int32? value)
{
if (!value.HasValue)
return SmartInt.BadValue;
return new SmartInt(value.Value);
}
// Converts the value of SmartInt to System.Int32?.
public static implicit operator System.Int32?(SmartInt value)
{
if (value.isBad())
return null;
return new System.Int32?(value.m_v);
}
public bool HasValue
{
get { return !isBad(); }
}
public SmartInt Value
{
get
{
if (!HasValue)
throw new System.InvalidOperationException("Must have a value.");
else
return this;
}
}
public SmartInt GetValueOrDefault()
{
return HasValue ? this : default(SmartInt);
}
这些改进允许在许多情况下将 SmartInt
类似于 Int32?
使用。例如
// Assign null to structure
SmartInt si = null;
// Checks if structure is null
if (si == null)
return 0;
不幸的是,以下 C# 代码将不会编译
SmartInt x = null;int y = x ?? -1;
但稍微修改后的代码将成功编译
SmartInt x = null;
int y = (int?)x ?? -1;
Parse() 方法
Parse
是 SmartInt
中改进最大的方法。改进包括专门用于仅解析 Int32
值类型并进行输入清理。输入清理允许许多输入数据类型和值范围,其中无法解析的值不会抛出异常,而是将输出值设置为 BadValue
。虽然测试显示性能显著提高,但该方法的开发、测试和文档编写仍在继续。
- 与标准
Int32.Parse
相比,它显示出 4 倍的性能提升(请参阅下面的“性能测试”章节) - 它可以解析
string
的子字符串,而无需将其拆分。此功能进一步提高了性能 Parse
有不同的重载,接受不同类型的数据- 它不抛出异常
ToString() 方法
ToString
方法经过改进,可将输出提供给 StringBuilder
。这种技术在组合复杂的文本结构(如 XML 或 JSON)时不会创建临时 string
。
用于输入清理
SmartInt
在输入清理中很有用。例如,提取 Web 请求值可以用三行代码编写
// Try parse value for parameter "id" without throwing
// Context.Request is not null, but QueryString can return any value
SmartInt id = SmartInt.Parse(context.Request.QueryString["id"]);
// Return if value did not extracted or less than 0
if (id.isBadOrNegative())
return;
另一个方法 SmartInt.isBad()
可用于检查值是否不好(是负数、0 或正数)。
抛出异常会显著降低性能和抵御 DDOS 攻击的能力。SmartInt.Parse
不抛出异常,并快速解析多种类型的输入值。
单元测试
单元测试是 SDLC 的重要组成部分,用于确保更改代码的正确行为。SmartInt
是一种小而简单的类型,在使用前应尽可能密集地进行测试。此处描述的类型经过了密集测试,其正确性已在生产环境中得到证实。
例如,其中一个测试用例是使用 SmartInt.Parse
和 Int32.Parse
暴力循环解析随机 string
值,并比较输出。以下过程测试常见值、负值和带正号的值
[TestMethod]
public void Test_SmartInt_Parse_BruteForce()
{
int test_count = 10000000;
Random rnd = new Random();
for (int i = 0; i < test_count; i++)
{
int v = rnd.Next();
string s = v.ToString();
string sp = "+" + s;
string sn = "-" + s;
SmartInt siv = SmartInt.Parse(s);
SmartInt sipv = SmartInt.Parse(sp);
SmartInt sinv = SmartInt.Parse(sn);
Assert.IsTrue( (siv == v) && (sipv == v)
&& (sinv == -v), "Parsing " + v.ToString());
}
}
还有其他测试可确保代码质量。作者增加了测试数量以确保代码的可靠性。
性能测试
性能测试通过使用 BenchmarkDotNet(https://github.com/PerfDotNet)的技术实现。测试显示性能提升超过 4 倍。开发仍在进行中,以确保代码无错误。
测试环境
BenchmarkDotNet=v0.9.1.0
OS=Microsoft Windows NT 6.1.7601 Service Pack 1
Processor=Intel(R) Core(TM) i7-3610QM CPU @ 2.30GHz, ProcessorCount=4
Frequency=2241298 ticks, Resolution=446.1700 ns
HostCLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]
Type=SmartIntBM_Parse_IntSmartInt_9_Digit Mode=吞吐量
-----------------------------------------------------------------------------------------------
方法 中位数 标准差
Parse_Int_9_Digit 16.1033 ms 0.3239 ms
Parse_SmartInt_9_Digit 3.4312 ms 0.0584 ms
-----------------------------------------------------------------------------------------------
还有许多其他观察到的性能改进。对于 .NET Framework 的现代版本,观察到积极的差异。不应使用 4.0 之前的版本,因为这些版本中的优化不够有效。
尽管观察到的结果显示出性能优势,但代码改进和测试仍在继续。
大小测试
简单的尺寸测试表明 SmartInt
使用与 int 相同的内存大小。Int32?
使用两倍大小。观察到的结果与预期相等。
public static void ArraySize()
{
int objects_count = 1000;
long memory1 = GC.GetTotalMemory(true);
int[] ai = new int[objects_count];
long memory2 = GC.GetTotalMemory(true);
int?[] ani = new int?[objects_count];
long memory3 = GC.GetTotalMemory(true);
SmartInt[] asi = new SmartInt[objects_count];
long memory4 = GC.GetTotalMemory(true);
// Compiler can optimize and do not allocate arrays if they are not used
// So we write their lengths
Console.WriteLine("Array sizes {0}, {1}, {2}", ai.Length, ani.Length, asi.Length);
Console.WriteLine("Memory for int \t {0}", memory2 - memory1);
Console.WriteLine("Memory for int? \t {0}", memory3 - memory2);
Console.WriteLine("Memory for SmartInt \t {0}", memory4 - memory3);
}
输出
Array sizes 1000, 1000, 1000
Memory for int 4024
Memory for int? 8024
Memory for SmartInt 4024
Press any key to exit.
Using the Code
这里有一个在 Microsoft Visual Studio Community 2015 中创建的 C# 小项目。在运行示例之前,您需要安装 SSTypesLT NuGet 包(例如,如 https://nuget.net.cn/packages/SSTypesLT 所述)。该项目只包含一小部分代码,以便快速入门。更多示例可在项目网站上通过链接 https://github.com/dgakh/SSTypes 获取。
关注点
本文提及的技术很有趣,需要进一步研究。开发者不应忽视改进代码的能力,特别是以下几点
- 如果使用现代编译器,结构内的包含不会增加执行代码的负担。
- 可以创建具有编译器控制语义的安全类型,而无需任何运行时开销(可能只在第一次启动时)。
- 可以创建与
Nullable
类型几乎相同的逻辑,而无需开销。Nullable
在许多平台上为状态控制额外消耗 32 位。 - 测试表明,可以编写比标准方法更快的方法。
- 研究开源代码可以提供许多有趣的思路。
所述技术的实际应用可能包括
- 移动应用程序的开发,其中资源稀缺
- 大数据,其中每个节省的字节都能节省存储空间,从而提高吞吐量并节省时间
- C# 语言、CLR、.Net Framework 等方面的研究
参考文献
- NuGet SSTypesLT 包:https://nuget.net.cn/packages/SSTypesLT
- SSTypes 项目:https://github.com/dgakh/SSTypes
- PerfDotNet 项目:https://github.com/PerfDotNet
- C# 中的 typedef:https://codeproject.org.cn/Questions/141385/typedef-in-C
- .NET 中引入语义类型:https://codeproject.org.cn/Articles/860646/Introducing-Semantic-Types-in-Net
- .NET 中的轻量级语义类型:https://codeproject.org.cn/Articles/1036239/Lightweight-Semantic-Types-in-NET#xx5141198xx
- 使用语义类型的强类型检查:https://codeproject.org.cn/Articles/1031504/Strong-Type-Checking-with-Semantic-Types
- C#.NET 中的可空类型:https://codeproject.org.cn/Articles/275471/Nullable-Types-in-Csharp-Net