ToB企服应用市场:ToB评测及商务社交产业平台

标题: C# 使用SIMD向量类型加速浮点数组求和运算(1):使用Vector4、Vector<T> [打印本页]

作者: 万有斥力    时间: 2022-9-16 17:18
标题: C# 使用SIMD向量类型加速浮点数组求和运算(1):使用Vector4、Vector<T>
作者:
目录

目录
一、缘由

从.NET Core 1.0开始,.NET里增加了2种向量类型——
到了 .NET Core 3.0,增加了内在函数(Intrinsics Functions)的支持,并增加了第3类向量类型——
3. 总位宽固定的向量(Vector of fixed total bit width)。例如 只读结构体 Vector64、Vector128、Vector256,及辅助的静态类 Vector64、Vector128、Vector256。
这3类向量类型,均能利用CPU硬件的SIMD(float Instruction Multiple Data,单指令多数据流)功能,来加速多媒体数据的处理。但是它们名称很接近,对于初学者来说容易混淆,而且应用场景稍有区别,本文致力于解决这些问题。
本章重点解说前2种向量类型(Vector4、Vector),第3种向量类型将由第2章来解说。
本章回答了这些问题——
二、使用向量类型

用高级语言处理数据时,一般是SISD(float instruction float data,单指令流单数据流)模型的,即一个语句只能处理一条数据。
而对于多媒体数据处理,任务的特点是运算相对简单,但是数据量很大,导致SISD模型的效率很低。
若使用SIMD模型的话,一次能处理多条数据,从而能成倍的提高性能。
.NET Core引入了向量数据类型,从而使C#(等.NET中语言)能使用SIMD加速数据的处理。
并不是所有的数据处理工作都适合SIMD处理。一般来说,需满足以下条件,才能充分利用SIMD加速——
对于以下情况,SIMD代码的性能会急剧下降,应尽量避免——
基于以上原因,发现最适合演示SIMD运算优势的,是做“浮点数组求和运算”。先在Map阶段处理并发的进行分组求和,最后在Reduce阶段将各组结果加起来。
2.1 基本算法

为了对比测试,先用传统的办法来编写一个“单精度浮点数组求和”的函数。
其实算法很简单,写个循环进行累加求和就行。代码如下。
  1. private static float SumBase(float[] src, int count) {
  2.     float rt = 0; // Result.
  3.     for(int i=0; i< count; ++i) {
  4.         rt += src[i];
  5.     }
  6.     return rt;
  7. }
复制代码
由于.NET向量类型的初始化会有一些开销,为了避免这些开销影响主循环的性能测试结果,于是需要将它们移到循环外。为了测试方便,求和函数可增加一个loops参数,它是测试次数,作为外循环。loops为1时,就是标准的变量求和;为其他值时,是多轮变量求和的累计值。由于浮点精度有限的问题,累计值可能与乘法结果不同。
为了能统一进行测试,于是基本算法也增加了 loops 参数。
  1. private static float SumBase(float[] src, int count, int loops) {
  2.     float rt = 0; // Result.
  3.     for (int j=0; j< loops; ++j) {
  4.         for(int i=0; i< count; ++i) {
  5.             rt += src[i];
  6.         }
  7.     }
  8.     return rt;
  9. }
复制代码
2.2 使用大小固定的向量(如 Vector4)

2.2.1 介绍

大小固定的向量类型,是以下3种结构体——
它们实际上是对数学(线性代数分支)里“向量”(Vector)的封装。命名规则为“'Vector' + [维数]”,例如 Vector2是数学里的“二维向量”、Vector3是数学里的“三维向量”、Vector4是数学里的“四维向量”。
于是这些类型,除了提供了常见的四则运算函数外,还提供了 向量长度(Length)、向量距离(Distance)、点积(Dot)、叉积(Cross) 等线性代数领域的函数。
它其中元素的数据类型,被限制为 float(32位单精度浮点值)。能用于常见单精度浮点运算场合。
使用这些向量类型时,JIT会尽可能的利用硬件加速,但是没有提供“是否有硬件加速”的标志。
这是因为不同的运算函数,在不同的CPU指令集里,有些能硬件加速,而另一些不能,很难通过简单的标志来区分。于是JIT仅是保证能尽可能的利用硬件加速,让使用者不用关心这些硬件细节。
一般来说,直接用这些类型的封装函数(如点积、叉积 运算等),比手工按数学定义编写的运算函数,效率更高。因为即使没有硬件加速时,这些封装好的函数是高水平的程序员编写的成熟代码。
Vector2、Vector3 比起 Vector4,元素个数要少一些,从数学定义上来看,理论运算量要少一些。
但是硬件的SIMD加速,大多是按“4元素并行处理”来设计。故很多时候,“Vector2、Vector3”运算性能与“Vector4”差不多。甚至在一些特别场合,比“Vector4”性能还低,因为对于硬件来说,可能会有多余的 忽略多余元素处理、数据转换 工作。
于是建议这样使用——
2.2.2 用Vector4编写浮点数组求和函数

