.net 如何提高C#中比较大型结构的性能?

chhkpiq4  于 2023-11-20  发布在  .NET
关注(0)|答案(5)|浏览(129)

我想通过Equals比较一个C#中的大型Struct,它又是通过IEquatable<T>接口实现的。
我的问题是它的性能非常差,因为我的结构体相当大。想象一下结构体的简化版本,如下所示:

  1. public struct Data
  2. {
  3. public byte b0;
  4. public byte b1;
  5. public byte b2;
  6. public byte b3;
  7. public byte b4;
  8. public byte b5;
  9. public byte b6;
  10. public byte b7;
  11. }

字符串
我现在写一个简单的Equals:

  1. public bool Equals(Data other)
  2. {
  3. return b0 == other.b0 &&
  4. b1 == other.b1 &&
  5. ...
  6. }


有没有一种方法可以使Equals方法更有效?

更新

根据here的定义,我的结构类型在unmanaged中。

7jmck4yq

7jmck4yq1#

假设你的字节大小的结构体是unmanaged types,并且你正在使用.NET Core 2.1或更高版本,你可以使用MemoryMarshal.Cast()沿着MemoryMarshal.CreateReadOnlySpan()将你的每个结构体转换为ReadOnlySpan<byte>。这样做之后,你可以逐字节比较它们的值相等性:

  1. public static class UnmanagedExtensions
  2. {
  3. public static bool Equals<T>(ref T x, ref T y) where T : unmanaged
  4. {
  5. var byteSpanX = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateReadOnlySpan(ref x, 1));
  6. var byteSpanY = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateReadOnlySpan(ref y, 1));
  7. return byteSpanX.SequenceEqual(byteSpanY);
  8. }
  9. }

字符串
如果你的结构体有很多很多字段,这比手动逐字段比较要简单得多。进一步注意,根据参考源,MemoryExtensions.SequenceEqual<T>(this ReadOnlySpan<T> span, ReadOnlySpan<T> other)Unrolled and vectorized for half-constant input,所以它可能会被合理优化。
演示fiddle hereEqualityComparer<T>.Default.Equals()相比有80-85%的加速。
话虽如此,如果你的结构真的是 * 在100字节 *,那么你很可能花了大量的时间来复制它们。在其设计指南Choosing Between Class and Struct中,Microsoft指出:
如果类型的示例是的,并且通常是短暂的,或者通常嵌入在其他对象中,那么请考虑定义结构而不是类。
避免定义结构,除非该类型具有以下所有特征:

  • 它在逻辑上表示单个值,类似于基本类型(int,double等)。
    *示例大小不超过16字节。
  • 它是不可改变的。
  • 它不需要经常装箱。

一个结构体的大小在100字节左右绝对不小。微软建议在这种情况下使用类。如果你必须使用这样一个大的结构体(例如,因为你正在与一些非托管代码交互),请确保尽可能通过refref readonlyin传递它。

展开查看全部
hec6srdp

hec6srdp2#

事实上,有一种方法可以提高复杂结构的性能。
以你为例:

  1. public struct Data
  2. {
  3. public byte b0;
  4. public byte b1;
  5. public byte b2;
  6. public byte b3;
  7. public byte b4;
  8. public byte b5;
  9. public byte b6;
  10. public byte b7;
  11. }

字符串
然后你可以写Equals方法如下:

  1. public bool Equals(Data other)
  2. {
  3. DatHelper helperThis = Unsafe.As<Data, DatHelper>(ref this);
  4. DatHelper helperOther = Unsafe.As<Data, DatHelper>(ref other);
  5. return helperThis.Equals(helperOther);
  6. }


DataHelper看起来像这样:

  1. public struct DataHelper
  2. {
  3. public long l0;
  4. public bool Equals(DataHelper other)
  5. {
  6. return l0 == other.l0;
  7. }
  8. }

为什么会这样?

这里重要的是,两个结构体在内存中的大小相同。然后我们可以使用Unsafe.AsData的内存重新解释为DataHelper,这允许我们比较,在我们的例子中,一个long和一个long,而不是八个字节和八个字节。
这是可扩展的。例如,如果你有一个结构体,它的大小是33字节,那么你可以创建DataHelper作为四个long和一个byte,并比较它们。
唯一必须保证的是:
1.你必须使用一个结构体。这对类不起作用,因为类只包含它们数据的位置,而结构体包含它们的实际数据。
1.两个结构的大小必须相同。
和往常一样,衡量一下您是否真的看到了性能提升。

展开查看全部
h9a6wy2h

h9a6wy2h3#

