【发布时间】:2019-12-22 15:00:02
【问题描述】:
我一直在自己编写一个深度学习库。在矩阵运算中,获得最佳性能对我来说是关键。我一直在研究编程语言及其在数值运算方面的表现。一段时间后,我发现 C# SIMD 与 C++ SIMD 的性能非常相似。所以,我决定用 C# 编写这个库。
首先,我测试了C# SIMD(我测试了很多东西,但这里就不写了)。我注意到在使用较小的数组时效果更好。 使用更大的数组时效率不佳。我认为这很荒谬。通常情况下,事情越大,效率越高。
我的问题是“为什么在 C# 中使用更大的数组时矢量化的工作速度会变慢?”
我将使用 BenchmarkNet 分享基准(由我自己完成)。
Program.Size = 10
| Method | Mean | Error | StdDev |
|------- |----------:|----------:|----------:|
| P1 | 28.02 ns | 0.5225 ns | 0.4888 ns |
| P2 | 154.15 ns | 1.1220 ns | 0.9946 ns |
| P3 | 100.88 ns | 0.8863 ns | 0.8291 ns |
Program.Size = 10000
| Method | Mean | Error | StdDev | Median |
|------- |---------:|---------:|---------:|---------:|
| P1 | 142.0 ms | 3.065 ms | 8.989 ms | 139.5 ms |
| P2 | 170.3 ms | 3.365 ms | 5.981 ms | 170.1 ms |
| P3 | 103.3 ms | 2.400 ms | 2.245 ms | 102.8 ms |
如您所见,我将大小增加了 1000 倍,这意味着将数组的大小增加了 1000000 倍。 P2 一开始需要 154 ns。在第二次测试中,花费了 170 毫秒,这是我们预期的 1000 倍以上。此外,P3 花费的时间正好是 1000 倍(100ns - 100ms)。但是,我想在这里指出的是,矢量化循环的 P1 的性能明显低于以前。我想知道为什么。
请注意,P3 独立于该主题。 P1 是 P2 的矢量化版本。所以,我们可以说向量化的效率是 P2/P1 就他们所花费的时间而言。我的代码如下:
矩阵类:
public sealed class Matrix1
{
public float[] Array;
public int D1, D2;
const int size = 110000000;
private static ArrayPool<float> sizeAwarePool = ArrayPool<float>.Create(size, 100);
public Matrix1(int d1, int d2)
{
D1 = d1;
D2 = d2;
if(D1*D2 > size)
{ throw new Exception("Size!"); }
Array = sizeAwarePool.Rent(D1 * D2);
}
bool Deleted = false;
public void Dispose()
{
sizeAwarePool.Return(Array);
Deleted = true;
}
~Matrix1()
{
if(!Deleted)
{
throw new Exception("Error!");
}
}
public float this[int x, int y]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return Array[x * D2 + y];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
Array[x * D2 + y] = value;
}
}
}
程序类:
public class Program
{
const int Size = 10000;
[Benchmark]
public void P1()
{
Matrix1 a = Program.a, b = Program.b, c = Program.c;
int sz = Vector<float>.Count;
for (int i = 0; i < Size * Size; i += sz)
{
var v1 = new Vector<float>(a.Array, i);
var v2 = new Vector<float>(b.Array, i);
var v3 = v1 + v2;
v3.CopyTo(c.Array, i);
}
}
[Benchmark]
public void P2()
{
Matrix1 a = Program.a, b = Program.b, c = Program.c;
for (int i = 0; i < Size; i++)
for (int j = 0; j < Size; j++)
c[i, j] = a[i, j] + b[i, j];
}
[Benchmark]
public void P3()
{
Matrix1 a = Program.a;
for (int i = 0; i < Size; i++)
for (int j = 0; j < Size; j++)
a[i, j] = i + j - j;
//could have written a.Array[i*size + j] = i + j
//but it would have made no difference in terms of performance.
//so leave it that way
}
public static Matrix1 a = new Matrix1(Size, Size);
public static Matrix1 b = new Matrix1(Size, Size);
public static Matrix1 c = new Matrix1(Size, Size);
static void Main(string[] args)
{
for (int i = 0; i < Size; i++)
for (int j = 0; j < Size; j++)
a[i, j] = i;
for (int i = 0; i < Size; i++)
for (int j = 0; j < Size; j++)
b[i, j] = j;
for (int i = 0; i < Size; i++)
for (int j = 0; j < Size; j++)
c[i, j] = 0;
var summary = BenchmarkRunner.Run<Program>();
a.Dispose();
b.Dispose();
c.Dispose();
}
}
我向您保证x[i,j] 不会影响性能。与使用x.Array[i*Size + j]相同
【问题讨论】:
-
P1 正在使用“new”,它正在调用一个类的构造函数,这会增加大量时间。
-
尚未查看您的代码,但较大的尺寸可能会成为 SIMD 内存带宽的瓶颈。即在您遇到内存带宽瓶颈之前没有太多加速空间。但是,由于 L1d 缓存中的热数据可以跟上 SIMD ALU,您可以接近完整的 8 倍或任何超过标量的加速,例如用于 AVX 8x 32 位元素。
-
不知道是不是和here一样的原因。
-
@GSerg:不,它正在使用
BenchmarkRunner.Run<Program>(),这将阻止P1()的不同调用之间的优化。它不能将实际工作提升或沉入基准重复循环之外,并且只能执行一次。 -
@jdweng 如果您查看深度汇编代码,新向量
不会创建对象。向量类是完全不同的。你应该搜索 c# simd。
标签: c# performance vectorization simd memory-bandwidth