.net 如何在C#中使用向量化计算双精度矩阵的和?

busg9geu  于 2023-11-20  发布在  .NET
关注(0)|答案(2)|浏览(208)

我有一个二维的双精度数组,表示一个矩阵,它可能很大,例如200x200。
我需要能够有效地计算这个矩阵的和。我如何在C#中使用向量化来实现这一点?
当前的vanilla方法是:

  1. double[,] matrix =
  2. {
  3. { 0.0, 1, 2, 3 },
  4. { 4, 5, 6, 7 },
  5. { 8, 9, 10, 11 },
  6. { 12, 13, 14, 15 }
  7. };
  8. int rows = matrix.GetLength(0);
  9. int cols = matrix.GetLength(1);
  10. double sum = 0;
  11. for (uint i = 0; i < rows; i++)
  12. {
  13. for (uint j = 0; j < cols; j++)
  14. {
  15. sum += matrix[i, j];
  16. }
  17. }

字符串

dkqlctbz

dkqlctbz1#

这可以通过System.Numerics vector API很好地完成,至少可以自由使用Unsafe类。
据我所知,从二维矩阵中加载向量没有一个好的“标准”方法。普通加载的重载都不适用,也没有一个普通的方法来获得二维数组的Span<T>。但是使用Unsafe,我们可以完成它。
使用8个单独的数组展开8(参见Unrolling FP loops with multiple accumulators),并通过使用Unsafe操作引用将2D矩阵视为1D数组,我们可以做到这一点:(未测试,但在sharplab.io上编译)

  1. static unsafe double Sum(double[,] matrix)
  2. {
  3. Vector<double> sum0 = Vector<double>.Zero;
  4. Vector<double> sum1 = Vector<double>.Zero;
  5. Vector<double> sum2 = Vector<double>.Zero;
  6. Vector<double> sum3 = Vector<double>.Zero;
  7. Vector<double> sum4 = Vector<double>.Zero;
  8. Vector<double> sum5 = Vector<double>.Zero;
  9. Vector<double> sum6 = Vector<double>.Zero;
  10. Vector<double> sum7 = Vector<double>.Zero;
  11. double sum8 = 0;
  12. uint vlen = (uint)Vector<double>.Count;
  13. ref double unaligneddata = ref matrix[0, 0];
  14. uint i = 0;
  15. uint alignmask = vlen * sizeof(double) - 1;
  16. for (; i < matrix.Length && ((IntPtr)Unsafe.AsPointer(ref unaligneddata) & alignmask) != 0; i++)
  17. {
  18. sum8 += unaligneddata;
  19. unaligneddata = ref Unsafe.Add(ref unaligneddata, 1);
  20. }
  21. uint alignment_skipped = i;
  22. ref Vector<double> data = ref Unsafe.As<double, Vector<double>>(ref unaligneddata);
  23. uint bigChunk = ((uint)matrix.Length - alignment_skipped & (0u - (vlen * 8))) + alignment_skipped;
  24. for (; i < bigChunk; i += vlen * 8)
  25. {
  26. sum0 += data;
  27. sum1 += Unsafe.Add(ref data, 1);
  28. sum2 += Unsafe.Add(ref data, 2);
  29. sum3 += Unsafe.Add(ref data, 3);
  30. sum4 += Unsafe.Add(ref data, 4);
  31. sum5 += Unsafe.Add(ref data, 5);
  32. sum6 += Unsafe.Add(ref data, 6);
  33. sum7 += Unsafe.Add(ref data, 7);
  34. data = ref Unsafe.Add(ref data, 8);
  35. }
  36. uint smallChunk = ((uint)matrix.Length - alignment_skipped & (0u - vlen)) + alignment_skipped;
  37. for (; i < smallChunk; i += vlen)
  38. {
  39. sum0 += data;
  40. data = ref Unsafe.Add(ref data, 1);
  41. }
  42. ref double remainder = ref Unsafe.As<Vector<double>, double>(ref data);
  43. for (; i < matrix.Length; i++)
  44. {
  45. sum8 += remainder;
  46. remainder = ref Unsafe.Add(ref remainder, 1);
  47. }
  48. sum0 += sum1;
  49. sum2 += sum3;
  50. sum4 += sum5;
  51. sum6 += sum7;
  52. sum0 += sum2;
  53. sum4 += sum6;
  54. sum0 += sum4;
  55. return Vector.Dot(sum0, new Vector<double>(1.0)) + sum8;
  56. }

字符串
最后使用Vector.Dot来做水平求和有点傻,但是很短,而且只发生一次。
在开始时,试图使地址对齐的循环主要是在AVX不使用时。不幸的是,这需要unsafe(关键字,而不是类),据我所知,即使原始指针立即转换为整数,并且从未用作指针。
当AVX 2可用时(Vector<T>是128位的,没有AVX 2,即使你只使用float/double),主循环在汇编中可能看起来像这样:

  1. L008c: vaddpd ymm0, ymm0, [rax]
  2. L0091: vaddpd ymm1, ymm1, [rax+0x20]
  3. L0097: vaddpd ymm2, ymm2, [rax+0x40]
  4. L009d: vaddpd ymm3, ymm3, [rax+0x60]
  5. L00a3: vaddpd ymm4, ymm4, [rax+0x80]
  6. L00ac: vaddpd ymm5, ymm5, [rax+0xa0]
  7. L00b5: vaddpd ymm6, ymm6, [rax+0xc0]
  8. L00be: vaddpd ymm7, ymm7, [rax+0xe0]
  9. L00c7: add rax, 0x100
  10. L00cd: add r8d, 0x20
  11. L00d1: cmp r8d, ecx
  12. L00d4: jb short L008c


在我看来很好。我们可以通过直接比较地址来保存add,而不是保留冗余索引,但这不是一个大问题。

展开查看全部
csga3l58

csga3l582#

首先,你应该做一些基准测试和/或分析,问问自己这是否真的重要?求和是一个非常简单的计算,200 x200不是很大。我猜它可能需要一微秒的数量级,但这只是一个猜测。你还需要一个基准来决定你是否真的实现了 * 任何 * 改进,或者你只是让代码变得更复杂。
但这真的是应用程序的最大瓶颈吗?优化通常是为了避免重复工作。任何SIMD优化给予的最好效果是持续的加速。浪费时间优化对用户没有明显影响的函数是没有意义的。
如果你决定你需要优化,那么我会从摆脱索引计算开始。当你做matrix[i, j]时,框架本质上做了一个i * width + j计算。这可能会比实际的求和时间更长。优化器可能会删除一些,但是我不会在没有实际确认的情况下从优化器中假设任何东西。你可以用fixed (double* ptr = matrix )做不安全的路由,或者创建一个自定义矩阵类,它使用1D数组进行存储,只允许您使用单个循环对值求和,如果您出于其他原因需要[x, y]语法,则可以自己实现2D索引器。
如果你真的需要SIMD的性能,你可以从两个方面入手

  1. Vector<T>
    1.内部特性,如Vector256
    参见the comparison。简而言之,内在给予更好的性能,代价是将其绑定到特定的cpu平台。
    在这两种情况下,你都需要了解内存布局来正确加载元素。但是一旦完成了,它应该非常简单,只需将所有向量加在一起,最后对元素求和。如果元素计数不能被向量长度整除,最后可能会使用一些标量代码。

相关问题