CLR 集成体系结构的性能
适用于: SQL Server Azure SQL 托管实例
本文讨论一些设计选择,这些选择可增强 SQL Server 与 Microsoft .NET Framework 公共语言运行时(CLR)的集成性能。
编译过程
在编译 SQL 表达式期间,遇到对托管例程的引用时,将生成Microsoft中间语言(MSIL)存根。 此存根包含用于将例程参数从 SQL Server 封送到 CLR、调用函数并返回结果的代码。 该“粘附”代码基于参数类型和参数方向(向内、向外或引用)。
粘附代码可实现特定于类型的优化,并确保有效强制实施 SQL Server 语义,例如可为 null 性、约束方面、按值和标准异常处理。 通过为确切类型的参数生成代码,可以避免跨调用边界的类型强制或包装对象创建开销(称为“装箱”)。
然后,生成的存根将编译为本机代码,并使用 CLR 的实时 (JIT) 编译服务针对 SQL Server 执行的特定硬件体系结构进行优化。 JIT 服务在方法级别调用,并允许 SQL Server 托管环境创建跨 SQL Server 和 CLR 执行的单个编译单元。 编译存根之后,生成的函数指针即成为函数的运行时实现。 此代码生成方法可确保在运行时不会产生与反射或元数据访问相关的额外调用成本。
SQL Server 和 CLR 之间的快速转换
编译过程生成的函数指针可以在运行时通过本机代码调用。 对于标量值用户定义函数,此函数调用按行进行。 为了最大程度地降低在 SQL Server 和 CLR 之间转换的成本,包含任何托管调用的语句都有一个启动步骤来标识目标应用程序域。 该标识步骤减少每行的转换成本。
性能注意事项
以下部分总结了特定于 SQL Server 中的 CLR 集成的性能注意事项。 有关详细信息,请参阅 在 SQL Server 2005 中使用 CLR 集成。 有关托管代码性能的信息,请参阅 改进 .NET 应用程序性能和可伸缩性。
用户定义的函数
CLR 函数受益于比 Transact-SQL 用户定义的函数更快的调用路径。 此外,在过程代码、计算和字符串操作方面,托管代码在 Transact-SQL 方面具有决定性的性能优势。 计算密集型且不执行数据访问的 CLR 函数最好用托管代码编写。 但是,Transact-SQL 函数比 CLR 集成更高效地执行数据访问。
用户定义聚合
托管代码的性能大大优于基于游标的聚合。 托管代码通常比内置 SQL Server 聚合函数慢一些。 如果存在本机内置聚合函数,建议您使用该函数。 如果本机不支持所需的聚合,请考虑基于游标的实现的 CLR 用户定义的聚合,因为性能原因。
流式处理表值函数
应用程序通常需要返回一个表作为调用函数的结果。 示例包括从文件读取表格格式数据作为导入操作的一部分,并将逗号分隔值转换为关系表示形式。 通常,您可以通过在调用方使用结果表之前具体化和填充此结果表来实现此目的。 CLR 与 SQL Server 的集成引入了一种称为流式处理表值函数(STVF)的新扩展性机制。 托管 STVF 的性能优于可比扩展存储过程实现的性能。
STVF 是可返回 IEnumerable
接口的托管函数。 IEnumerable
具有用于导航 STVF 返回的结果集的方法。 当调用 STVF 时,返回的 IEnumerable
直接连接到查询计划。 查询计划在需要提取行时调用 IEnumerable
方法。 使用此迭代模型,结果在第一行生成之后即可使用,而不需要等到整个表填充完。 还可以极大地减少调用该函数而占用的内存。
数组与游标
当 Transact-SQL 游标必须遍历更容易表示为数组的数据时,托管代码可以显著提高性能。
字符串数据
SQL Server 字符数据(如 varchar)可以是托管函数中的 SqlString 或 SqlChars 类型。 SqlString 变量将整个值的实例创建到内存中。 SqlChars 变量提供可用于获得更好性能和可扩展性的流式接口,而无需将整个值的实例创建到内存中。 这对于大型对象(LOB)数据非常重要。 此外,还可以通过 SqlXml.CreateReader()
返回的流式接口访问服务器 XML 数据。
CLR 与扩展存储过程
Microsoft.SqlServer.Server
允许托管过程将结果集发送回客户端的应用程序编程接口(API)的性能优于扩展存储过程使用的 Open Data Services (ODS) API。 此外,System.Data.SqlServer API 支持 SQL Server 2005(9.x)中引入的 xml、varchar(max)、nvarchar(max)和 varbinary(max)等数据类型,而 ODS API 尚未扩展以支持新的数据类型。
使用托管代码,SQL Server 管理对内存、线程和同步等资源的使用。 这是因为公开这些资源的托管 API 是在 SQL Server 资源管理器之上实现的。 相反,SQL Server 对扩展存储过程的资源使用情况没有查看或控制。 例如,如果扩展存储过程消耗过多的 CPU 或内存资源,则无法通过 SQL Server 检测或控制此问题。 但是,使用托管代码,SQL Server 可以检测到给定线程长时间未生成,然后强制任务生成,以便可以计划其他工作。 因此,使用托管代码可提供更好的可伸缩性和系统资源使用情况。
托管代码可能会导致维护执行环境和执行安全检查所需的额外开销。 例如,在 SQL Server 中运行时,需要从托管代码到本机代码进行大量转换(因为 SQL Server 在移出到本机代码时需要对线程特定的设置执行额外的维护)。 因此,对于托管代码和本机代码之间频繁转换的情况,扩展存储过程可以明显优于 SQL Server 中运行的托管代码。
注意
不要开发新的扩展存储过程,因为此功能已弃用。
用户定义的类型的本机序列化
用户定义类型 (UDT) 是作为标量类型系统的扩展性机制设计的。 SQL Server 为调用 Format.Native
的 UDT 实现序列化格式。 在编译期间,检查该类型的结构以便生成针对该特定类型定义自定义的 MSIL。
本机序列化是 SQL Server 的默认实现。 用户定义序列化调用由类型作者定义的方法来执行序列化。 应尽可能使用 Format.Native
序列化以便获得最佳性能。
可比较 UDT 的规范化
关系操作(例如对 UDT 进行排序和比较)是针对值的二进制表示形式直接执行的。 通过在磁盘上存储 UDT 状态的规范化(二进制排序)表示形式可以实现此目的。
规范化有两个优点:
它通过避免构造类型实例和方法调用开销,使比较操作的成本要低得多
它为 UDT 创建二进制域,从而为类型的值启用直方图、索引和直方图的构造。
因此,规范化 UDT 具有与本机内置类型类似的性能配置文件,这些操作不涉及方法调用。
可缩放内存使用情况
为了使托管垃圾回收在 SQL Server 中执行和缩放良好,请避免进行大型的单一分配。 大于 88 千字节(KB)大小的分配放置在大型对象堆上,这会导致垃圾回收的执行和缩放比许多较小的分配更差。 例如,如果需要分配大型多维数组,最好分配杂交(散点)数组。