现在,我们使用Vector4,来编写浮点数组求和函数。
思路:Vector4内有4个元素,于是可以分为4个组分别进行求和(即Map阶段),最后再将4个组的结果加起来(即Reduce阶段)。
我们先可建立SumVector4函数。根据之前所说(为了.NET向量类型的初始化),该函数还增加了1个loops参数。
  1. /// <summary>
  2. /// Sum - Vector4.
  3. /// </summary>
  4. /// <param name="src">Soure array.</param>
  5. /// <param name="count">Soure array count.</param>
  6. /// <param name="loops">Benchmark loops.</param>
  7. /// <returns>Return the sum value.</returns>
  8. private static float SumVector4(float[] src, int count, int loops) {
  9.     float rt = 0; // Result.
  10.     // TODO
  11.     return rt;
  12. }
复制代码
注意,数组长度可能不是4的整数倍。此时仅能对前面的、4的整数倍的数据用Vector4进行运算,而对于末尾剩余的元素,只能用传统办法来处理。
此时可利用“块”(Block)的概念来简化思路:每次内循环处理1个块,先对能凑齐整块的数据用Vector4进行循环处理(cntBlock),最后再对末尾剩余的元素(cntRem)按传统方式来处理。
Vector4有4个元素,于是块宽度(nBlockWidth)为4。代码摘录如下。
  1.     const int VectorWidth = 4;
  2.     int nBlockWidth = VectorWidth; // Block width.
  3.     int cntBlock = count / nBlockWidth; // Block count.
  4.     int cntRem = count % nBlockWidth; // Remainder count.
复制代码
C#是强类型的,会严格检查类型是否匹配,为了能使用Vector4,需要先将浮点数组转换为Vector4。这一步骤,一般叫做“Load”(加载)。
再加上相关变量的定义及初始化,“Load”部分的代码摘录如下。
  1.     Vector4 vrt = Vector4.Zero; // Vector result.
  2.     int p; // Index for src data.
  3.     int i;
  4.     // Load.
  5.     Vector4[] vsrc = new Vector4[cntBlock]; // Vector src.
  6.     p = 0;
  7.     for (i = 0; i < vsrc.Length; ++i) {
  8.         vsrc[i] = new Vector4(src[p], src[p + 1], src[p + 2], src[p + 3]);
  9.         p += VectorWidth;
  10.     }
复制代码
由于 Vector4 的构造函数不支持从数组里加载数据,仅支持“传递4个浮点变量”。于是上面的循环里,使用“传递4个浮点变量”的方式创建Vector4,然后放到vsrc数组中。vsrc数组中的每一项,就是一个块(Block)。
现在已经准备好了,可以用循环进行数据运算(Map阶段:分为4个组分别进行求和)了。代码摘录如下。
  1.     // Body.
  2.     for (int j = 0; j < loops; ++j) {
  3.         // Vector processs.
  4.         for (i = 0; i < cntBlock; ++i) {
  5.             // Equivalent to scalar model: rt += src[i];
  6.             vrt += vsrc[i]; // Add.
  7.         }
  8.         // Remainder processs.
  9.         p = cntBlock * nBlockWidth;
  10.         for (i = 0; i < cntRem; ++i) {
  11.             rt += src[p + i];
  12.         }
  13.     }
复制代码
外循环loops的作用仅是为了方便测试,关键代码在2个内循环里:
由于Vector4重载了“+”运算法,所以可以很简单的使用“+=”运算符来做“相加并赋值”操作。代码写法,与传统的标量代码很相似,代码可读性高。
  1. rt += src[i]; // 标量代码.
  2. vrt += vsrc[i]; // 向量代码.
复制代码
最后我们需要将各组的结果加在一起(Reduce阶段)。代码摘录如下。
  1.     // Reduce.
  2.     rt += vrt.X + vrt.Y + vrt.Z + vrt.W;
  3.     return rt;
