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

chhkpiq4  于 12个月前  发布在  .NET
关注(0)|答案(5)|浏览(81)

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

public struct Data
{
    public byte b0;
    public byte b1;
    public byte b2;
    public byte b3;
    public byte b4;
    public byte b5;
    public byte b6;
    public byte b7;
}

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

public bool Equals(Data other)
{
    return b0 == other.b0 &&
           b1 == other.b1 &&
           ... 
}


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

更新

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

7jmck4yq

7jmck4yq1#

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

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

字符串
如果你的结构体有很多很多字段,这比手动逐字段比较要简单得多。进一步注意,根据参考源,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#

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

public struct Data
{
    public byte b0;
    public byte b1;
    public byte b2;
    public byte b3;
    public byte b4;
    public byte b5;
    public byte b6;
    public byte b7;
}

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

public bool Equals(Data other)
{ 
    DatHelper helperThis = Unsafe.As<Data, DatHelper>(ref this); 
    DatHelper helperOther = Unsafe.As<Data, DatHelper>(ref other); 
    return helperThis.Equals(helperOther); 
}


DataHelper看起来像这样:

public struct DataHelper
{
    public long l0;
    
    public bool Equals(DataHelper other)
    {
        return l0 == other.l0; 
    }
}

为什么会这样?

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

h9a6wy2h

h9a6wy2h3#

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

var len = Unsafe.SizeOf<Data>();
ViaVector(ref Unsafe.As<Data, byte>(ref left), ref Unsafe.As<Data, byte>(ref right), (uint)len)

public static bool ViaVector(ref byte first, ref byte second, uint length)
{
    nuint offset = 0;
    nuint lengthToExamine = length - (nuint)Vector64<byte>.Count;
    if (lengthToExamine != 0)
    {
        do
        {
            if (Vector64.LoadUnsafe(ref first, offset) != Vector64.LoadUnsafe(ref second, offset))
            {
                return false;
            }

            offset += (nuint)Vector64<byte>.Count;
        } while (lengthToExamine > offset);
    }

    if (Vector64.LoadUnsafe(ref first, lengthToExamine) == Vector64.LoadUnsafe(ref second, lengthToExamine))
    {
        return true;
    }

    return false;
}

字符串
又组成了following benchmark

public class StructEqualityBench
{
    private const int iterations = 10_000;

    private Data left = new()
    {
        b7 = 1
    };

    private Data right = new()
    {
        b7 = 1
    };

    [Benchmark]
    public bool AutoEquals()
    {
        var r = true;
        for (int i = 0; i < iterations; i++)
        {
            r |= left.Equals(right);
        }

        return r;
    }

    [Benchmark]
    public bool ViaCustomEquals()
    {
        var r = true;
        for (int i = 0; i < iterations; i++)
        {
            r |= UnmanagedExtensions.CustomEquals(ref left, ref right);
        }

        return r;
    }

    [Benchmark]
    public bool ViaVector()
    {
        var r = true;
        for (int i = 0; i < iterations; i++)
        {
            r |= UnmanagedExtensions.ViaVector(ref Unsafe.As<Data, byte>(ref left), ref Unsafe.As<Data, byte>(ref right), (uint)Unsafe.SizeOf<Data>());
        }

        return r;
    }

    [Benchmark]
    public bool ViaDataHelper()
    {
        var r = true;
        for (int i = 0; i < iterations; i++)
        {
            var helperThis = Unsafe.As<Data, DataHelper>(ref left);
            var helperOther = Unsafe.As<Data, DataHelper>(ref right);
            r |= helperOther.Equals(helperThis);
        }

        return r;
    }
}


它给出了“在我的机器上”:
| 方法|是说|误差|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应已针对以下情况进行了大量优化:
// if there are no GC references in this object we can avoid reflection
// and do a fast memcmp


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

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

gdx19jrr4#

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

public record struct Data
{
    public byte b0;
    public byte b1;
    public byte b2;
    public byte b3;
    public byte b4;
    public byte b5;
    public byte b6;
    public byte b7;
}

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

public struct Data : IEquatable<Data>
{
    /* ... */

    [IsReadOnly]
    [CompilerGenerated]
    public bool Equals(Data other)
    {
        return EqualityComparer<byte>.Default.Equals(b0, other.b0)
            && EqualityComparer<byte>.Default.Equals(b1, other.b1)
            && EqualityComparer<byte>.Default.Equals(b2, other.b2)
            && EqualityComparer<byte>.Default.Equals(b3, other.b3)
            && EqualityComparer<byte>.Default.Equals(b4, other.b4)
            && EqualityComparer<byte>.Default.Equals(b5, other.b5)
            && EqualityComparer<byte>.Default.Equals(b6, other.b6)
            && EqualityComparer<byte>.Default.Equals(b7, other.b7);
    }
}


SharpLab演示版。

wvt8vs2t

wvt8vs2t5#

请尝试以下操作:

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

字符串

相关问题