C# .Net SIMD System.Numerics.Vector4 比循环慢
C# .Net SIMD System.Numerics.Vector4 slower than loop
我编写了以下代码来试验 System.Numerics.Vector4 并评估性能增益:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Numerics;
namespace ConsoleApp8
{
class Program
{
static void Main(string[] args)
{
const int N = 100000000;
long ticks_start, ticks_end;
ticks_start = DateTime.Now.Ticks;
float[] a = { 10, 10, 10, 10 };
float[] b = new float[4];
for (int i = 0; i < N; i++)
for (int j = 0; j < 4; j++)
b[j] = a[j] + a[j];
ticks_end = DateTime.Now.Ticks;
Console.WriteLine($"Done in {ticks_end - ticks_start} ticks");
ticks_start = DateTime.Now.Ticks;
Vector4 result;
Vector4 v = new Vector4();
for (int i = 0; i < N; i++)
{
v.W = a[0];
v.X = a[1];
v.Y = a[2];
v.Z = a[3];
result = Vector4.Add(v, v);
b[0] = result.W;
b[1] = result.X;
b[2] = result.Y;
b[3] = result.Z;
}
ticks_end = DateTime.Now.Ticks;
Console.WriteLine($"Done in {ticks_end - ticks_start} ticks");
Console.ReadKey();
}
}
}
输出为:
Done in 14257591 ticks
Done in 18591588 ticks
所以看起来我们使用 Vector4 没有任何优势。 Add 方法 returns Vector4 的新实例。有没有办法改变其中一个向量以避免内存分配影响?或者也许有另一种方法来做事?
我还没有真正对其进行基准测试,但是在内部循环中:
for (int i = 0; i < N; i++)
{
v.W = a[0];
v.X = a[1];
v.Y = a[2];
v.Z = a[3];
result = Vector4.Add(v, v);
b[0] = result.W;
b[1] = result.X;
b[2] = result.Y;
b[3] = result.Z;
}
更等同于:
float[] a = { 10, 10, 10, 10 };
float[] b = new float[4];
float[] v = new float[4];
float[] result = new float[4];
for (int i = 0; i < N; i++)
{
v[0] = a[0];
v[1] = a[1];
v[2] = a[2];
v[3] = a[3];
result[0] = v[0] + v[0];
result[1] = v[1] + v[1];
result[2] = v[2] + v[2];
result[3] = v[3] + v[3];
b[0] = result[0];
b[1] = result[1];
b[2] = result[2];
b[3] = result[3];
}
比你写的要多(你正在做 4 个赋值 - 赋值给 v
-,然后是加法,然后是另外 4 个赋值 - 赋值 result
到 b
-,你只是跳过你的数组操作)。
我刚刚使用秒表在 linqpad 上对其进行了测试(这绝不是基准测试),如果你这样做,数组添加(即使总和展开)比 Vector4
(以非常小的幅度)。
根据评论区的各种建议,在release模式下测试,向量化版本明显比数组版本快。
发布此答案是为了提高可见度。感谢 Jeroen van Langen、phuclv 和 Alexei Levenkov。
感谢他们花时间和耐心回答这个问题。
可以在不使用分配的情况下改进代码。 MemoryMarshal.Cast
从字面上创建一个 Span
指向与数组相同的内存区域。 float
数组的大小可以是任意的,它将产生大小为 a.Length / 4
或 Vector4
的 Span
。
结果相同。当您写入输出 Span
时,数据将直接写入输出数组内存区域。
static void Main(string[] args)
{
const int N = 100000000;
long ticks_start, ticks_end;
ticks_start = DateTime.Now.Ticks;
float[] a = { 10, 10, 10, 10 };
float[] b = new float[4];
for (int i = 0; i < N; i++)
for (int j = 0; j < 4; j++)
b[j] = a[j] + a[j];
ticks_end = DateTime.Now.Ticks;
Console.WriteLine(string.Join(", ", b));
Console.WriteLine($"Done in {ticks_end - ticks_start} ticks");
Console.WriteLine("Clearing...");
Array.Clear(b, 0, b.Length);
Console.WriteLine(string.Join(", ", b));
ticks_start = DateTime.Now.Ticks;
ReadOnlySpan<Vector4> v = MemoryMarshal.Cast<float, Vector4>(a);
Span<Vector4> r = MemoryMarshal.Cast<float, Vector4>(b);
for (int i = 0; i < N; i++)
{
r[0] = Vector4.Add(v[0], v[0]);
}
ticks_end = DateTime.Now.Ticks;
Console.WriteLine(string.Join(", ", b));
Console.WriteLine($"Done in {ticks_end - ticks_start} ticks");
Console.ReadKey();
}
运行 发布版本。
20, 20, 20, 20
Done in 2172911 ticks
Clearing...
0, 0, 0, 0
20, 20, 20, 20
Done in 649113 ticks
看起来快了 3.3 倍。
我编写了以下代码来试验 System.Numerics.Vector4 并评估性能增益:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Numerics;
namespace ConsoleApp8
{
class Program
{
static void Main(string[] args)
{
const int N = 100000000;
long ticks_start, ticks_end;
ticks_start = DateTime.Now.Ticks;
float[] a = { 10, 10, 10, 10 };
float[] b = new float[4];
for (int i = 0; i < N; i++)
for (int j = 0; j < 4; j++)
b[j] = a[j] + a[j];
ticks_end = DateTime.Now.Ticks;
Console.WriteLine($"Done in {ticks_end - ticks_start} ticks");
ticks_start = DateTime.Now.Ticks;
Vector4 result;
Vector4 v = new Vector4();
for (int i = 0; i < N; i++)
{
v.W = a[0];
v.X = a[1];
v.Y = a[2];
v.Z = a[3];
result = Vector4.Add(v, v);
b[0] = result.W;
b[1] = result.X;
b[2] = result.Y;
b[3] = result.Z;
}
ticks_end = DateTime.Now.Ticks;
Console.WriteLine($"Done in {ticks_end - ticks_start} ticks");
Console.ReadKey();
}
}
}
输出为:
Done in 14257591 ticks
Done in 18591588 ticks
所以看起来我们使用 Vector4 没有任何优势。 Add 方法 returns Vector4 的新实例。有没有办法改变其中一个向量以避免内存分配影响?或者也许有另一种方法来做事?
我还没有真正对其进行基准测试,但是在内部循环中:
for (int i = 0; i < N; i++)
{
v.W = a[0];
v.X = a[1];
v.Y = a[2];
v.Z = a[3];
result = Vector4.Add(v, v);
b[0] = result.W;
b[1] = result.X;
b[2] = result.Y;
b[3] = result.Z;
}
更等同于:
float[] a = { 10, 10, 10, 10 };
float[] b = new float[4];
float[] v = new float[4];
float[] result = new float[4];
for (int i = 0; i < N; i++)
{
v[0] = a[0];
v[1] = a[1];
v[2] = a[2];
v[3] = a[3];
result[0] = v[0] + v[0];
result[1] = v[1] + v[1];
result[2] = v[2] + v[2];
result[3] = v[3] + v[3];
b[0] = result[0];
b[1] = result[1];
b[2] = result[2];
b[3] = result[3];
}
比你写的要多(你正在做 4 个赋值 - 赋值给 v
-,然后是加法,然后是另外 4 个赋值 - 赋值 result
到 b
-,你只是跳过你的数组操作)。
我刚刚使用秒表在 linqpad 上对其进行了测试(这绝不是基准测试),如果你这样做,数组添加(即使总和展开)比 Vector4
(以非常小的幅度)。
根据评论区的各种建议,在release模式下测试,向量化版本明显比数组版本快。
发布此答案是为了提高可见度。感谢 Jeroen van Langen、phuclv 和 Alexei Levenkov。
感谢他们花时间和耐心回答这个问题。
可以在不使用分配的情况下改进代码。 MemoryMarshal.Cast
从字面上创建一个 Span
指向与数组相同的内存区域。 float
数组的大小可以是任意的,它将产生大小为 a.Length / 4
或 Vector4
的 Span
。
结果相同。当您写入输出 Span
时,数据将直接写入输出数组内存区域。
static void Main(string[] args)
{
const int N = 100000000;
long ticks_start, ticks_end;
ticks_start = DateTime.Now.Ticks;
float[] a = { 10, 10, 10, 10 };
float[] b = new float[4];
for (int i = 0; i < N; i++)
for (int j = 0; j < 4; j++)
b[j] = a[j] + a[j];
ticks_end = DateTime.Now.Ticks;
Console.WriteLine(string.Join(", ", b));
Console.WriteLine($"Done in {ticks_end - ticks_start} ticks");
Console.WriteLine("Clearing...");
Array.Clear(b, 0, b.Length);
Console.WriteLine(string.Join(", ", b));
ticks_start = DateTime.Now.Ticks;
ReadOnlySpan<Vector4> v = MemoryMarshal.Cast<float, Vector4>(a);
Span<Vector4> r = MemoryMarshal.Cast<float, Vector4>(b);
for (int i = 0; i < N; i++)
{
r[0] = Vector4.Add(v[0], v[0]);
}
ticks_end = DateTime.Now.Ticks;
Console.WriteLine(string.Join(", ", b));
Console.WriteLine($"Done in {ticks_end - ticks_start} ticks");
Console.ReadKey();
}
运行 发布版本。
20, 20, 20, 20
Done in 2172911 ticks
Clearing...
0, 0, 0, 0
20, 20, 20, 20
Done in 649113 ticks
看起来快了 3.3 倍。