复制代码
因 Vector4 暴露了 X、Y、Z、W 这4个成员,于是可以很方便的用“+”运算符,将结果加在一起。
该函数的完整代码如下。
  1. private static float SumVector4(float[] src, int count, int loops) {    float rt = 0; // Result.    const int VectorWidth = 4;
  2.     int nBlockWidth = VectorWidth; // Block width.
  3.     int cntBlock = count / nBlockWidth; // Block count.
  4.     int cntRem = count % nBlockWidth; // Remainder count.    Vector4 vrt = Vector4.Zero; // Vector result.
  5.     int p; // Index for src data.
  6.     int i;
  7.     // Load.
  8.     Vector4[] vsrc = new Vector4[cntBlock]; // Vector src.
  9.     p = 0;
  10.     for (i = 0; i < vsrc.Length; ++i) {
  11.         vsrc[i] = new Vector4(src[p], src[p + 1], src[p + 2], src[p + 3]);
  12.         p += VectorWidth;
  13.     }    // Body.
  14.     for (int j = 0; j < loops; ++j) {
  15.         // Vector processs.
  16.         for (i = 0; i < cntBlock; ++i) {
  17.             // Equivalent to scalar model: rt += src[i];
  18.             vrt += vsrc[i]; // Add.
  19.         }
  20.         // Remainder processs.
  21.         p = cntBlock * nBlockWidth;
  22.         for (i = 0; i < cntRem; ++i) {
  23.             rt += src[p + i];
  24.         }
  25.     }    // Reduce.
  26.     rt += vrt.X + vrt.Y + vrt.Z + vrt.W;
  27.     return rt;}
复制代码
2.3 使用大小与硬件相关的向量(如 Vector)

2.3.1 介绍

Vector4的痛点是——元素类型固定为float,且仅有4个元素。导致它的使用范围有限。
而 Vector 解决了这2大痛点——
以下是官方文档对 Vector 的介绍。
  1. `Vector<T>` 是一个不可变结构,表示指定数值类型的单个向量。 实例计数是固定的 `Vector<T>` ,但其上限取决于 CPU 寄存器。 它旨在用作向量大型算法的构建基块,因此不能直接用作任意长度向量或张量。
  2. 该 `Vector<T>` 结构为硬件加速提供支持。
  3. 本文中的术语 基元数值数据类型 是指 CPU 直接支持的数值数据类型,并具有可以操作这些数据类型的说明。 下表显示了哪些基元数值数据类型和操作组合使用内部指令来加快执行速度:
复制代码
基元类型+-*/sbyte是是否否byte是是否否short是是是否ushort是是否否int是是是否uint是是否否long是是否否ulong是是否否float是是是是double是是是是2.2.1.1 使用经验

有一个跟 Vector 配合使用的静态类 Vector。它有2大作用——
Vector 具有这些属性:
因为 Vector 长度是与硬件有关的,所以每次在使用 Vector 时,别忘了需要先从 Count 属性里的到元素数量。
一般来说——
这些情况的IsHardwareAccelerated、Count属性,一般为这些值——
  1. // If the CPU is x86 and supports the AVX2 instruction set.
  2. Vector.IsHardwareAccelerated = true
  3. Vector<sbyte>.Count = 32
  4. Vector<byte>.Count = 32
  5. Vector<short>.Count = 16
  6. Vector<ushort>.Count = 16
  7. Vector<int>.Count = 8
  8. Vector<uint>.Count = 8
  9. Vector<long>.Count = 4
  10. Vector<ulong>.Count = 4
  11. Vector<float>.Count = 8
  12. Vector<double>.Count = 4
  13. // If the CPU is x86, the AVX2 instruction set is not supported, but the SSE2 instruction set is supported.
  14. Vector.IsHardwareAccelerated = true
  15. Vector<sbyte>.Count = 16
  16. Vector<byte>.Count = 16
  17. Vector<short>.Count = 8
  18. Vector<ushort>.Count = 8
  19. Vector<int>.Count = 4
  20. Vector<uint>.Count = 4
  21. Vector<long>.Count = 2
  22. Vector<ulong>.Count = 2
  23. Vector<float>.Count = 4
  24. Vector<double>.Count = 2
  25. // If the CPU does not support vector hardware acceleration.
  26. Vector.IsHardwareAccelerated = false
  27. Vector<sbyte>.Count = 16
  28. Vector<byte>.Count = 16
  29. Vector<short>.Count = 8
  30. Vector<ushort>.Count = 8
  31. Vector<int>.Count = 4
  32. Vector<uint>.Count = 4
  33. Vector<long>.Count = 2
  34. Vector<ulong>.Count = 2
  35. Vector<float>.Count = 4
  36. Vector<double>.Count = 2
