作者:
目录Vector<T>
)Vector<T>
编写浮点数组求和函数从.NET Core 1.0开始,.NET里增加了2种向量类型——
Vector<T>
,及辅助的静态类 Vector。到了 .NET Core 3.0,增加了内在函数(Intrinsics Functions)的支持,并增加了第3类向量类型——
3. 总位宽固定的向量(Vector of fixed total bit width)。例如 只读结构体 Vector64<T>
、Vector128<T>
、Vector256<T>
,及辅助的静态类 Vector64、Vector128、Vector256。
这3类向量类型,均能利用CPU硬件的SIMD(float Instruction Multiple Data,单指令多数据流)功能,来加速多媒体数据的处理。但是它们名称很接近,对于初学者来说容易混淆,而且应用场景稍有区别,本文致力于解决这些问题。
本章重点解说前2种向量类型(Vector4、Vector<T>
),第3种向量类型将由第2章来解说。
本章回答了这些问题——
Vector<T>
未提到.NET Framework的支持版本。难道 .NET Framework用不了Vector<T>
吗? .NET Framework 4.5等版本时是否能使用它们?Vector.IsHardwareAccelerated==false
)时,使用向量类型会有什么问题吗?用高级语言处理数据时,一般是SISD(float instruction float data,单指令流单数据流)模型的,即一个语句只能处理一条数据。
而对于多媒体数据处理,任务的特点是运算相对简单,但是数据量很大,导致SISD模型的效率很低。
若使用SIMD模型的话,一次能处理多条数据,从而能成倍的提高性能。
.NET Core引入了向量数据类型,从而使C#(等.NET中语言)能使用SIMD加速数据的处理。
并不是所有的数据处理工作都适合SIMD处理。一般来说,需满足以下条件,才能充分利用SIMD加速——
对于以下情况,SIMD代码的性能会急剧下降,应尽量避免——
基于以上原因,发现最适合演示SIMD运算优势的,是做“浮点数组求和运算”。先在Map阶段处理并发的进行分组求和,最后在Reduce阶段将各组结果加起来。
为了对比测试,先用传统的办法来编写一个“单精度浮点数组求和”的函数。
其实算法很简单,写个循环进行累加求和就行。代码如下。
private static float SumBase(float[] src, int count) { float rt = 0; // Result. for(int i=0; i< count; ++i) { rt += src[i]; } return rt; }
由于.NET向量类型的初始化会有一些开销,为了避免这些开销影响主循环的性能测试结果,于是需要将它们移到循环外。为了测试方便,求和函数可增加一个loops参数,它是测试次数,作为外循环。loops为1时,就是标准的变量求和;为其他值时,是多轮变量求和的累计值。由于浮点精度有限的问题,累计值可能与乘法结果不同。
为了能统一进行测试,于是基本算法也增加了 loops 参数。
private static float SumBase(float[] src, int count, int loops) { float rt = 0; // Result. for (int j=0; j< loops; ++j) { for(int i=0; i< count; ++i) { rt += src[i]; } } return rt; }
大小固定的向量类型,是以下3种结构体——
它们实际上是对数学(线性代数分支)里“向量”(Vector)的封装。命名规则为“'Vector' + [维数]”,例如 Vector2是数学里的“二维向量”、Vector3是数学里的“三维向量”、Vector4是数学里的“四维向量”。
于是这些类型,除了提供了常见的四则运算函数外,还提供了 向量长度(Length)、向量距离(Distance)、点积(Dot)、叉积(Cross) 等线性代数领域的函数。
它其中元素的数据类型,被限制为 float(32位单精度浮点值)。能用于常见单精度浮点运算场合。
使用这些向量类型时,JIT会尽可能的利用硬件加速,但是没有提供“是否有硬件加速”的标志。
这是因为不同的运算函数,在不同的CPU指令集里,有些能硬件加速,而另一些不能,很难通过简单的标志来区分。于是JIT仅是保证能尽可能的利用硬件加速,让使用者不用关心这些硬件细节。
一般来说,直接用这些类型的封装函数(如点积、叉积 运算等),比手工按数学定义编写的运算函数,效率更高。因为即使没有硬件加速时,这些封装好的函数是高水平的程序员编写的成熟代码。
Vector2、Vector3 比起 Vector4,元素个数要少一些,从数学定义上来看,理论运算量要少一些。
但是硬件的SIMD加速,大多是按“4元素并行处理”来设计。故很多时候,“Vector2、Vector3”运算性能与“Vector4”差不多。甚至在一些特别场合,比“Vector4”性能还低,因为对于硬件来说,可能会有多余的 忽略多余元素处理、数据转换 工作。
于是建议这样使用——
现在,我们使用Vector4,来编写浮点数组求和函数。
思路:Vector4内有4个元素,于是可以分为4个组分别进行求和(即Map阶段),最后再将4个组的结果加起来(即Reduce阶段)。
我们先可建立SumVector4函数。根据之前所说(为了.NET向量类型的初始化),该函数还增加了1个loops参数。
/// <summary> /// Sum - Vector4. /// </summary> /// <param name="src">Soure array.</param> /// <param name="count">Soure array count.</param> /// <param name="loops">Benchmark loops.</param> /// <returns>Return the sum value.</returns> private static float SumVector4(float[] src, int count, int loops) { float rt = 0; // Result. // TODO return rt; }
注意,数组长度可能不是4的整数倍。此时仅能对前面的、4的整数倍的数据用Vector4进行运算,而对于末尾剩余的元素,只能用传统办法来处理。
此时可利用“块”(Block)的概念来简化思路:每次内循环处理1个块,先对能凑齐整块的数据用Vector4进行循环处理(cntBlock),最后再对末尾剩余的元素(cntRem)按传统方式来处理。
Vector4有4个元素,于是块宽度(nBlockWidth)为4。代码摘录如下。
const int VectorWidth = 4; int nBlockWidth = VectorWidth; // Block width. int cntBlock = count / nBlockWidth; // Block count. int cntRem = count % nBlockWidth; // Remainder count.
C#是强类型的,会严格检查类型是否匹配,为了能使用Vector4,需要先将浮点数组转换为Vector4。这一步骤,一般叫做“Load”(加载)。
再加上相关变量的定义及初始化,“Load”部分的代码摘录如下。
Vector4 vrt = Vector4.Zero; // Vector result. int p; // Index for src data. int i; // Load. Vector4[] vsrc = new Vector4[cntBlock]; // Vector src. p = 0; for (i = 0; i < vsrc.Length; ++i) { vsrc[i] = new Vector4(src[p], src[p + 1], src[p + 2], src[p + 3]); p += VectorWidth; }
由于 Vector4 的构造函数不支持从数组里加载数据,仅支持“传递4个浮点变量”。于是上面的循环里,使用“传递4个浮点变量”的方式创建Vector4,然后放到vsrc数组中。vsrc数组中的每一项,就是一个块(Block)。
现在已经准备好了,可以用循环进行数据运算(Map阶段:分为4个组分别进行求和)了。代码摘录如下。
// Body. for (int j = 0; j < loops; ++j) { // Vector processs. for (i = 0; i < cntBlock; ++i) { // Equivalent to scalar model: rt += src[i]; vrt += vsrc[i]; // Add. } // Remainder processs. p = cntBlock * nBlockWidth; for (i = 0; i < cntRem; ++i) { rt += src[p + i]; } }
外循环loops的作用仅是为了方便测试,关键代码在2个内循环里:
vsrc[i]
的值,加到 vrt
里。vrt
是Vector4类型的变量,定义时已初始化为0。p = cntBlock * nBlockWidth
),然后使用传统循环写法,将剩余数据累积到 rt
里。由于Vector4重载了“+”运算法,所以可以很简单的使用“+=”运算符来做“相加并赋值”操作。代码写法,与传统的标量代码很相似,代码可读性高。
rt += src[i]; // 标量代码. vrt += vsrc[i]; // 向量代码.
最后我们需要将各组的结果加在一起(Reduce阶段)。代码摘录如下。
// Reduce. rt += vrt.X + vrt.Y + vrt.Z + vrt.W; return rt;
因 Vector4 暴露了 X、Y、Z、W 这4个成员,于是可以很方便的用“+”运算符,将结果加在一起。
该函数的完整代码如下。
private static float SumVector4(float[] src, int count, int loops) { float rt = 0; // Result. const int VectorWidth = 4; int nBlockWidth = VectorWidth; // Block width. int cntBlock = count / nBlockWidth; // Block count. int cntRem = count % nBlockWidth; // Remainder count. Vector4 vrt = Vector4.Zero; // Vector result. int p; // Index for src data. int i; // Load. Vector4[] vsrc = new Vector4[cntBlock]; // Vector src. p = 0; for (i = 0; i < vsrc.Length; ++i) { vsrc[i] = new Vector4(src[p], src[p + 1], src[p + 2], src[p + 3]); p += VectorWidth; } // Body. for (int j = 0; j < loops; ++j) { // Vector processs. for (i = 0; i < cntBlock; ++i) { // Equivalent to scalar model: rt += src[i]; vrt += vsrc[i]; // Add. } // Remainder processs. p = cntBlock * nBlockWidth; for (i = 0; i < cntRem; ++i) { rt += src[p + i]; } } // Reduce. rt += vrt.X + vrt.Y + vrt.Z + vrt.W; return rt; }
Vector<T>
)Vector4的痛点是——元素类型固定为float,且仅有4个元素。导致它的使用范围有限。
而 Vector<T>
解决了这2大痛点——
T
,可以支持各种数值型的基元类型,如 float、double、int 等。Vector<T>
的元素个数便越大。使用Vector<T>
在各种向量位宽的硬件上运行时,会以最大向量位宽来运行,而仅需只编写一套代码。以下是官方文档对 Vector<T>
的介绍。
`Vector<T>` 是一个不可变结构,表示指定数值类型的单个向量。 实例计数是固定的 `Vector<T>` ,但其上限取决于 CPU 寄存器。 它旨在用作向量大型算法的构建基块,因此不能直接用作任意长度向量或张量。 该 `Vector<T>` 结构为硬件加速提供支持。 本文中的术语 基元数值数据类型 是指 CPU 直接支持的数值数据类型,并具有可以操作这些数据类型的说明。 下表显示了哪些基元数值数据类型和操作组合使用内部指令来加快执行速度:
基元类型 | + |
- |
* |
/ |
---|---|---|---|---|
sbyte | 是 | 是 | 否 | 否 |
byte | 是 | 是 | 否 | 否 |
short | 是 | 是 | 是 | 否 |
ushort | 是 | 是 | 否 | 否 |
int | 是 | 是 | 是 | 否 |
uint | 是 | 是 | 否 | 否 |
long | 是 | 是 | 否 | 否 |
ulong | 是 | 是 | 否 | 否 |
float | 是 | 是 | 是 | 是 |
double | 是 | 是 | 是 | 是 |
有一个跟 Vector<T>
配合使用的静态类 Vector。它有2大作用——
Vector<T>
是否有硬件加速。应用程序应该检查该属性,仅在该属性为true,才使用 Vector<T>
。Vector<T>
只是重载了运算符,对于运算符无法办到的一些数学运算,可以去静态类 Vector 里找。Vector<T>
具有这些属性:
因为 Vector<T>
长度是与硬件有关的,所以每次在使用 Vector<T>
时,别忘了需要先从 Count 属性里的到元素数量。
一般来说——
Vector<T>
长度为256位,即32字节。此时能并行的处理 32个byte,或 16个short、8个int、4个long、8个float、4个double。Vector<T>
长度为128位,即16字节。此时能并行的处理 16个byte,或 8个short、4个int、2个long、4个float、2个double。Vector<T>
长度仍为128位,即16字节。Vector.IsHardwareAccelerated
为false,不建议使用。长度仍为128位,这可能是为了方便代码兼容性。这些情况的IsHardwareAccelerated、Count属性,一般为这些值——
// If the CPU is x86 and supports the AVX2 instruction set. Vector.IsHardwareAccelerated = true Vector<sbyte>.Count = 32 Vector<byte>.Count = 32 Vector<short>.Count = 16 Vector<ushort>.Count = 16 Vector<int>.Count = 8 Vector<uint>.Count = 8 Vector<long>.Count = 4 Vector<ulong>.Count = 4 Vector<float>.Count = 8 Vector<double>.Count = 4 // If the CPU is x86, the AVX2 instruction set is not supported, but the SSE2 instruction set is supported. Vector.IsHardwareAccelerated = true Vector<sbyte>.Count = 16 Vector<byte>.Count = 16 Vector<short>.Count = 8 Vector<ushort>.Count = 8 Vector<int>.Count = 4 Vector<uint>.Count = 4 Vector<long>.Count = 2 Vector<ulong>.Count = 2 Vector<float>.Count = 4 Vector<double>.Count = 2 // If the CPU does not support vector hardware acceleration. Vector.IsHardwareAccelerated = false Vector<sbyte>.Count = 16 Vector<byte>.Count = 16 Vector<short>.Count = 8 Vector<ushort>.Count = 8 Vector<int>.Count = 4 Vector<uint>.Count = 4 Vector<long>.Count = 2 Vector<ulong>.Count = 2 Vector<float>.Count = 4 Vector<double>.Count = 2
Vector<T>
编写浮点数组求和函数现在,我们使用 Vector<T>
,来编写浮点数组求和函数。
思路:先使用Count属性获得元素个数,然后按Count分组分别进行求和(即Map阶段),最后再将这些组的结果加起来(即Reduce阶段)。
根据上面的经验,我们可编写好 SumVectorT 函数。
private static float SumVectorT(float[] src, int count, int loops) { float rt = 0; // Result. int VectorWidth = Vector<float>.Count; // Block width. int nBlockWidth = VectorWidth; // Block width. int cntBlock = count / nBlockWidth; // Block count. int cntRem = count % nBlockWidth; // Remainder count. Vector<float> vrt = Vector<float>.Zero; // Vector result. int p; // Index for src data. int i; // Load. Vector<float>[] vsrc = new Vector<float>[cntBlock]; // Vector src. p = 0; for (i = 0; i < vsrc.Length; ++i) { vsrc[i] = new Vector<float>(src, p); p += VectorWidth; } // Body. for (int j = 0; j < loops; ++j) { // Vector processs. for (i = 0; i < cntBlock; ++i) { vrt += vsrc[i]; // Add. } // Remainder processs. p = cntBlock * nBlockWidth; for (i = 0; i < cntRem; ++i) { rt += src[p + i]; } } // Reduce. for (i = 0; i < VectorWidth; ++i) { rt += vrt[i]; } return rt; }
对比 SumVector4,除了将 Vector4 类型换为 Vector<T>
,还有这些变化——
Vector<float>.Count
属性来得到。Vector<T>
的构造函数支持数组参数。于是可以用 new Vector<float>(src, p)
,代替繁琐的 new Vector4(src[p], src[p + 1], src[p + 2], src[p + 3])
。Vector<T>
支持索引器(文档里的Item属性),可以使用索引器运算符 [],简洁的获取它的元素。于是在Reduce阶段,可以写个循环对结果进行累加。对于这2类向量类型,计划在以下平台进行测试——
开发环境选择VS2017。解决方案名的名称是“BenchmarkVector”。
因需要测试这么多平台,为了避免代码重复问题,故将主测试代码放到共享项目(Shared Project)里。随后各个平台的测试程序,可以引用该共享项目。
共享项目的名称是“BenchmarkVector”。其中的BenchmarkVectorDemo类,是主测试代码。
Benchmark是测试方法,代码如下。
/// <summary> /// Do Benchmark. /// </summary> /// <param name="tw">Output <see cref="TextWriter"/>.</param> /// <param name="indent">The indent.</param> public static void Benchmark(TextWriter tw, string indent) { if (null == tw) return; if (null == indent) indent = ""; //string indentNext = indent + "\t"; // init. int tickBegin, msUsed; double mFlops; // MFLOPS/s . double scale; float rt; const int count = 1024*4; const int loops = 1000 * 1000; //const int loops = 1; const double countMFlops = count * (double)loops / (1000.0 * 1000); float[] src = new float[count]; for(int i=0; i< count; ++i) { src[i] = i; } tw.WriteLine(indent + string.Format("Benchmark: \tcount={0}, loops={1}, countMFlops={2}", count, loops, countMFlops)); // SumBase. tickBegin = Environment.TickCount; rt = SumBase(src, count, loops); msUsed = Environment.TickCount - tickBegin; mFlops = countMFlops * 1000 / msUsed; tw.WriteLine(indent + string.Format("SumBase:\t{0}\t# msUsed={1}, MFLOPS/s={2}", rt, msUsed, mFlops)); double mFlopsBase = mFlops; // SumVector4. tickBegin = Environment.TickCount; rt = SumVector4(src, count, loops); msUsed = Environment.TickCount - tickBegin; mFlops = countMFlops * 1000 / msUsed; scale = mFlops / mFlopsBase; tw.WriteLine(indent + string.Format("SumVector4:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale)); // SumVectorT. tickBegin = Environment.TickCount; rt = SumVectorT(src, count, loops); msUsed = Environment.TickCount - tickBegin; mFlops = countMFlops * 1000 / msUsed; scale = mFlops / mFlopsBase; tw.WriteLine(indent + string.Format("SumVectorT:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale)); }
变量说明——
Environment.TickCount
,它以毫秒为单位.注:只有一级缓存是在CPU中的,一级缓存的读取需要1-4个时钟周期;二级缓存的读取需要10个左右的时钟周期;而三级缓存需要30-40个时钟周期,但是容量一次增大。
SIMD的数据规模大,一级缓存放不下。为了避免缓存速度干扰运算速度评测,故一般建议测试数据不要超过二级缓存的大小。
于是本范例的数据长度为 4K(1024*4),这是现代CPU的二级缓存大多能接受的长度。
例如在 .NET Core 2.0、lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz
、Windows 10 平台运行时,该测试函数的测试结果为:
Benchmark: count=4096, loops=1000000, countMFlops=4096 SumBase: 6.871948E+10 # msUsed=4937, MFLOPS/s=829.653635811221 SumVector4: 2.748779E+11 # msUsed=1234, MFLOPS/s=3319.2868719611, scale=4.00081037277147 SumVectorT: 5.497558E+11 # msUsed=625, MFLOPS/s=6553.6, scale=7.8992
输出信息说明——
性能提高倍数(scale),与理论值相符。因为SumVector4能同时处理4个浮点数,支持AVX2指令集时的SumVectorT能同时处理8个浮点数。
i5-8250U是2017年Intel发布的芯片,对于现在来说是老掉牙的配置了。C#代码不使用硬件加速时,是 0.829 GFLOPS/s 的浮点性能;使用 Vector<T>
并有硬件加速时,能达到 6.553 GFLOPS/s 的浮点性能,这样的指标已经很不错了。
而且我们的测试,只是对单核的测试,多核并行处理的浮点性能会更高。编写多线程程序便利用CPU多核,有兴趣的读者可以自己试试。
注意上面的测试结果中,各函数返回的累加结果是不同的。这是主要是因为是分组统计,循环次数(loops)比较多,导致超过单精度浮点数的精度范围。
若临时将loops改回1,会发现各函数的返回值是相同。故在开发时,可将loops改回1,便于检查程序是否有问题;带了测试时,再将loops改为较大的值。
因为这次测试了多个平台,不同平台的环境信息信息均不同。于是可以专门用一个函数来输出环境信息,源码如下。
/// <summary> /// Is release make. /// </summary> public static readonly bool IsRelease = #if DEBUG false #else true #endif ; /// <summary> /// Output Environment. /// </summary> /// <param name="tw">Output <see cref="TextWriter"/>.</param> /// <param name="indent">The indent.</param> public static void OutputEnvironment(TextWriter tw, string indent) { if (null == tw) return; if (null == indent) indent=""; //string indentNext = indent + "\t"; tw.WriteLine(indent + string.Format("IsRelease:\t{0}", IsRelease)); tw.WriteLine(indent + string.Format("EnvironmentVariable(PROCESSOR_IDENTIFIER):\t{0}", Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER"))); tw.WriteLine(indent + string.Format("Environment.ProcessorCount:\t{0}", Environment.ProcessorCount)); tw.WriteLine(indent + string.Format("Environment.Is64BitOperatingSystem:\t{0}", Environment.Is64BitOperatingSystem)); tw.WriteLine(indent + string.Format("Environment.Is64BitProcess:\t{0}", Environment.Is64BitProcess)); tw.WriteLine(indent + string.Format("Environment.OSVersion:\t{0}", Environment.OSVersion)); tw.WriteLine(indent + string.Format("Environment.Version:\t{0}", Environment.Version)); //tw.WriteLine(indent + string.Format("RuntimeEnvironment.GetSystemVersion:\t{0}", System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion())); // Same Environment.Version tw.WriteLine(indent + string.Format("RuntimeEnvironment.GetRuntimeDirectory:\t{0}", System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory())); #if (NET47 || NET462 || NET461 || NET46 || NET452 || NET451 || NET45 || NET40 || NET35 || NET20) || (NETSTANDARD1_0) #else tw.WriteLine(indent + string.Format("RuntimeInformation.FrameworkDescription:\t{0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription)); #endif tw.WriteLine(indent + string.Format("BitConverter.IsLittleEndian:\t{0}", BitConverter.IsLittleEndian)); tw.WriteLine(indent + string.Format("IntPtr.Size:\t{0}", IntPtr.Size)); tw.WriteLine(indent + string.Format("Vector.IsHardwareAccelerated:\t{0}", Vector.IsHardwareAccelerated)); tw.WriteLine(indent + string.Format("Vector<byte>.Count:\t{0}\t# {1}bit", Vector<byte>.Count, Vector<byte>.Count * sizeof(byte) * 8)); tw.WriteLine(indent + string.Format("Vector<float>.Count:\t{0}\t# {1}bit", Vector<float>.Count, Vector<float>.Count*sizeof(float)*8)); tw.WriteLine(indent + string.Format("Vector<double>.Count:\t{0}\t# {1}bit", Vector<double>.Count, Vector<double>.Count * sizeof(double) * 8)); Assembly assembly = typeof(Vector4).GetTypeInfo().Assembly; //tw.WriteLine(string.Format("Vector4.Assembly:\t{0}", assembly)); tw.WriteLine(string.Format("Vector4.Assembly.CodeBase:\t{0}", assembly.CodeBase)); assembly = typeof(Vector<float>).GetTypeInfo().Assembly; tw.WriteLine(string.Format("Vector<T>.Assembly.CodeBase:\t{0}", assembly.CodeBase)); }
例如在 .NET Core 2.0 平台运行时,会输出这些信息:
IsRelease: True EnvironmentVariable(PROCESSOR_IDENTIFIER): Intel64 Family 6 Model 142 Stepping 10, GenuineIntel Environment.ProcessorCount: 8 Environment.Is64BitOperatingSystem: True Environment.Is64BitProcess: True Environment.OSVersion: Microsoft Windows NT 10.0.19044.0 Environment.Version: 4.0.30319.42000 RuntimeEnvironment.GetRuntimeDirectory: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.0.9\ RuntimeInformation.FrameworkDescription: .NET Core 4.6.26614.01 BitConverter.IsLittleEndian: True IntPtr.Size: 8 Vector.IsHardwareAccelerated: True Vector<byte>.Count: 32 # 256bit Vector<float>.Count: 8 # 256bit Vector<double>.Count: 4 # 256bit Vector4.Assembly.CodeBase: file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll Vector<T>.Assembly.CodeBase: file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll
输出信息说明——
Vector<T>
是否支持硬件加速。Vector<byte>.Count
: Vector<byte>
的元素个数、总位数。Vector<float>.Count
: Vector<float>
的元素个数、总位数。Vector<double>.Count
: Vector<double>
的元素个数、总位数。Vector4.Assembly.CodeBase
: Vector4
所属程序集的路径。Vector<T>.Assembly.CodeBase
: Vector<T>
所属程序集的路径。下面是BenchmarkVectorDemo类的完整代码。
using System; using System.Collections.Generic; using System.IO; using System.Numerics; using System.Reflection; using System.Text; namespace BenchmarkVector { /// <summary> /// Benchmark Vector Demo /// </summary> static class BenchmarkVectorDemo { /// <summary> /// Is release make. /// </summary> public static readonly bool IsRelease = #if DEBUG false #else true #endif ; /// <summary> /// Output Environment. /// </summary> /// <param name="tw">Output <see cref="TextWriter"/>.</param> /// <param name="indent">The indent.</param> public static void OutputEnvironment(TextWriter tw, string indent) { if (null == tw) return; if (null == indent) indent=""; //string indentNext = indent + "\t"; tw.WriteLine(indent + string.Format("IsRelease:\t{0}", IsRelease)); tw.WriteLine(indent + string.Format("EnvironmentVariable(PROCESSOR_IDENTIFIER):\t{0}", Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER"))); tw.WriteLine(indent + string.Format("Environment.ProcessorCount:\t{0}", Environment.ProcessorCount)); tw.WriteLine(indent + string.Format("Environment.Is64BitOperatingSystem:\t{0}", Environment.Is64BitOperatingSystem)); tw.WriteLine(indent + string.Format("Environment.Is64BitProcess:\t{0}", Environment.Is64BitProcess)); tw.WriteLine(indent + string.Format("Environment.OSVersion:\t{0}", Environment.OSVersion)); tw.WriteLine(indent + string.Format("Environment.Version:\t{0}", Environment.Version)); //tw.WriteLine(indent + string.Format("RuntimeEnvironment.GetSystemVersion:\t{0}", System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion())); // Same Environment.Version tw.WriteLine(indent + string.Format("RuntimeEnvironment.GetRuntimeDirectory:\t{0}", System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory())); #if (NET47 || NET462 || NET461 || NET46 || NET452 || NET451 || NET45 || NET40 || NET35 || NET20) || (NETSTANDARD1_0) #else tw.WriteLine(indent + string.Format("RuntimeInformation.FrameworkDescription:\t{0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription)); #endif tw.WriteLine(indent + string.Format("BitConverter.IsLittleEndian:\t{0}", BitConverter.IsLittleEndian)); tw.WriteLine(indent + string.Format("IntPtr.Size:\t{0}", IntPtr.Size)); tw.WriteLine(indent + string.Format("Vector.IsHardwareAccelerated:\t{0}", Vector.IsHardwareAccelerated)); tw.WriteLine(indent + string.Format("Vector<byte>.Count:\t{0}\t# {1}bit", Vector<byte>.Count, Vector<byte>.Count * sizeof(byte) * 8)); tw.WriteLine(indent + string.Format("Vector<float>.Count:\t{0}\t# {1}bit", Vector<float>.Count, Vector<float>.Count*sizeof(float)*8)); tw.WriteLine(indent + string.Format("Vector<double>.Count:\t{0}\t# {1}bit", Vector<double>.Count, Vector<double>.Count * sizeof(double) * 8)); Assembly assembly = typeof(Vector4).GetTypeInfo().Assembly; //tw.WriteLine(string.Format("Vector4.Assembly:\t{0}", assembly)); tw.WriteLine(string.Format("Vector4.Assembly.CodeBase:\t{0}", assembly.CodeBase)); assembly = typeof(Vector<float>).GetTypeInfo().Assembly; tw.WriteLine(string.Format("Vector<T>.Assembly.CodeBase:\t{0}", assembly.CodeBase)); } /// <summary> /// Do Benchmark. /// </summary> /// <param name="tw">Output <see cref="TextWriter"/>.</param> /// <param name="indent">The indent.</param> public static void Benchmark(TextWriter tw, string indent) { if (null == tw) return; if (null == indent) indent = ""; //string indentNext = indent + "\t"; // init. int tickBegin, msUsed; double mFlops; // MFLOPS/s . double scale; float rt; const int count = 1024*4; const int loops = 1000 * 1000; //const int loops = 1; const double countMFlops = count * (double)loops / (1000.0 * 1000); float[] src = new float[count]; for(int i=0; i< count; ++i) { src[i] = i; } tw.WriteLine(indent + string.Format("Benchmark: \tcount={0}, loops={1}, countMFlops={2}", count, loops, countMFlops)); // SumBase. tickBegin = Environment.TickCount; rt = SumBase(src, count, loops); msUsed = Environment.TickCount - tickBegin; mFlops = countMFlops * 1000 / msUsed; tw.WriteLine(indent + string.Format("SumBase:\t{0}\t# msUsed={1}, MFLOPS/s={2}", rt, msUsed, mFlops)); double mFlopsBase = mFlops; // SumVector4. tickBegin = Environment.TickCount; rt = SumVector4(src, count, loops); msUsed = Environment.TickCount - tickBegin; mFlops = countMFlops * 1000 / msUsed; scale = mFlops / mFlopsBase; tw.WriteLine(indent + string.Format("SumVector4:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale)); // SumVectorT. tickBegin = Environment.TickCount; rt = SumVectorT(src, count, loops); msUsed = Environment.TickCount - tickBegin; mFlops = countMFlops * 1000 / msUsed; scale = mFlops / mFlopsBase; tw.WriteLine(indent + string.Format("SumVectorT:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale)); } /// <summary> /// Sum - base. /// </summary> /// <param name="src">Soure array.</param> /// <param name="count">Soure array count.</param> /// <param name="loops">Benchmark loops.</param> /// <returns>Return the sum value.</returns> private static float SumBase(float[] src, int count, int loops) { float rt = 0; // Result. for (int j=0; j< loops; ++j) { for(int i=0; i< count; ++i) { rt += src[i]; } } return rt; } /// <summary> /// Sum - Vector4. /// </summary> /// <param name="src">Soure array.</param> /// <param name="count">Soure array count.</param> /// <param name="loops">Benchmark loops.</param> /// <returns>Return the sum value.</returns> private static float SumVector4(float[] src, int count, int loops) { float rt = 0; // Result. const int VectorWidth = 4; int nBlockWidth = VectorWidth; // Block width. int cntBlock = count / nBlockWidth; // Block count. int cntRem = count % nBlockWidth; // Remainder count. Vector4 vrt = Vector4.Zero; // Vector result. int p; // Index for src data. int i; // Load. Vector4[] vsrc = new Vector4[cntBlock]; // Vector src. p = 0; for (i = 0; i < vsrc.Length; ++i) { vsrc[i] = new Vector4(src[p], src[p + 1], src[p + 2], src[p + 3]); p += VectorWidth; } // Body. for (int j = 0; j < loops; ++j) { // Vector processs. for (i = 0; i < cntBlock; ++i) { // Equivalent to scalar model: rt += src[i]; vrt += vsrc[i]; // Add. } // Remainder processs. p = cntBlock * nBlockWidth; for (i = 0; i < cntRem; ++i) { rt += src[p + i]; } } // Reduce. rt += vrt.X + vrt.Y + vrt.Z + vrt.W; return rt; } /// <summary> /// Sum - Vector<T>. /// </summary> /// <param name="src">Soure array.</param> /// <param name="count">Soure array count.</param> /// <param name="loops">Benchmark loops.</param> /// <returns>Return the sum value.</returns> private static float SumVectorT(float[] src, int count, int loops) { float rt = 0; // Result. int VectorWidth = Vector<float>.Count; // Block width. int nBlockWidth = VectorWidth; // Block width. int cntBlock = count / nBlockWidth; // Block count. int cntRem = count % nBlockWidth; // Remainder count. Vector<float> vrt = Vector<float>.Zero; // Vector result. int p; // Index for src data. int i; // Load. Vector<float>[] vsrc = new Vector<float>[cntBlock]; // Vector src. p = 0; for (i = 0; i < vsrc.Length; ++i) { vsrc[i] = new Vector<float>(src, p); p += VectorWidth; } // Body. for (int j = 0; j < loops; ++j) { // Vector processs. for (i = 0; i < cntBlock; ++i) { vrt += vsrc[i]; // Add. } // Remainder processs. p = cntBlock * nBlockWidth; for (i = 0; i < cntRem; ++i) { rt += src[p + i]; } } // Reduce. for (i = 0; i < VectorWidth; ++i) { rt += vrt[i]; } return rt; } } }
虽然从.NET Core 1.0开始就支持了向量类型,但本文考虑到需要与.NET Standard进行对比测试,故选择 .NET Core 2.0 比较好。
在解决方案里建立新项目“BenchmarkVectorCore20”,它是 .NET Core 2.0 控制台程序的项目。并让“BenchmarkVectorCore20”引用共享项目“BenchmarkVector”。
随后我们修改一下 Program 类的代码,加上调用测试函数的代码。代码如下。
using BenchmarkVector; using System; using System.IO; using System.Numerics; namespace BenchmarkVectorCore20 { class Program { static void Main(string[] args) { string indent = ""; TextWriter tw = Console.Out; tw.WriteLine("BenchmarkVectorCore20"); tw.WriteLine(); BenchmarkVectorDemo.OutputEnvironment(tw, indent); //tw.WriteLine(string.Format("Main-Vector4.Assembly.CodeBase:\t{0}", typeof(Vector4).Assembly.CodeBase)); tw.WriteLine(indent); BenchmarkVectorDemo.Benchmark(tw, indent); // Vector<int> a = Vector<int>.One; // a <<= 1; // CS0019 Operator '<<=' cannot be applied to operands of type 'Vector<int>' and 'int' } } }
注:上面代码还测试了一下 Vector<T>
是否支持移位运算符,发现目前不支持。从 .NET 的发展路线图来看,到了 .NET 7
,Vector<T>
会支持移位运算符。
在我的电脑(lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz
、Windows 10)上运行时,输出信息为:
BenchmarkVectorCore20 IsRelease: True EnvironmentVariable(PROCESSOR_IDENTIFIER): Intel64 Family 6 Model 142 Stepping 10, GenuineIntel Environment.ProcessorCount: 8 Environment.Is64BitOperatingSystem: True Environment.Is64BitProcess: True Environment.OSVersion: Microsoft Windows NT 10.0.19044.0 Environment.Version: 4.0.30319.42000 RuntimeEnvironment.GetRuntimeDirectory: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.0.9\ RuntimeInformation.FrameworkDescription: .NET Core 4.6.26614.01 BitConverter.IsLittleEndian: True IntPtr.Size: 8 Vector.IsHardwareAccelerated: True Vector<byte>.Count: 32 # 256bit Vector<float>.Count: 8 # 256bit Vector<double>.Count: 4 # 256bit Vector4.Assembly.CodeBase: file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll Vector<T>.Assembly.CodeBase: file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll Benchmark: count=4096, loops=1000000, countMFlops=4096 SumBase: 6.871948E+10 # msUsed=4937, MFLOPS/s=829.653635811221 SumVector4: 2.748779E+11 # msUsed=1234, MFLOPS/s=3319.2868719611, scale=4.00081037277147 SumVectorT: 5.497558E+11 # msUsed=625, MFLOPS/s=6553.6, scale=7.8992
官方文档上,仅 .NET Standard 2.1 才支持这2种向量类型。而.NET Standard 2.0应用最广泛,该怎么在.NET Standard 2.0上使用它们?
在nuget上找了一下,发现 System.Numerics.Vectors
包提供了这2类向量类型,且它支持 .NET Standard 2.0 平台。可以考虑引用该包。
此时有一个疑问——若引用的是nuget的System.Numerics.Vectors
包,向量类型是否仍会有硬件加速?
我们将建立一个测试程序,来检测这一点。
在解决方案里建立新项目“BenchmarkVectorLib”,它是 .NET Standard 2.0 类库项目。并让“BenchmarkVectorLib”引用共享项目“BenchmarkVector”。
随后建立一个 BenchmarkVectorUtil 类,用于暴露测试函数。代码如下。
using BenchmarkVector; using System; using System.IO; namespace BenchmarkVectorLib { /// <summary> /// Benchmark Vector Util /// </summary> public static class BenchmarkVectorUtil { /// <summary> /// Output Environment. /// </summary> /// <param name="tw">Output <see cref="TextWriter"/>.</param> /// <param name="indent">The indent.</param> public static void OutputEnvironment(TextWriter tw, string indent) { BenchmarkVectorDemo.OutputEnvironment(tw, indent); } /// <summary> /// Do Benchmark. /// </summary> /// <param name="tw">Output <see cref="TextWriter"/>.</param> /// <param name="indent">The indent.</param> public static void Benchmark(TextWriter tw, string indent) { BenchmarkVectorDemo.Benchmark(tw, indent); } } }
在解决方案里建立新项目“BenchmarkVectorCore20UseLib”,它是 .NET Core 2.0 控制台程序的项目。并让“BenchmarkVectorCore20”引用刚才建立的.NET Standard 2.0类库“BenchmarkVectorLib”。
随后我们修改一下 Program 类的代码,加上调用测试函数的代码。代码如下。
using BenchmarkVectorLib; using System; using System.IO; using System.Numerics; namespace BenchmarkVectorCore20UseLib { class Program { static void Main(string[] args) { string indent = ""; TextWriter tw = Console.Out; tw.WriteLine("BenchmarkVectorCore20UseLib"); tw.WriteLine(); BenchmarkVectorUtil.OutputEnvironment(tw, indent); //tw.WriteLine(string.Format("Main-Vector4.Assembly.CodeBase:\t{0}", typeof(Vector4).Assembly.CodeBase)); tw.WriteLine(indent); BenchmarkVectorUtil.Benchmark(tw, indent); } } }
在我的电脑(lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz
、Windows 10)上运行时,输出信息为:
BenchmarkVectorCore20UseLib IsRelease: True EnvironmentVariable(PROCESSOR_IDENTIFIER): Intel64 Family 6 Model 142 Stepping 10, GenuineIntel Environment.ProcessorCount: 8 Environment.Is64BitOperatingSystem: True Environment.Is64BitProcess: True Environment.OSVersion: Microsoft Windows NT 10.0.19044.0 Environment.Version: 4.0.30319.42000 RuntimeEnvironment.GetRuntimeDirectory: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.0.9\ RuntimeInformation.FrameworkDescription: .NET Core 4.6.26614.01 BitConverter.IsLittleEndian: True IntPtr.Size: 8 Vector.IsHardwareAccelerated: True Vector<byte>.Count: 32 # 256bit Vector<float>.Count: 8 # 256bit Vector<double>.Count: 4 # 256bit Vector4.Assembly.CodeBase: file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll Vector<T>.Assembly.CodeBase: file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll Benchmark: count=4096, loops=1000000, countMFlops=4096 SumBase: 6.871948E+10 # msUsed=4906, MFLOPS/s=834.896045658377 SumVector4: 2.748779E+11 # msUsed=1219, MFLOPS/s=3360.13125512715, scale=4.02461033634126 SumVectorT: 5.497558E+11 # msUsed=625, MFLOPS/s=6553.6, scale=7.8496
可以发现该程序测得的浮点性能,与BenchmarkVectorCore20的差不多,表示硬件加速生效了。于是可以解答之前的问题了——
System.Numerics.Vectors
包,向量类型仍会有硬件加速。官方文档上,.NET Framework 4.6 才支持大小固定的向量(如Vector4),且Vector<T>
未提到.NET Framework的支持版本。难道 .NET Framework用不了Vector<T>
吗? .NET Framework 4.5等版本时是否能使用它们?
在nuget上找了一下,发现 System.Numerics.Vectors
包支持.NET Framework,最早能支持 .NET Framework 4.5。
而且 System.Numerics.Vectors
包里提供了这2类向量类型。对比官方文档,此时有这些疑惑——
Vector<T>
未提到.NET Framework的支持版本,当 .NET Framework 下使用System.Numerics.Vectors
包时,是否有硬件加速?System.Numerics.Vectors
包时,是否有硬件加速?System.Numerics.Vectors
包时,Vector4是属于哪个程序集的?下面的测试程序,将回答以上问题。
在解决方案里建立新项目“BenchmarkVectorFw45”,它是 .NET Framework 4.5 控制台程序的项目。并让“BenchmarkVectorFw45”引用共享项目“BenchmarkVector”。
随后我们修改一下 Program 类的代码,加上调用测试函数的代码。代码如下:
using BenchmarkVector; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BenchmarkVectorFw45 { class Program { static void Main(string[] args) { string indent = ""; TextWriter tw = Console.Out; tw.WriteLine("BenchmarkVectorFw45"); tw.WriteLine(); BenchmarkVectorDemo.OutputEnvironment(tw, indent); //tw.WriteLine(string.Format("Main-Vector4.Assembly.CodeBase:\t{0}", typeof(Vector4).Assembly.CodeBase)); tw.WriteLine(indent); BenchmarkVectorDemo.Benchmark(tw, indent); } } }
在我的电脑(lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz
、Windows 10)上运行时,输出信息为:
BenchmarkVectorFw45 IsRelease: True EnvironmentVariable(PROCESSOR_IDENTIFIER): Intel64 Family 6 Model 142 Stepping 10, GenuineIntel Environment.ProcessorCount: 8 Environment.Is64BitOperatingSystem: True Environment.Is64BitProcess: True Environment.OSVersion: Microsoft Windows NT 6.2.9200.0 Environment.Version: 4.0.30319.42000 RuntimeEnvironment.GetRuntimeDirectory: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\ BitConverter.IsLittleEndian: True IntPtr.Size: 8 Vector.IsHardwareAccelerated: True Vector<byte>.Count: 32 # 256bit Vector<float>.Count: 8 # 256bit Vector<double>.Count: 4 # 256bit Vector4.Assembly.CodeBase: file:///E:/zylSelf/Code/cs/base/BenchmarkVector/BenchmarkVector1/BenchmarkVectorFw45/bin/Release/System.Numerics.Vectors.DLL Vector<T>.Assembly.CodeBase: file:///E:/zylSelf/Code/cs/base/BenchmarkVector/BenchmarkVector1/BenchmarkVectorFw45/bin/Release/System.Numerics.Vectors.DLL Benchmark: count=4096, loops=1000000, countMFlops=4096 SumBase: 6.871948E+10 # msUsed=4922, MFLOPS/s=832.182039821211 SumVector4: 2.748779E+11 # msUsed=1235, MFLOPS/s=3316.5991902834, scale=3.98542510121457 SumVectorT: 5.497558E+11 # msUsed=625, MFLOPS/s=6553.6, scale=7.8752
可以发现该程序测得的浮点性能,与BenchmarkVectorCore20的差不多,表示硬件加速生效了。于是可以解答之前的问题了——
Vector<T>
未提到.NET Framework的支持版本,当 .NET Framework 下使用System.Numerics.Vectors
包时,仍会有硬件加速。System.Numerics.Vectors
包时,仍会有硬件加速。这一点貌似有点奇怪——.NET Framework 4.5 标准库未提供向量类型,靠nuget引用第三方库使用向量类型,却也能得到硬件加速。
其实原因并不复杂,让向量类型获得硬件加速,其实是JIT(即时编译器)的工作。具体来说,是 RyuJIT 让向量类型获得了硬件加速的。
.NET Framework 4.5 标准库未提供向量类型,仅是编译无法通过的问题;通过nuget包,可以引入向量类型,解决了编译问题。随后.NET Framework 4.5程序运行时,若用了RyuJIT且硬件支持SIMD时,程序便能用上硬件加速。
官方文档里说.NET Framework 4.6才支持大小固定的向量(如Vector4),我们来测试一下吧。随后为了便于与 .NET Standard 2.0类库测试做对比,故选择了 .NET Framework 4.6.1。为了使项目名简单,故项目名为“BenchmarkVectorFw46”。
在解决方案里建立新项目“BenchmarkVectorFw46”,它是 .NET Framework 4.6.1 控制台程序的项目。并让“BenchmarkVectorFw46”引用共享项目“BenchmarkVector”。
随后我们修改一下 Program 类的代码,加上调用测试函数的代码。代码如下:
using BenchmarkVector; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using System.Text; using System.Threading.Tasks; namespace BenchmarkVectorFw46 { class Program { static void Main(string[] args) { string indent = ""; TextWriter tw = Console.Out; tw.WriteLine("BenchmarkVectorFw46"); tw.WriteLine(); BenchmarkVectorDemo.OutputEnvironment(tw, indent); //tw.WriteLine(string.Format("Main-Vector4.Assembly.CodeBase:\t{0}", typeof(Vector4).Assembly.CodeBase)); tw.WriteLine(indent); BenchmarkVectorDemo.Benchmark(tw, indent); } } }
在我的电脑(lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz
、Windows 10)上运行时,输出信息为:
BenchmarkVectorFw46 IsRelease: True EnvironmentVariable(PROCESSOR_IDENTIFIER): Intel64 Family 6 Model 142 Stepping 10, GenuineIntel Environment.ProcessorCount: 8 Environment.Is64BitOperatingSystem: True Environment.Is64BitProcess: True Environment.OSVersion: Microsoft Windows NT 6.2.9200.0 Environment.Version: 4.0.30319.42000 RuntimeEnvironment.GetRuntimeDirectory: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\ BitConverter.IsLittleEndian: True IntPtr.Size: 8 Vector.IsHardwareAccelerated: True Vector<byte>.Count: 32 # 256bit Vector<float>.Count: 8 # 256bit Vector<double>.Count: 4 # 256bit Vector4.Assembly.CodeBase: file:///C:/WINDOWS/Microsoft.Net/assembly/GAC_MSIL/System.Numerics/v4.0_4.0.0.0__b77a5c561934e089/System.Numerics.dll Vector<T>.Assembly.CodeBase: file:///E:/zylSelf/Code/cs/base/BenchmarkVector/BenchmarkVector1/BenchmarkVectorFw46/bin/Release/System.Numerics.Vectors.DLL Benchmark: count=4096, loops=1000000, countMFlops=4096 SumBase: 6.871948E+10 # msUsed=4922, MFLOPS/s=832.182039821211 SumVector4: 2.748779E+11 # msUsed=1218, MFLOPS/s=3362.88998357964, scale=4.04105090311987 SumVectorT: 5.497558E+11 # msUsed=609, MFLOPS/s=6725.77996715928, scale=8.08210180623974
可以发现该程序测得的浮点性能,与BenchmarkVectorCore20、BenchmarkVectorFw45的差不多,表示硬件加速生效了。
还可发现 Vector4 与 Vector<T>
的程序集不同,Vector4的程序集在系统目录,而Vector<T>
的程序集在程序目录。
这表示官方文档里说的“.NET Framework 4.6才支持大小固定的向量(如Vector4)”,原来是这样的——.NET Framework 4.6内置支持大小固定的向量(如Vector4),于是它们的程序集在系统目录;而 Vector<T>
不是内置支持,是引用nuget包,于是程序集在程序目录。
查了一下资料,.NET Framework 4.6 宣称不再使用已使用10年的JIT64,换成 RyuJIT x64。这可能就是 .NET Framework 4.6 官方文档说支持支持大小固定的向量(如Vector4)的原因。
而对于 Vector<T>
,可能是因为它的最新版设计为“只读结构体”(readonly struct)、且很多方法依赖 Span。只读结构体是 C# 7.2、VS2017.4 才支持的功能,比.NET Framework 4.6晚好几年,那时微软已宣布不再继续发展.NET Framework,转为统一的 .NET了。这可能就是 .NET Framework 里不包含 Vector<T>
的原因。
但由于 RyuJIT 是支持 Vector<T>
的,于是引用 nuget 包后,就能通过Vector<T>
使用硬件加速了。
现在我们来试试,在 .NET Framework 里测试 .NET Standard类库里的测试代码。
先前我们建立了 .NET Standard 2.0类库“BenchmarkVectorLib”,现在可以建立一个.NET Framework 控制台程序引用它,进行测试。
因 .NET Framework 4.6.1 是支持 .NET Standard 2.0 的最低版本。于是测试程序选择了 .NET Framework 4.6.1。
在解决方案里建立新项目“BenchmarkVectorFw46UseLib”,它是 .NET Framework 4.6.1 控制台程序的项目。并让“BenchmarkVectorFw46UseLib”引用.NET Standard 2.0类库“BenchmarkVectorLib”。
随后我们修改一下 Program 类的代码,加上调用测试函数的代码。代码如下:
using BenchmarkVectorLib; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BenchmarkVectorFw46UseLib { class Program { static void Main(string[] args) { string indent = ""; TextWriter tw = Console.Out; tw.WriteLine("BenchmarkVectorFw46UseLib"); tw.WriteLine(); BenchmarkVectorUtil.OutputEnvironment(tw, indent); tw.WriteLine(indent); BenchmarkVectorUtil.Benchmark(tw, indent); } } }
在我的电脑(lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz
、Windows 10)上运行时,输出信息为:
BenchmarkVectorFw46UseLib IsRelease: True EnvironmentVariable(PROCESSOR_IDENTIFIER): Intel64 Family 6 Model 142 Stepping 10, GenuineIntel Environment.ProcessorCount: 8 Environment.Is64BitOperatingSystem: True Environment.Is64BitProcess: True Environment.OSVersion: Microsoft Windows NT 6.2.9200.0 Environment.Version: 4.0.30319.42000 RuntimeEnvironment.GetRuntimeDirectory: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\ RuntimeInformation.FrameworkDescription: .NET Framework 4.8.4515.0 BitConverter.IsLittleEndian: True IntPtr.Size: 8 Vector.IsHardwareAccelerated: True Vector<byte>.Count: 32 # 256bit Vector<float>.Count: 8 # 256bit Vector<double>.Count: 4 # 256bit Vector4.Assembly.CodeBase: file:///C:/WINDOWS/Microsoft.Net/assembly/GAC_MSIL/System.Numerics/v4.0_4.0.0.0__b77a5c561934e089/System.Numerics.dll Vector<T>.Assembly.CodeBase: file:///E:/zylSelf/Code/cs/base/BenchmarkVector/BenchmarkVector1/BenchmarkVectorFw46UseLib/bin/Release/System.Numerics.Vectors.DLL Benchmark: count=4096, loops=1000000, countMFlops=4096 SumBase: 6.871948E+10 # msUsed=4922, MFLOPS/s=832.182039821211 SumVector4: 2.748779E+11 # msUsed=1234, MFLOPS/s=3319.2868719611, scale=3.98865478119935 SumVectorT: 5.497558E+11 # msUsed=625, MFLOPS/s=6553.6, scale=7.8752
可以发现该程序测得的浮点性能,与BenchmarkVectorFw46的差不多,表示硬件加速生效了。
测试环境统一是 lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz
、Windows 10。CPU硬件支持AVX2指令集,Vector<T>
理论上能同时处理4个 float。
用上面的测试程序,补上一些 Debug/Release、x86/x64 情况时的测试数据,再加上 .NET Core 3.0的测试数据(下一篇文章会详细说明),可汇总为一个表。为了便于阅读,省略了MFLOPS的小数,且scale保留3位小数,表格如下:
程序及配置 | 加速 | SumBase | SumVector4 | SumVectorT |
---|---|---|---|---|
BenchmarkVectorCore20, Debug, x64 | true | 368 | 946, scale=2.570 | 1659, scale=4.506 |
BenchmarkVectorCore20, Release, x64 | true | 829 | 3319, scale=4.001 | 6554, scale=7.899 |
BenchmarkVectorCore20UseLib, Release, x64 | true | 834 | 3360, scale=4.025 | 6554, scale=7.850 |
BenchmarkVectorFw45, Release, x64 | true | 832 | 3316, scale=3.985 | 6554, scale=7.875 |
BenchmarkVectorFw45, Release, x86 | false | 1111 | 883, scale=0.795 | 213, scale=0.192 |
BenchmarkVectorFw46, Release, x86 | false | 1101 | 880, scale=0.799 | 214, scale=0.194 |
BenchmarkVectorFw46, Release, x64 | true | 832 | 3363, scale=4.041 | 6726, scale=8.082 |
BenchmarkVectorFw46UseLib, Release, x64 | true | 832 | 3319, scale=3.989 | 6554, scale=7.875 |
BenchmarkVectorFw46UseLib, Release, x86 | false | 1111 | 883, scale=0.794 | 165, scale=0.149 |
BenchmarkVectorCore30, Debug, x86 | true | 368 | 822, scale=2.231 | 1560, scale=4.238 |
BenchmarkVectorCore30, Debug, x64 | true | 370 | 946, scale=2.559 | 1659, scale=4.487 |
BenchmarkVectorCore30, Release, x86 | true | 835 | 1481, scale=1.774 | 2648, scale=3.171 |
BenchmarkVectorCore30, Release, x64 | true | 829 | 3363, scale=4.054 | 6726, scale=8.108 |
注:“加速”指硬件加速。
从该表中可以看出——
Vector<T>
前,一定要检查是否支持硬件加速(Vector.IsHardwareAccelerated).最核心的使用经验就2条——
Vector<T>
的向量代码。在使用时别忘了检查是否支持硬件加速,若不支持,应退回到使用传统代码。采用以上策略,对于一项计算任务,最多只需开发2套代码(数学向量/传统、Vector<T>
)就行。
编译选项里的CPU平台,选“Any CPU”就行了。因为向量类型的硬件加速是由JIT处理的。当编译好的程序在 x86、x64等平台下运行时,JIT会使用该平台的向量硬件加速。
当然,若业务需要,也可以固定选择 x86、x64等平台。
源码地址——
https://github.com/zyl910/BenchmarkVector/tree/main/BenchmarkVector1
Vector<T>
结构》. https://docs.microsoft.com/zh-cn/dotnet/api/system.numerics.vector-1?view=netcore-1.0