Shone.Math开源系列1 — 基于.NET 5实现Math<T>泛型数值计算

2021-02-18 04:19

阅读:463

标签:osi   ash   rabl   center   oat   覆盖   run   review   支持ie   

Shone.Math开源系列1 — 基于.NET 5实现Math泛型数值计算

作者:Shone

.NET 5 preview 4已经可用了,从微软Build2020给出的信息看,.NET 5将实现框架统一,.NET 6将实现界面统一。开源的.NET更加有活力,咱们也从基础开始贡献一点微薄力量,拥抱开源,拥抱.NET未来。

Shone.Math是一个支持Math泛型数值计算和Real实数运算(浮点数、分数、PI,E,Log,Exp等无理数)的轻量级基础数学库。该项目开源地址https://github.com/shonescript/Shone.Math,是本人把多年代码积累正式转向.NET 5,也是我的第一个开源项目,请大家多多支持了。

一、.NET泛型数值计算优势

.NET 2.0开始支持泛型编程,支持IEnumerable, List, Func等各种泛型类型,提高了编程效率和质量,这是公认的价值。

但是对于基础类似的数值运算,.NET没有默认泛型实现方式。StackOverflow上有大量关于泛型数值计算的讨论,C#9.0的部分草案建议也提出添加对泛型计算的支持。

在大量处理数据时,特别是几何或空间数据计算时,泛型数值计算的主要优势是:

(1)可重用:专注于数值计算算法,不用为每种数据编写实现,提高开发效率;

(2)无装箱:直接支持各种数值类型,减少struct数值类型无装箱和拆箱,提高运行效率;

(3)动态切换:可在运行时动态切换数据类型,从float, double, decimal,根据需要可随时提高计算精度,平衡计算性能和存储占用。

二、.NET泛型数值计算难点

泛型数值计算优势这么多,那就赶快实现吧。但是彻底实现有点难,真的难,需要语言、甚至编译器底层支持。对于.net和C#语言是这样,其他大部分语言也是这样。

泛型数值计算的难点在于:

(1)数值类型很多:.NET有13中基础数值类型,包括bool, char, byte, sbyte, short, ushort, int, uint, long, ulong, float, double, decimal。除此之外,还有我自己编写实数类Real及其派生类Ration,IrrationXXX等,另有11种。(C#比较全面,其他语言略有差异)

(2)运算能力不同:浮点数支持大部分计算,整数只支持+-*/,char和bool型支持的更少,不支持运算编译会报错。(各语言类似)

(3)运算实现差异:.NET CLR为了提高效率,int, float, double等符号运算直接使用指令实现,在类型定义中找不到方法,而decimal则使用运算符重载实现。(其他语言应该也有类似技巧)

(4)泛型实现机制:.NET泛型属于运行时泛型,泛型T的可使用方法需要从约束推导。由于int, float, double等是系统特殊类型,其基类直接是object,没有暴露.Add/Multiply虚方法,也没有提供静态运算符扩展重载,因此没法直接从object做泛型。其实通过dynamic也可以,但动态类型开销巨大,而且必须进行装箱、拆箱,不是好办法。(C++等采用编译时模板实现泛型的,比较容易实现数值泛型,其他语言java以及动态语言有装箱、拆箱问题)

(5)泛型数据转换:泛型T在动态运行时如何与其他数据进行转换也是个难题,而且要求避免装箱、拆箱问题。(编译时泛型语言实现比较困难,而动态语言有装箱、拆箱问题)

(6)动态类型切换:在运行时动态切换数据类型,这个更难(静态语言较难,动态语言有优势)。

总之,泛型数值计算确实很难,各种实现都有利弊,否则微软应该在.NET2.0推出时就有解决方案,肯定是经过平衡取舍,只好留给开发者根据需要自己实现了。

三、Shone.Math泛型实现方法

Shone.Math有针对性解决了大部分上述障碍,填了很多坑,尽量做到易用性、性能等各方面平衡。各位有兴趣可以到开源项目地址,下载dll试用或代码研究一下,有BUG、问题或建议可以在上面直接提出来,也可以pull参与项目代码完善和实现。

1、关键在于delegate和Math

泛型数值计算实现方法很多,不外乎通过inerface、struct、以及delegate这三种进行各种姿势的补锅。我个人研究下来,interface实现很难避开装箱拆箱问题,struct需要组装等开销也不小使用不便,delegate是微软留给这个问题解决办法的一线生机,虽然还有小遗憾,但总体优雅直接。

delegate大家都知道,其实就是.NET托管世界的函数指针,对应C/C++函数指针功能。数值计算各类符号和函数说白了不就是函数调用,完全可以用函数指针、或delegate动态表达,不需要各种代码直接表达。那C#打开unsafe模式,也有函数指针,为什么不用呢?因为C#的指针是简化版,不支持泛型。现在很清楚了,只能用delegate,那么在哪里用呢?

平时不管大家编什么程序,应该都用过Math.Abs, Cos, Sin, Log, Exp等函数吧,主要支持double数值各种计算,.NET Core中后来还提供了MathF静态类,提供对应的float数值各种计算。看到这里应该有点明白了吧,与其每种数据类型写一个MathXXX静态类,不如直接提供一个Math泛型静态类,包装所有计算的delegate,提供泛型调用就可以了,简单直接明了。

2、Math有哪些内容

从上面叙述可以看出,Math应该包含数值类型和MathXXX的常用方法,主要常量和方法分类列出如下:

    public static class Math

{

//各种常量

        public static T MinValue;

        public static T MaxValue;

        public static T Epsilon;

        public static T NegativeInfinity;

        public static T PositiveInfinity;

        public static T NaN;

        public static T Zero;

        public static T One;

        public static T MinusOne;

        public static T PI;

        public static T E;

        public static T RadFactor;

        public static T DegFactor;

         //各种方法

        public static Func IsNormal = xTrue;

        public static Func IsSubnormal = xFalse;

        public static Func IsFinite = xTrue;

        public static Func IsNaN = xFalse;

        public static Func IsInfinity = xFalse;

        public static Func IsPositiveInfinity = xFalse;

        public static Func IsNegativeInfinity = xFalse;

        public static Func IsNegative = x => LessThan(x, Zero);

 

        public static Func Negate = x => FromDecimal(-ToDecimal(x));

        public static Func Increase = x => FromDecimal(ToDecimal(x) + 1);

        public static Func Decrease = x => FromDecimal(ToDecimal(x) - 1);

        public static Func Comp = x => FromLong(~ToLong(x));

        public static Func Not = x => !ToBool(x);

 

        public static Func Add = (x, y) => FromInt(ToInt(x) + ToInt(y));

        public static Func Subtract = (x, y) => FromInt(ToInt(x) - ToInt(y));

        public static Func Multiply = (x, y) => FromInt(ToInt(x) * ToInt(y));

        public static Func Divide = (x, y) => FromInt(ToInt(x) / ToInt(y));

        public static Func Modulus = (x, y) => FromInt(ToInt(x) % ToInt(y));

 

        public static Func BitAnd = (x, y) => FromLong(ToLong(x) & ToLong(y));

        public static Func BitOr = (x, y) => FromLong(ToLong(x) | ToLong(y));

        public static Func BitXOr = (x, y) => FromLong(ToLong(x) ^ ToLong(y));

        public static Func LeftShift = (x, y) => FromLong(ToLong(x)

        public static Func RightShif = (x, y) => FromLong(ToLong(x) >> ToInt(y));

 

        public static Func And = (x, y) => ToBool(x) && ToBool(x);

        public static Func Or = (x, y) => ToBool(x) || ToBool(x);

        public static Func LessThan = (x, y) => ToInt(x)

        public static Func GreatThan = (x, y) => ToInt(x) > ToInt(y);

        public static Func LessEqual = (x, y) => ToInt(x)

        public static Func GreatEqual = (x, y) => ToInt(x) >= ToInt(y);

        public static Func Equal;

        public static Func NotEqual;

 

        public static Func FromBool;

        public static Func FromChar;

        public static Func FromSByte;

        public static Func FromByte;

        public static Func FromShort;

        public static Func FromUShort;

        public static Func FromInt;

        public static Func FromUInt;

        public static Func FromLong;

        public static Func FromULong;

        public static Func FromFloat;

        public static Func FromDouble;

        public static Func FromDecimal;

        public static Func FromReal;

 

        public static Func ToBool;

        public static Func ToChar;

        public static Func ToSByte;

        public static Func ToByte;

        public static Func ToShort;

        public static Func ToUShort;

        public static Func ToInt;

        public static Func ToUInt;

        public static Func ToLong;

        public static Func ToULong;

        public static Func ToFloat;

        public static Func ToDouble;

        public static Func ToDecimal;

        public static Func ToReal;

 

        public static Func Parse;

        public static TryParseDelegate TryParse;

 

        public static Func Sign = x => Math.Sign(ToInt(x));

        public static Func Abs => x => FromInt(Math.Abs(ToInt(x)));

        public static Func Sqrt = x => FromDouble(Math.Sqrt(ToDouble(x)));

        public static Func Cbrt = x => FromDouble(Math.Pow(ToDouble(x), 1d / 3d));

        public static Func Exp = x => FromDouble(Math.Exp(ToDouble(x)));

        public static Func Pow = (x, y) => FromDouble(Math.Pow(ToDouble(x), ToDouble(y)));

        public static Func Log = x => FromDouble(Math.Log(ToDouble(x)));

        public static Func Log2 = x => FromDouble(Math.Log2(ToDouble(x)));

        public static Func Log10 = x => FromDouble(Math.Log10(ToDouble(x)));

        public static Func Logx = (x, y) => FromDouble(Math.Log(ToDouble(x), ToDouble(y)));

 

        public static Func Floor = xSelf;

        public static Func Ceiling = xSelf;

        public static Func Round = xSelf;

        public static Func Truncate = xSelf;

        public static Func Min = (x, y) => FromDouble(Math.Min(ToDouble(x), ToDouble(y)));

        public static Func Max = (x, y) => FromDouble(Math.Max(ToDouble(x), ToDouble(y)));

 

        public static Func Sin = x => FromDouble(Math.Sin(ToDouble(x)));

        public static Func Cos = x => FromDouble(Math.Cos(ToDouble(x)));

        public static Func Tan = x => FromDouble(Math.Tan(ToDouble(x)));

        public static Func Sinh = x => FromDouble(Math.Sinh(ToDouble(x)));

        public static Func Cosh = x => FromDouble(Math.Cosh(ToDouble(x)));

        public static Func Tanh = x => FromDouble(Math.Tanh(ToDouble(x)));

        public static Func Asin = x => FromDouble(Math.Asin(ToDouble(x)));

        public static Func Acos = x => FromDouble(Math.Acos(ToDouble(x)));

        public static Func Atan = x => FromDouble(Math.Atan(ToDouble(x)));

        public static Func Atan2 = (x, y) => FromDouble(Math.Atan2(ToDouble(x), ToDouble(y)));

        public static Func Asinh = x => FromDouble(Math.Asinh(ToDouble(x)));

        public static Func Acosh = x => FromDouble(Math.Acosh(ToDouble(x)));

        public static Func Atanh = x => FromDouble(Math.Atanh(ToDouble(x)));

 

        public static Func SinDeg = x => Sin(Multiply(x, RadFactor));

        public static Func CosDeg = x => Cos(Multiply(x, RadFactor));

        public static Func TanDeg = x => Tan(Multiply(x, RadFactor));

        public static Func SinhDeg = x => Sinh(Multiply(x, RadFactor));

        public static Func CoshDeg = x => Cosh(Multiply(x, RadFactor));

        public static Func TanhDeg = x => Tanh(Multiply(x, RadFactor));

        public static Func AsinDeg = x => Multiply(Asin(x), DegFactor);

        public static Func AcosDeg = x => Multiply(Acos(x), DegFactor);

        public static Func AtanDeg = x => Multiply(Atan(x), DegFactor);

        public static Func AtanDeg2 = (x, y) => Multiply(Atan2(x, y), DegFactor);

        public static Func AsinhDeg = x => Multiply(Asinh(x), DegFactor);

        public static Func AcoshDeg = x => Multiply(Acosh(x), DegFactor);

        public static Func AtanhDeg = x => Multiply(Atanh(x), DegFactor);

}

3、Math实现原则

Math实现还是有些技巧和原则的:

(1)有默认实现:所有常量都有默认值,方法都有默认实现,可能效率不高,但支持所有数据类型,而且可根据需要覆盖重载。这样整数(包括bool和char)也能进行各种Log, Sin运算,只不过运算结果进行了取整,不会报错。

(2)一次静态初始化:每个类型的初始化放在Math的静态构造函数中,只有第一次使用时有点开销,后续调用没有任何性能损失。

4、Math解决问题

(1)24个数值类型全部支持:其他自定义类型只要提供相关实现,也可以扩展支持到Math中,我的Real类型就是这样干的。本系列博客会有专门文章介绍。

(2)统一提供所有运算符:不管数据类型,来者不拒,统统支持。

(3)共性默认,个性重载:所有实现方法提供默认算法实现,常用热点函数直接使用反射,从数据类型、Math、MathF、甚至DecimalEx等中抓取delegate进行覆盖重载,性能与原始实现接近。

(4)数值和引用泛型都支持:int, float, double等系统特殊类型为struct,直接按强类型运算,无装箱拆箱开销。Real等实数类型为object引用类型,可自由转换,也无装箱拆箱开销。

(5)统一提供泛型数据转换:从上面的Math可以看到,该类中包含了14个FromXXX和14个数据ToXXX进出函数,涵盖最常用的所有数据转换情况,使用起来非常方便。

(6)为动态切换奠定基础:有了Math,可以调用typeof(Math).MakeGenericType()在运行时实现Math的动态调用,当然要支持动态切换数据类型好需要一些技巧和实现。目前版本Shone.Math暂不支持,后续我会补充实现,并在系列中重点介绍。

四、Shone.Math泛型使用方法

Shone.Math只有一个dll文件,除了.NET5系统外无任何外部依赖。注意:Shone.Math支持.NET5以上版本,一方面是拥抱未来向前看,另一方面是开始时发现.NET4和.NET5差好多内容,如MathF类,Math.Asinh,Acosh,Atanh,还有各种Span,Memory等高级类型,这也符合.NET5一统江湖的趋势。

1、安装Visual Studio 2019

更新到最新版,在选项设置中打开.net preview支持。

2、下载nuget包或github代码

Nuget包:https://www.nuget.org/packages/Shone.Math/1.0.0

源代码:https://github.com/shonescript/Shone.Math/releases

3、引用nuget包或Shone.Math.dll到你的项目中

4、添加命名空间using Shone;

5、愉快地使用Math方法或扩展

using Shone;   // import Shone namespace

var d = Math.Pow(5,3);     // use just like Math.Pow, but it is generic now!

var x = 5m.Pow(3);     // write in dot style

var ds = new double[]{5m, 6m, 7m}.Pow(3);   // calculate array easily

五、Math唯一遗憾

由于.NET目前暂不支持泛型静态运算符扩展重载,因此还无法使用+,-,*,/等符号书写泛型计算表达式,编程代码有所冗余。不过据说C#9.0会解决该问题,那就拭目以待,如果有Shone.Math会站第一排给予支持了。

没有运算符,做一下sin((x+y)/2)泛型计算的代码刚开始是这样:

Math.Sin(Math.Divide(Math.Add(x, y), FromInt(2))

这很罗嗦了,为此Shone.Math专门提供了MyNum的扩展类,可以简化成那样:

x.Add(y).Divide(FromInt(2)).Sin()

这不就是传说中的Linq流派写法,已经比较接近符号写法了,你说还要哪样。

六、小结

Shone.Math通过各种精巧实现,提供了统一的泛型数值计算静态类Math,为开发各类自定义数值、几何、空间、公式解析等泛型数值应用打下了坚实基础。本系列下一章节将介绍Shone.Math的一些.NET5专用高级特性如ref, Span, Memory的泛型数值计算扩展。

今年初我个人开始全面转向使用.NET 5开发,感觉非常简洁顺畅,结合C#语言新特性nuget和github工作流。基于.NET和C#语言层面开发已经酸爽无比,社区各类开源项目也在不断增强,希望也从自己做起,通过Shone.Math为.NET社区做点贡献。

声明:原创文章欢迎转载,但请注明出处,https://www.cnblogs.com/ShoneSharp。

Shone.Math开源系列1 — 基于.NET 5实现Math泛型数值计算

标签:osi   ash   rabl   center   oat   覆盖   run   review   支持ie   

原文地址:https://www.cnblogs.com/ShoneSharp/p/ShoneMath-1.html


评论


亲,登录后才可以留言!