复制代码
2.3.2 用 Vector 编写浮点数组求和函数

现在,我们使用 Vector,来编写浮点数组求和函数。
思路:先使用Count属性获得元素个数,然后按Count分组分别进行求和(即Map阶段),最后再将这些组的结果加起来(即Reduce阶段)。
根据上面的经验,我们可编写好 SumVectorT 函数。
  1. private static float SumVectorT(float[] src, int count, int loops) {
  2.     float rt = 0; // Result.
  3.     int VectorWidth = Vector<float>.Count; // Block width.
  4.     int nBlockWidth = VectorWidth; // Block width.
  5.     int cntBlock = count / nBlockWidth; // Block count.
  6.     int cntRem = count % nBlockWidth; // Remainder count.
  7.     Vector<float> vrt = Vector<float>.Zero; // Vector result.
  8.     int p; // Index for src data.
  9.     int i;
  10.     // Load.
  11.     Vector<float>[] vsrc = new Vector<float>[cntBlock]; // Vector src.
  12.     p = 0;
  13.     for (i = 0; i < vsrc.Length; ++i) {
  14.         vsrc[i] = new Vector<float>(src, p);
  15.         p += VectorWidth;
  16.     }
  17.     // Body.
  18.     for (int j = 0; j < loops; ++j) {
  19.         // Vector processs.
  20.         for (i = 0; i < cntBlock; ++i) {
  21.             vrt += vsrc[i]; // Add.
  22.         }
  23.         // Remainder processs.
  24.         p = cntBlock * nBlockWidth;
  25.         for (i = 0; i < cntRem; ++i) {
  26.             rt += src[p + i];
  27.         }
  28.     }
  29.     // Reduce.
  30.     for (i = 0; i < VectorWidth; ++i) {
  31.         rt += vrt[i];
  32.     }
  33.     return rt;
  34. }
复制代码
对比 SumVector4,除了将 Vector4 类型换为 Vector,还有这些变化——
三、搭建测试程序

对于这2类向量类型,计划在以下平台进行测试——
开发环境选择VS2017。解决方案名的名称是“BenchmarkVector”。
因需要测试这么多平台,为了避免代码重复问题,故将主测试代码放到共享项目(Shared Project)里。随后各个平台的测试程序,可以引用该共享项目。
3.1 主测试代码(BenchmarkVectorDemo)

共享项目的名称是“BenchmarkVector”。其中的BenchmarkVectorDemo类,是主测试代码。
3.1.1 测试方法(Benchmark)

Benchmark是测试方法,代码如下。
  1. /// <summary>
  2. /// Do Benchmark.
  3. /// </summary>
  4. /// <param name="tw">Output <see cref="TextWriter"/>.</param>
  5. /// <param name="indent">The indent.</param>
  6. public static void Benchmark(TextWriter tw, string indent) {
  7.     if (null == tw) return;
  8.     if (null == indent) indent = "";
  9.     //string indentNext = indent + "\t";
  10.     // init.
  11.     int tickBegin, msUsed;
  12.     double mFlops; // MFLOPS/s .
  13.     double scale;
  14.     float rt;
  15.     const int count = 1024*4;
  16.     const int loops = 1000 * 1000;
  17.     //const int loops = 1;
  18.     const double countMFlops = count * (double)loops / (1000.0 * 1000);
  19.     float[] src = new float[count];
  20.     for(int i=0; i< count; ++i) {
  21.         src[i] = i;
  22.     }
  23.     tw.WriteLine(indent + string.Format("Benchmark: \tcount={0}, loops={1}, countMFlops={2}", count, loops, countMFlops));
  24.     // SumBase.
  25.     tickBegin = Environment.TickCount;
  26.     rt = SumBase(src, count, loops);
  27.     msUsed = Environment.TickCount - tickBegin;
  28.     mFlops = countMFlops * 1000 / msUsed;
  29.     tw.WriteLine(indent + string.Format("SumBase:\t{0}\t# msUsed={1}, MFLOPS/s={2}", rt, msUsed, mFlops));
  30.     double mFlopsBase = mFlops;
  31.     // SumVector4.
  32.     tickBegin = Environment.TickCount;
  33.     rt = SumVector4(src, count, loops);
  34.     msUsed = Environment.TickCount - tickBegin;
  35.     mFlops = countMFlops * 1000 / msUsed;
  36.     scale = mFlops / mFlopsBase;
  37.     tw.WriteLine(indent + string.Format("SumVector4:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale));
  38.     // SumVectorT.
  39.     tickBegin = Environment.TickCount;
  40.     rt = SumVectorT(src, count, loops);
  41.     msUsed = Environment.TickCount - tickBegin;
  42.     mFlops = countMFlops * 1000 / msUsed;
  43.     scale = mFlops / mFlopsBase;
  44.     tw.WriteLine(indent + string.Format("SumVectorT:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale));
  45. }
复制代码
变量说明——
注:只有一级缓存是在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 平台运行时,该测试函数的测试结果为:
  1. Benchmark:      count=4096, loops=1000000, countMFlops=4096
  2. SumBase:        6.871948E+10    # msUsed=4937, MFLOPS/s=829.653635811221
  3. SumVector4:     2.748779E+11    # msUsed=1234, MFLOPS/s=3319.2868719611, scale=4.00081037277147
  4. 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 并有硬件加速时,能达到 6.553 GFLOPS/s 的浮点性能,这样的指标已经很不错了。
而且我们的测试,只是对单核的测试,多核并行处理的浮点性能会更高。编写多线程程序便利用CPU多核,有兴趣的读者可以自己试试。
注意上面的测试结果中,各函数返回的累加结果是不同的。这是主要是因为是分组统计,循环次数(loops)比较多,导致超过单精度浮点数的精度范围。
若临时将loops改回1,会发现各函数的返回值是相同。故在开发时,可将loops改回1,便于检查程序是否有问题;带了测试时,再将loops改为较大的值。
3.1.2 输出环境信息(OutputEnvironment)

因为这次测试了多个平台,不同平台的环境信息信息均不同。于是可以专门用一个函数来输出环境信息,源码如下。
  1. /// <summary>
  2. /// Is release make.
  3. /// </summary>
  4. public static readonly bool IsRelease =
  5. #if DEBUG
  6.     false
  7. #else
  8.     true
  9. #endif
  10. ;
  11. /// <summary>
  12. /// Output Environment.
  13. /// </summary>
  14. /// <param name="tw">Output <see cref="TextWriter"/>.</param>
  15. /// <param name="indent">The indent.</param>
  16. public static void OutputEnvironment(TextWriter tw, string indent) {
  17.     if (null == tw) return;
  18.     if (null == indent) indent="";
  19.     //string indentNext = indent + "\t";
  20.     tw.WriteLine(indent + string.Format("IsRelease:\t{0}", IsRelease));
  21.     tw.WriteLine(indent + string.Format("EnvironmentVariable(PROCESSOR_IDENTIFIER):\t{0}", Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER")));
  22.     tw.WriteLine(indent + string.Format("Environment.ProcessorCount:\t{0}", Environment.ProcessorCount));
  23.     tw.WriteLine(indent + string.Format("Environment.Is64BitOperatingSystem:\t{0}", Environment.Is64BitOperatingSystem));
  24.     tw.WriteLine(indent + string.Format("Environment.Is64BitProcess:\t{0}", Environment.Is64BitProcess));
  25.     tw.WriteLine(indent + string.Format("Environment.OSVersion:\t{0}", Environment.OSVersion));
  26.     tw.WriteLine(indent + string.Format("Environment.Version:\t{0}", Environment.Version));
  27.     //tw.WriteLine(indent + string.Format("RuntimeEnvironment.GetSystemVersion:\t{0}", System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion())); // Same Environment.Version
  28.     tw.WriteLine(indent + string.Format("RuntimeEnvironment.GetRuntimeDirectory:\t{0}", System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()));
  29. #if (NET47 || NET462 || NET461 || NET46 || NET452 || NET451 || NET45 || NET40 || NET35 || NET20) || (NETSTANDARD1_0)
  30. #else
  31.     tw.WriteLine(indent + string.Format("RuntimeInformation.FrameworkDescription:\t{0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription));
  32. #endif
  33.     tw.WriteLine(indent + string.Format("BitConverter.IsLittleEndian:\t{0}", BitConverter.IsLittleEndian));
  34.     tw.WriteLine(indent + string.Format("IntPtr.Size:\t{0}", IntPtr.Size));
  35.     tw.WriteLine(indent + string.Format("Vector.IsHardwareAccelerated:\t{0}", Vector.IsHardwareAccelerated));
  36.     tw.WriteLine(indent + string.Format("Vector<byte>.Count:\t{0}\t# {1}bit", Vector<byte>.Count, Vector<byte>.Count * sizeof(byte) * 8));
  37.     tw.WriteLine(indent + string.Format("Vector<float>.Count:\t{0}\t# {1}bit", Vector<float>.Count, Vector<float>.Count*sizeof(float)*8));
  38.     tw.WriteLine(indent + string.Format("Vector<double>.Count:\t{0}\t# {1}bit", Vector<double>.Count, Vector<double>.Count * sizeof(double) * 8));
  39.     Assembly assembly = typeof(Vector4).GetTypeInfo().Assembly;
  40.     //tw.WriteLine(string.Format("Vector4.Assembly:\t{0}", assembly));
  41.     tw.WriteLine(string.Format("Vector4.Assembly.CodeBase:\t{0}", assembly.CodeBase));
  42.     assembly = typeof(Vector<float>).GetTypeInfo().Assembly;
  43.     tw.WriteLine(string.Format("Vector<T>.Assembly.CodeBase:\t{0}", assembly.CodeBase));
  44. }
复制代码
例如在 .NET Core 2.0 平台运行时,会输出这些信息:
  1. IsRelease:      True
  2. EnvironmentVariable(PROCESSOR_IDENTIFIER):      Intel64 Family 6 Model 142 Stepping 10, GenuineIntel
  3. Environment.ProcessorCount:     8
  4. Environment.Is64BitOperatingSystem:     True
  5. Environment.Is64BitProcess:     True
  6. Environment.OSVersion:  Microsoft Windows NT 10.0.19044.0
  7. Environment.Version:    4.0.30319.42000
  8. RuntimeEnvironment.GetRuntimeDirectory: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.0.9\
  9. RuntimeInformation.FrameworkDescription:        .NET Core 4.6.26614.01
  10. BitConverter.IsLittleEndian:    True
  11. IntPtr.Size:    8
  12. Vector.IsHardwareAccelerated:   True
  13. Vector<byte>.Count:     32      # 256bit
  14. Vector<float>.Count:    8       # 256bit
  15. Vector<double>.Count:   4       # 256bit
  16. Vector4.Assembly.CodeBase:      file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll
  17. Vector<T>.Assembly.CodeBase:    file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/2.0.9/System.Numerics.Vectors.dll
复制代码
输出信息说明——
3.1.3 汇总

下面是BenchmarkVectorDemo类的完整代码。
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Numerics;
  5. using System.Reflection;
  6. using System.Text;
  7. namespace BenchmarkVector {
  8.     /// <summary>
  9.     /// Benchmark Vector Demo
  10.     /// </summary>
  11.     static class BenchmarkVectorDemo {
  12.         /// <summary>
  13.         /// Is release make.
  14.         /// </summary>
  15.         public static readonly bool IsRelease =
  16. #if DEBUG
  17.             false
  18. #else
  19.             true
  20. #endif
  21.         ;
  22.         /// <summary>
  23.         /// Output Environment.
  24.         /// </summary>
  25.         /// <param name="tw">Output <see cref="TextWriter"/>.</param>
  26.         /// <param name="indent">The indent.</param>
  27.         public static void OutputEnvironment(TextWriter tw, string indent) {
  28.             if (null == tw) return;
  29.             if (null == indent) indent="";
  30.             //string indentNext = indent + "\t";
  31.             tw.WriteLine(indent + string.Format("IsRelease:\t{0}", IsRelease));
  32.             tw.WriteLine(indent + string.Format("EnvironmentVariable(PROCESSOR_IDENTIFIER):\t{0}", Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER")));
  33.             tw.WriteLine(indent + string.Format("Environment.ProcessorCount:\t{0}", Environment.ProcessorCount));
  34.             tw.WriteLine(indent + string.Format("Environment.Is64BitOperatingSystem:\t{0}", Environment.Is64BitOperatingSystem));
  35.             tw.WriteLine(indent + string.Format("Environment.Is64BitProcess:\t{0}", Environment.Is64BitProcess));
  36.             tw.WriteLine(indent + string.Format("Environment.OSVersion:\t{0}", Environment.OSVersion));
  37.             tw.WriteLine(indent + string.Format("Environment.Version:\t{0}", Environment.Version));
  38.             //tw.WriteLine(indent + string.Format("RuntimeEnvironment.GetSystemVersion:\t{0}", System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion())); // Same Environment.Version
  39.             tw.WriteLine(indent + string.Format("RuntimeEnvironment.GetRuntimeDirectory:\t{0}", System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()));
  40. #if (NET47 || NET462 || NET461 || NET46 || NET452 || NET451 || NET45 || NET40 || NET35 || NET20) || (NETSTANDARD1_0)
  41. #else
  42.             tw.WriteLine(indent + string.Format("RuntimeInformation.FrameworkDescription:\t{0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription));
  43. #endif
  44.             tw.WriteLine(indent + string.Format("BitConverter.IsLittleEndian:\t{0}", BitConverter.IsLittleEndian));
  45.             tw.WriteLine(indent + string.Format("IntPtr.Size:\t{0}", IntPtr.Size));
  46.             tw.WriteLine(indent + string.Format("Vector.IsHardwareAccelerated:\t{0}", Vector.IsHardwareAccelerated));
  47.             tw.WriteLine(indent + string.Format("Vector<byte>.Count:\t{0}\t# {1}bit", Vector<byte>.Count, Vector<byte>.Count * sizeof(byte) * 8));
  48.             tw.WriteLine(indent + string.Format("Vector<float>.Count:\t{0}\t# {1}bit", Vector<float>.Count, Vector<float>.Count*sizeof(float)*8));
  49.             tw.WriteLine(indent + string.Format("Vector<double>.Count:\t{0}\t# {1}bit", Vector<double>.Count, Vector<double>.Count * sizeof(double) * 8));
  50.             Assembly assembly = typeof(Vector4).GetTypeInfo().Assembly;
  51.             //tw.WriteLine(string.Format("Vector4.Assembly:\t{0}", assembly));
  52.             tw.WriteLine(string.Format("Vector4.Assembly.CodeBase:\t{0}", assembly.CodeBase));
  53.             assembly = typeof(Vector<float>).GetTypeInfo().Assembly;
  54.             tw.WriteLine(string.Format("Vector<T>.Assembly.CodeBase:\t{0}", assembly.CodeBase));
  55.         }
  56.         /// <summary>
  57.         /// Do Benchmark.
  58.         /// </summary>
  59.         /// <param name="tw">Output <see cref="TextWriter"/>.</param>
  60.         /// <param name="indent">The indent.</param>
  61.         public static void Benchmark(TextWriter tw, string indent) {
  62.             if (null == tw) return;
  63.             if (null == indent) indent = "";
  64.             //string indentNext = indent + "\t";
  65.             // init.
  66.             int tickBegin, msUsed;
  67.             double mFlops; // MFLOPS/s .
  68.             double scale;
  69.             float rt;
  70.             const int count = 1024*4;
  71.             const int loops = 1000 * 1000;
  72.             //const int loops = 1;
  73.             const double countMFlops = count * (double)loops / (1000.0 * 1000);
  74.             float[] src = new float[count];
  75.             for(int i=0; i< count; ++i) {
  76.                 src[i] = i;
  77.             }
  78.             tw.WriteLine(indent + string.Format("Benchmark: \tcount={0}, loops={1}, countMFlops={2}", count, loops, countMFlops));
  79.             // SumBase.
  80.             tickBegin = Environment.TickCount;
  81.             rt = SumBase(src, count, loops);
  82.             msUsed = Environment.TickCount - tickBegin;
  83.             mFlops = countMFlops * 1000 / msUsed;
  84.             tw.WriteLine(indent + string.Format("SumBase:\t{0}\t# msUsed={1}, MFLOPS/s={2}", rt, msUsed, mFlops));
  85.             double mFlopsBase = mFlops;
  86.             // SumVector4.
  87.             tickBegin = Environment.TickCount;
  88.             rt = SumVector4(src, count, loops);
  89.             msUsed = Environment.TickCount - tickBegin;
  90.             mFlops = countMFlops * 1000 / msUsed;
  91.             scale = mFlops / mFlopsBase;
  92.             tw.WriteLine(indent + string.Format("SumVector4:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale));
  93.             // SumVectorT.
  94.             tickBegin = Environment.TickCount;
  95.             rt = SumVectorT(src, count, loops);
  96.             msUsed = Environment.TickCount - tickBegin;
  97.             mFlops = countMFlops * 1000 / msUsed;
  98.             scale = mFlops / mFlopsBase;
  99.             tw.WriteLine(indent + string.Format("SumVectorT:\t{0}\t# msUsed={1}, MFLOPS/s={2}, scale={3}", rt, msUsed, mFlops, scale));
  100.         }
  101.         /// <summary>
  102.         /// Sum - base.
  103.         /// </summary>
  104.         /// <param name="src">Soure array.</param>
  105.         /// <param name="count">Soure array count.</param>
  106.         /// <param name="loops">Benchmark loops.</param>
  107.         /// <returns>Return the sum value.</returns>
  108.         private static float SumBase(float[] src, int count, int loops) {
  109.             float rt = 0; // Result.
  110.             for (int j=0; j< loops; ++j) {
  111.                 for(int i=0; i< count; ++i) {
  112.                     rt += src[i];
  113.                 }
  114.             }
  115.             return rt;
  116.         }
  117.         /// <summary>
  118.         /// Sum - Vector4.
  119.         /// </summary>
  120.         /// <param name="src">Soure array.</param>
  121.         /// <param name="count">Soure array count.</param>
  122.         /// <param name="loops">Benchmark loops.</param>
  123.         /// <returns>Return the sum value.</returns>
  124.         private static float SumVector4(float[] src, int count, int loops) {
  125.             float rt = 0; // Result.
  126.             const int VectorWidth = 4;
  127.             int nBlockWidth = VectorWidth; // Block width.
  128.             int cntBlock = count / nBlockWidth; // Block count.
  129.             int cntRem = count % nBlockWidth; // Remainder count.
  130.             Vector4 vrt = Vector4.Zero; // Vector result.
  131.             int p; // Index for src data.
  132.             int i;
  133.             // Load.
  134.             Vector4[] vsrc = new Vector4[cntBlock]; // Vector src.
  135.             p = 0;
  136.             for (i = 0; i < vsrc.Length; ++i) {
  137.                 vsrc[i] = new Vector4(src[p], src[p + 1], src[p + 2], src[p + 3]);
  138.                 p += VectorWidth;
  139.             }
  140.             // Body.
  141.             for (int j = 0; j < loops; ++j) {
  142.                 // Vector processs.
  143.                 for (i = 0; i < cntBlock; ++i) {
  144.                     // Equivalent to scalar model: rt += src[i];
  145.                     vrt += vsrc[i]; // Add.
  146.                 }
  147.                 // Remainder processs.
  148.                 p = cntBlock * nBlockWidth;
  149.                 for (i = 0; i < cntRem; ++i) {
  150.                     rt += src[p + i];
  151.                 }
  152.             }
  153.             // Reduce.
  154.             rt += vrt.X + vrt.Y + vrt.Z + vrt.W;
  155.             return rt;
  156.         }
  157.         /// <summary>
  158.         /// Sum - Vector<T>.
  159.         /// </summary>
  160.         /// <param name="src">Soure array.</param>
  161.         /// <param name="count">Soure array count.</param>
  162.         /// <param name="loops">Benchmark loops.</param>
  163.         /// <returns>Return the sum value.</returns>
  164.         private static float SumVectorT(float[] src, int count, int loops) {
  165.             float rt = 0; // Result.
  166.             int VectorWidth = Vector<float>.Count; // Block width.
  167.             int nBlockWidth = VectorWidth; // Block width.
  168.             int cntBlock = count / nBlockWidth; // Block count.
  169.             int cntRem = count % nBlockWidth; // Remainder count.
  170.             Vector<float> vrt = Vector<float>.Zero; // Vector result.
  171.             int p; // Index for src data.
  172.             int i;
  173.             // Load.
  174.             Vector<float>[] vsrc = new Vector<float>[cntBlock]; // Vector src.
  175.             p = 0;
  176.             for (i = 0; i < vsrc.Length; ++i) {
  177.                 vsrc[i] = new Vector<float>(src, p);
  178.                 p += VectorWidth;
  179.             }
  180.             // Body.
  181.             for (int j = 0; j < loops; ++j) {
  182.                 // Vector processs.
  183.                 for (i = 0; i < cntBlock; ++i) {
  184.                     vrt += vsrc[i]; // Add.
  185.                 }
  186.                 // Remainder processs.
  187.                 p = cntBlock * nBlockWidth;
  188.                 for (i = 0; i < cntRem; ++i) {
  189.                     rt += src[p + i];
  190.                 }
  191.             }
  192.             // Reduce.
  193.             for (i = 0; i < VectorWidth; ++i) {
  194.                 rt += vrt[i];
  195.             }
  196.             return rt;
  197.         }
  198.     }
  199. }
复制代码
3.2 在 .NET Core 里进行测试

3.2.1 搭建测试项目(BenchmarkVectorCore20)

虽然从.NET Core 1.0开始就支持了向量类型,但本文考虑到需要与.NET Standard进行对比测试,故选择 .NET Core 2.0 比较好。
在解决方案里建立新项目“BenchmarkVectorCore20”,它是 .NET Core 2.0 控制台程序的项目。并让“BenchmarkVectorCore20”引用共享项目“BenchmarkVector”。
随后我们修改一下 Program 类的代码,加上调用测试函数的代码。代码如下。
[code]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 a = Vector.One;            // a




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4