采取了一些建议张贴在这里和我的个人采取通过Vector64

  1. var len = Unsafe.SizeOf<Data>();
  2. ViaVector(ref Unsafe.As<Data, byte>(ref left), ref Unsafe.As<Data, byte>(ref right), (uint)len)
  3. public static bool ViaVector(ref byte first, ref byte second, uint length)
  4. {
  5. nuint offset = 0;
  6. nuint lengthToExamine = length - (nuint)Vector64<byte>.Count;
  7. if (lengthToExamine != 0)
  8. {
  9. do
  10. {
  11. if (Vector64.LoadUnsafe(ref first, offset) != Vector64.LoadUnsafe(ref second, offset))
  12. {
  13. return false;
  14. }
  15. offset += (nuint)Vector64<byte>.Count;
  16. } while (lengthToExamine > offset);
  17. }
  18. if (Vector64.LoadUnsafe(ref first, lengthToExamine) == Vector64.LoadUnsafe(ref second, lengthToExamine))
  19. {
  20. return true;
  21. }
  22. return false;
  23. }

字符串
又组成了following benchmark

  1. public class StructEqualityBench
  2. {
  3. private const int iterations = 10_000;
  4. private Data left = new()
  5. {
  6. b7 = 1
  7. };
  8. private Data right = new()
  9. {
  10. b7 = 1
  11. };
  12. [Benchmark]
  13. public bool AutoEquals()
  14. {
  15. var r = true;
  16. for (int i = 0; i < iterations; i++)
  17. {
  18. r |= left.Equals(right);
  19. }
  20. return r;
  21. }
  22. [Benchmark]
  23. public bool ViaCustomEquals()
  24. {
  25. var r = true;
  26. for (int i = 0; i < iterations; i++)
  27. {
  28. r |= UnmanagedExtensions.CustomEquals(ref left, ref right);
  29. }
  30. return r;
  31. }
  32. [Benchmark]
  33. public bool ViaVector()
  34. {
  35. var r = true;
  36. for (int i = 0; i < iterations; i++)
  37. {
  38. r |= UnmanagedExtensions.ViaVector(ref Unsafe.As<Data, byte>(ref left), ref Unsafe.As<Data, byte>(ref right), (uint)Unsafe.SizeOf<Data>());
  39. }
  40. return r;
  41. }
  42. [Benchmark]
  43. public bool ViaDataHelper()
  44. {
  45. var r = true;
  46. for (int i = 0; i < iterations; i++)
  47. {
  48. var helperThis = Unsafe.As<Data, DataHelper>(ref left);
  49. var helperOther = Unsafe.As<Data, DataHelper>(ref right);
  50. r |= helperOther.Equals(helperThis);
  51. }
  52. return r;
  53. }
  54. }


它给出了“在我的机器上”:
| 方法|是说|误差|StdDev|
| --|--|--|--|
| 自动相等|144.292美制|1.1918美制|1.1149美制|
| 通过自定义等于|145.347美制|0.3022 μ s的范围|0.2523 μ s的范围|
| 通过向量|54.058美制|0.1327 μ s的范围|0.1176 μ s的电流|
| 通过数据帮助程序|5.285美制|0.0134 μ s的范围|0.0119 μ s的电流|
备注:

  • 基于源代码,ValueType.Equals应已针对以下情况进行了大量优化:
  1. // if there are no GC references in this object we can avoid reflection
  2. // and do a fast memcmp


但请注意,根据CLR实现-it can be a brittle heuristic

  • My Vector方法基本上是基于此实现(@ github.com)中采用的方法。
  • DataHelper方法似乎是最快的一种(在我的硬件上),但对不同数量的 prop 的可扩展性较低。
展开查看全部
gdx19jrr

gdx19jrr4#

在这个答案中,我并不是要说明如何优化Equals方法,而是要说明如何避免编写它。

  1. public record struct Data
  2. {
  3. public byte b0;
  4. public byte b1;
  5. public byte b2;
  6. public byte b3;
  7. public byte b4;
  8. public byte b5;
  9. public byte b6;
  10. public byte b7;
  11. }

字符串
这样,C#编译器将为您实现Equals方法:

  1. public struct Data : IEquatable<Data>
  2. {
  3. /* ... */
  4. [IsReadOnly]
  5. [CompilerGenerated]
  6. public bool Equals(Data other)
  7. {
  8. return EqualityComparer<byte>.Default.Equals(b0, other.b0)
  9. && EqualityComparer<byte>.Default.Equals(b1, other.b1)
  10. && EqualityComparer<byte>.Default.Equals(b2, other.b2)
  11. && EqualityComparer<byte>.Default.Equals(b3, other.b3)
  12. && EqualityComparer<byte>.Default.Equals(b4, other.b4)
  13. && EqualityComparer<byte>.Default.Equals(b5, other.b5)
  14. && EqualityComparer<byte>.Default.Equals(b6, other.b6)
  15. && EqualityComparer<byte>.Default.Equals(b7, other.b7);
  16. }
  17. }


SharpLab演示版。

展开查看全部
wvt8vs2t

wvt8vs2t5#

请尝试以下操作:

  1. [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Ansi)]
  2. public struct Data
  3. {
  4. [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8), FieldOffset(0)]
  5. public byte[] b;
  6. [FieldOffset(0)]
  7. public long l;
  8. }

字符串

相关问题