17

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

 3 years ago
source link: http://www.cnblogs.com/ShoneSharp/p/ShoneMath-1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

作者:Shone

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

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

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

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

但是对于基础类似的数值运算,.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<T>

泛型数值计算实现方法很多,不外乎通过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<T>泛型静态类,包装所有计算的delegate,提供泛型调用就可以了,简单直接明了。

2、Math<T>有哪些内容

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

public static class Math<T>

{

//各种常量

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<T, bool> IsNormal = xTrue;

public static Func<T, bool> IsSubnormal = xFalse;

public static Func<T, bool> IsFinite = xTrue;

public static Func<T, bool> IsNaN = xFalse;

public static Func<T, bool> IsInfinity = xFalse;

public static Func<T, bool> IsPositiveInfinity = xFalse;

public static Func<T, bool> IsNegativeInfinity = xFalse;

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

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

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

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

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

public static Func<T, bool> Not = x => !ToBool(x);

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

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

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

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

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

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

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

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

public static Func<T, T, T> LeftShift = (x, y) => FromLong(ToLong(x) << ToInt(y));

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

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

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

public static Func<T, T, bool> LessThan = (x, y) => ToInt(x) < ToInt(y);

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

public static Func<T, T, bool> LessEqual = (x, y) => ToInt(x) <= ToInt(y);

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

public static Func<T, T, bool> Equal;

public static Func<T, T, bool> NotEqual;

public static Func<bool, T> FromBool;

public static Func<char, T> FromChar;

public static Func<sbyte, T> FromSByte;

public static Func<byte, T> FromByte;

public static Func<short, T> FromShort;

public static Func<ushort, T> FromUShort;

public static Func<int, T> FromInt;

public static Func<uint, T> FromUInt;

public static Func<long, T> FromLong;

public static Func<ulong, T> FromULong;

public static Func<float, T> FromFloat;

public static Func<double, T> FromDouble;

public static Func<decimal, T> FromDecimal;

public static Func<Real, T> FromReal;

public static Func<T, bool> ToBool;

public static Func<T, char> ToChar;

public static Func<T, sbyte> ToSByte;

public static Func<T, byte> ToByte;

public static Func<T, short> ToShort;

public static Func<T, ushort> ToUShort;

public static Func<T, int> ToInt;

public static Func<T, uint> ToUInt;

public static Func<T, long> ToLong;

public static Func<T, ulong> ToULong;

public static Func<T, float> ToFloat;

public static Func<T, double> ToDouble;

public static Func<T, decimal> ToDecimal;

public static Func<T, Real> ToReal;

public static Func<string, T> Parse;

public static TryParseDelegate TryParse;

public static Func<T, int> Sign = x => Math.Sign(ToInt(x));

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

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

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

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

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

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

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

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

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

public static Func<T, T> Floor = xSelf;

public static Func<T, T> Ceiling = xSelf;

public static Func<T, T> Round = xSelf;

public static Func<T, T> Truncate = xSelf;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

}

3、Math<T>实现原则

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

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

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

4、Math<T>解决问题

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

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

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

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

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

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

四、Shone.Math泛型使用方法

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

using Shone;   // import Shone namespace

var d = Math<decimal>.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<T>唯一遗憾

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

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

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

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

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

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

六、小结

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK