.NET在数值计算中的速度

根据我的经验,.NET比本机代码慢2到3倍。 (我实施了L-BFGS进行多变量优化)。

我已经在stackoverflow上跟踪广告到http://www.centerspace.net/products/

速度真的很惊人,速度接近本机代码。 他们怎么能这样做? 他们说:

问:NMath是“纯粹的”.NET吗?

答:答案在某种程度上取决于您对“纯.NET”的定义。 NMath是用C#编写的,加上一个小的Managed C ++层。 但是,为了更好地执行基本线性代数运算,NMath确实依赖于原生的英特尔数学核心库(包含在NMath中)。 但是没有COM组件,没有DLL – 只是.NET程序集。 此外,在托管C ++层中分配并由本机代码使用的所有内存都是从托管堆中分配的。

有人可以向我解释一下吗?

关于C ++ / CLI的观点是正确的。 要完成图片,只需另外两点:

  • .NET内存管理(垃圾收集器)显然不是问题,因为NMath仍然依赖于它

  • 性能优势实际上是由英特尔MKL提供的,它为许多CPU提供了极其优化的实现。 从我的观点来看,这是至关重要的一点。 使用直接的,naiv C / C ++代码不一定会比C#/ .NET提供更好的性能,有时甚至更糟。 但是,C ++ / CLI允许您利用所有“脏”优化选项。

他们怎么能这样做?

与.NET的大多数数字库一样,NMath只不过是.NET程序集中嵌入的英特尔MKL的包装器,可能通过与C ++ / CLI链接来创建混合程序集 。 您可能只是对那些实际上没有用.NET编写的位进行基准测试。

F#.NET期刊文章数值库:特殊函数,插值和随机数 (2008年3月16日)和数值库:线性代数和频谱​​方法 (2008年4月16日)测试了相当多的function,而NMath实际上是所有的最慢的商业图书馆。 他们的PRNG比其他所有人慢,比免费的Math.NET库慢50%,缺少一些基本function(例如计算Gamma(-0.5)的能力)和其他基本function(他们提供的Gamma相关function)坏了。 Extreme Optimization和Bluebit都在eigensolver基准测试中击败了NMath。 NMath当时甚至没有提供傅立叶变换。

更令人惊讶的是,性能差异有时是巨大的。 我们测试的最昂贵的商业数字库(IMSL)比FFT基准测试中的免费FFTW库慢500多倍,并且当时没有一个库使用多个内核。

事实上,正是这些库的质量差,鼓励我们将我们自己的F#商业化为Numerics库(这是100%纯F#代码)。

我是ILNumerics的主要开发人员之一。 所以我很有偏见,很明显;)但是我们更多地披露了我们的内部情况,所以我会对我们的速度’秘密’给出一些见解。

这一切都取决于系统资源的利用方式! 如果你是关于纯粹的速度并且需要处理大型数组,你将确保(按重要性排序,最重要的是首先)

  1. 妥善管理你的记忆! “天真”的内存管理会导致性能不佳,因为它严重影响GC,导致内存碎片并降低内存局部性(因此缓存性能)。 在像.NET这样的垃圾收集环境中,这归结为防止频繁的内存分配。 在ILNumerics中,我们实现了一个高性能内存池,以实现这一目标(并确定性地处理临时数组,以获得一个不错的,舒适的语法而没有笨拙的函数语义)。

  2. 利用并行性! 这针对两者:线程级并行和数据级并行。 通过计算的计算密集部分来利用多个核。 在X86 / X64 CPU上,像SSE.XX和AVX这样的SIMD /多媒体扩展允许小而有效的矢量化。 它们不能被当前的.NET语言直接寻址。 这是唯一的原因,为什么MKL可能仍然比’纯’.NET代码更快。 (但解决方案已在上升。)

  3. 为了实现高度优化的语言(如FORTRAN和C ++)的速度,必须将相同的优化应用于您的代码。 C#提供了这样做的选项。

