使用 C# 进行系统编程

  • Pierre-Luc Maheu
  • 大愚若智

2016 年 7 月 17 日

话题:.NETC#语言 & 开发架构

虽然对于系统编程(System programming)的定义很模糊,不过可以将其描述为在比特、字节、指令,或 CPU 周期层面所进行的思考。系统编程这个概念也暗含了对性能和可靠性的需求。Microsoft 技术总监 Joe Duffy 在QCon New York活动中介绍了使用 C# 进行系统编程的战略,同时他还谈到了这其中的一些陷阱以及缓解方法。

Joe 的演讲中很多内容来自一个名为 Midori 的研究项目。该项目意在使用 C# 从零开始打造一个操作系统,这也让我们对编译器的结构和有关高性能代码的新战略有了全新认识。

使用托管语言(Managed language)构建操作系统使得我们能够在内存层面上运用 C# 中的安全功能。这样做可以避免由于缓冲区溢出或格式字符串(Format string)弱点而针对内存进行的代码注入攻击,因为此时可以由运行时负责边界检查(Bound checking)和类型安全(Type safety)。

代码的生成

代码可以通过预先(Ahead of time,Aot)或即时(Just in time,Jit)的方式编译。Jit 的优势在于编译速度更快,但 Aot 可以获得更好的机器代码,因为编译器可以对代码执行更多优化。

原生语言编译器实现的很多优化原本是托管语言所不具备的。一般原因通常在于,通过 Jit 编译器实现这些优化通常可能需要极大的运算量或过于复杂。正是这些问题导致 C# 在紧密、高效的底层代码生成方面口碑不佳。最近通过 RyuJit 实现了下列这些优化:

  • Inlining(使用所调用函数的主体取代函数调用点)
  • Flowgraph 和 Loop 分析
  • 静态单一指派(SSA)和全局值编号
  • 通用子表达式消元(Common subexpression elimination)
  • 复制 / 常量传播(Copy/constant propagation)
  • 无用代码删除
  • 范围分析
  • 非虚拟化(Devirtualization)
  • 循环不变量代码提升(Loop invariant code hoisting)
  • SIMD 和向量化
  • 通用共享(Generic sharing)
  • 堆栈分配(正在开发中)

垃圾回收

.NET 中的垃圾回收已发展到第三代。一些数据程序分析师需要将自己过半的时间用于垃圾回收,而无法用在更有价值的工作中。

改善性能的方法之一是使用 Struct,Struct 可改善下列领域的性能问题:

  • 降低 GC 压力,因为 Struct 是针对堆栈分配的。
  • 提高内存局部性(Memory locality),改善缓存命中率。
  • 整体减少对内存的使用,避免 32-64 位应用程序中对象的 8-16 字节开销。

关于 Struct 有个问题需要注意:在复制超出某一大小的 Struct 时可能导致 Memcpy。为了优化性能,应确保 Struct 尽可能小,不要超过 32/64 字节。

C# 7 的一些功能使得通过 Struct 进行底层优化的过程变得更容易。C# 7 的元数组(Tuple)是 Struct,而非老版本中的 System.Tuple<>,后者现已成为对象。引用返回则是 Struct 的另一个特性,可以无需复制直接通过函数返回 Struct。

错误处理

可恢复的错误总会存在例外,然而很多错误都是不可恢复的。诸如无效转换、栈溢出以及空引用等错误实际上属于 Bug。但 I/O 故障和验证错误是可以预见并从中恢复的。

这种错误恢复催生了 Fail fast 策略。Fail fast是一种包含在.NET 中的机制,这种机制下诸如 StackOverflow 等例外可能会绕过异常处理程序导致进程崩溃。该策略使得查找此类错误的过程变得更简单,因为此时例外已经无法被过度的通用异常处理程序所处理。Midori 团队发现他们的可恢复错误(例外)与 Bug(Fail fast)已经达到了 1:10 的比例。

详细信息请参阅 Joe 的博客,他在博客上写了很多有关 Midori 的文章。他的演讲演示文稿也已经发布到网上。

查看英文原文Systems Programming in C#

.NETC#语言 & 开发架构