注意,这些注意事项应该按顺序进行! 如果瓶颈是内存带宽并且处理器花费大部分时间等待新数据,那么关心SSE扩展甚至绑定检查删除是没有意义的。 此外,对于许多简单的操作而言,投入巨大的努力来实现最后的小规模达到峰值性能甚至都不值得付出代价! 考虑LAPACK函数DAXPY的常见示例。 它将向量X的元素添加到另一个向量Y的相应元素。如果这是第一次完成,则必须从主存储器获取X和Y的所有内存。 你无能为力。 而记忆是瓶颈! 所以不管最后的添加是否以C#中的天真方式完成

 for (int i = 0; i < C.Length; i++) { C[i] = X[i] + Y[i]; } 

或者通过使用矢量化策略完成 - 它将不得不等待内存!

我知道,这个答案在某种程度上“回答”了这个问题,因为目前大多数这些策略都没有从上述产品中使用(但是?)。 通过遵循这些要点,您最终将获得比“本机”语言中的每个天真实现更好的性能。

如果您有兴趣,可以透露您的L-BFGS实施情况? 我很乐意将其转换为ILNumerics并发布比较结果,我相信,此处列出的其他库将遵循。 (?)

我发布了一篇解决这个问题的博客文章 。

关键是C ++ / CLI 。 它允许您将C ++代码编译为托管.NET程序集。

今天,制作混合.Net /本机库是行业标准,以便利用这两个平台进行性能优化。 不仅是NMath,许多商业和免费的图书馆都有.net接口这样工作。 例如: Math.NET Numerics, dnAnalytics ,Extreme Optimization, FinMath等等。 与MKL的集成在.net数值库中非常流行,并且大多数仅使用托管C ++程序集作为中间级别。 但是这个解决方案有许多缺点:

  1. 英特尔MKL是一款专有软件,价格有点贵。 但是像dnAnalytics这样的库提供了纯粹的.net代码免费替换MKLfunction。 当然,它慢得多,但它是免费的,function齐全。

  2. 它会降低您在32位和64位模式下拥有大量托管C ++内核dll所需的兼容性。

  3. 管理到本机调用需要执行编组,这会降低快速调用的快速操作(如Gamma或NormalCDF)的性能。

RTMath FinMath库中解决了最后两个问题。 我真的不知道他们是怎么做到的,但是他们提供了单纯的.net dll,它为Any CPU平台编译并支持32bit和64bit。 当我需要拨打数十亿次NormalCDF时,我也没有看到任何针对MKL的性能下降。

由于(本机)英特尔MKL正在进行数学计算,因此您实际上没有在托管代码中进行数学运算。 您只是使用.Net的内存管理器,因此.Net代码可以轻松使用结果。

我从@Darin Dimitrov对他的回答和@Trevor Misfeldt对@ Darin评论的评论中得到了更多的评论。 因此,将其作为答案发布给未来的读者。

NMath使用P / Invoke或C ++ / CLI来调用Intel Math Kernel Library本机函数,这是进行最密集计算的原因,也是它如此之快的原因。

花在 英特尔MKL内部的分解方法上时间也不需要复制数据 。 因此, 这不是CLI是否快速的问题这是关于执行发生的地方

另外@Paul的博客也很好看。 这是摘要。

C#是快速的,内存分配不是。 将变量重用为ref或out参数 ,而不是从方法返回新变量。 分配新变量会占用内存并降低执行速度。 @Haymo Kutschbach对此做了很好的解释。

如果不需要精度,则从双精度切换到单精度的性能提升是相当大的(更不用说数据存储的内存节省)。

对于许多简短的计算,从C#调用C ++ / cli例程,将所有指针固定到托管空间中分配的数据,然后调用Intel库通常比使用P / Invoke直接从C#调用库更好,因为整理数据的成本。 正如@Haymo Kutschbach在评论中提到的那样,对于blittable类型,C ++ / CLI和C#之间没有区别。 仅包含blittable成员的blittable类型和类的数组在编组期间被固定而不是复制。 有关blittable和non-blittable类型的列表,请参阅https://msdn.microsoft.com/en-us/library/75dwhxf7(v=vs.110).aspx 。