【问题标题】:Obtain a Span<byte> over a struct without making a copy of the struct在结构上获取 Span<byte> 而不复制结构
【发布时间】:2020-04-23 01:37:44
【问题描述】:

我一直在尝试将 Span&lt;T&gt; 作为 ReadOnlySequence&lt;T&gt; 和 System.IO.Pipelines 的一部分。

我目前正在尝试在不使用 unsafe 代码且不复制 struct 的情况下通过 struct 获取 Span&lt;T&gt;

我的结构很简单:

    [StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)]
    public struct Packet
    {
        public byte TestByte;
    }

方法 1 - 可行 - 但感觉“不安全”

    //
    // Method 1 - uses Unsafe to get a span over the struct
    //
    var packet = new Packet();
    unsafe
    {
        var packetSpan = new Span<byte>(&packet, Marshal.SizeOf(packet));

        packetSpan[0] = 0xFF; // Set the test byte
        Debug.Assert(packet.TestByte == 0xFF, "Error, packetSpan did not update packet.");
            // ^^^ Succeeds
        packet.TestByte = 0xEE;
        Debug.Assert(packetSpan[0] == 0xEE, "Error, packet did not update packetSpan.");
            // ^^^ Succeeds
    }

方法 2 - 不能按预期工作,因为它需要副本

    //
    // Method 2
    //
    // This doesn't work as intended because the original packet is actually
    // coppied to packet2Array because it's a value type
    //
    // Coppies the packet to an Array of Packets
    // Gets a Span<Packet> of the Array of Packets
    // Casts the Span<Packet> as a Span<byte>
    //
    var packet2 = new Packet();

    // create an array and store a copy of packet2 in it
    Packet[] packet2Array = new Packet[1];
    packet2Array[0] = packet2;

    // Get a Span<Packet> of the packet2Array
    Span<Packet> packet2SpanPacket = MemoryExtensions.AsSpan<Packet>(packet2Array);

    // Cast the Span<Packet> as a Span<byte>
    Span<byte> packet2Span = MemoryMarshal.Cast<Packet, byte>(packet2SpanPacket);

    packet2Span[0] = 0xFF; // Set the test byte
    Debug.Assert(packet2.TestByte == 0xFF, "Error, packet2Span did not update packet2");
        // ^^^ fails because packet2 was coppied into the array, and thus packet2 has not changed.
    Debug.Assert(packet2Array[0].TestByte == 0xFF, "Error, packet2Span did not update packet2Array[i]");
        // ^^^ succeeds

    packet2.TestByte = 0xEE;
    Debug.Assert(packet2Span[0] == 0xEE, "Error, packet2 did not update in packet2Span");
        // ^^^ fails because packet2Span is covering packet2Array which has a copy of packet2 
    packet2Array[0].TestByte = 0xEE;
    Debug.Assert(packet2Span[0] == 0xEE, "Error, packet2 did not update in packet2Span");
        // ^^^ succeeds

进一步的研究表明

Span&lt;T&gt; 可以从 byte[] 隐式转换,例如,我可以这样做

Span<byte> packetSpan = new Packet().ToByteArray();

但我目前的任何 ToByteArray() 实现仍在制作 Packet 结构的副本。

我不能这样做:

Span<byte> packetSpan = (byte[])packet;
    // ^^ Won't compile

【问题讨论】:

  • 我认为您可以编写用户定义的转换运算符。 docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
  • 我很确定如果没有unsafe,一般的结构就无法做到这一点,因为如果你在结构的所有字节上获得Span,你可能会改变任何位以任何方式在该结构中 - 这本质上是不安全的。

标签: c# system.io.pipelines


【解决方案1】:

没有unsafe,就无法在任意结构上获取Span&lt;byte&gt;,因为这样的跨度将允许您以任何方式更改结构的任何位,可能违反类型的不变量 - 这本质上是不安全的操作.

好的,但是ReadOnlySpan&lt;byte&gt; 呢?请注意,您必须将StructLayoutAttribute 放在您的结构上,以使您的代码更合理。这应该是一个提示。想象一下尝试编写一个更简单的方法,该方法为任意T where T : struct 返回一个byte[]。您必须先找出struct 的大小,不是吗?那么,如何在 C# 中找出struct 的大小?您可以使用sizeof 运算符,它需要unsafe 上下文并且需要结构为unmanaged type;或者你可以Marshall.SizeOf 这很奇怪,只适用于具有顺序或显式字节布局的结构。 没有安全、通用的方法,因此您不能这样做。

Span&lt;T&gt;ReadOnlySpan&lt;T&gt; 在设计时并没有考虑到访问结构字节,而是考虑了跨越数组的片段,这些片段具有已知的大小并保证是连续的。

如果您确信自己知道自己在做什么,则可以在 unsafe 上下文中执行此操作 - 这就是它的用途。但请注意,由于上述原因,您使用 unsafe 的解决方案不能推广到任意结构。

如果您打算将您的结构用作 IO 操作的缓冲区,您可能需要查看fixed size buffers。它们还需要unsafe 上下文,但您可以将不安全性封装在结构中并将Span&lt;byte&gt; 返回到该固定缓冲区。基本上任何处理内存中对象字节结构的东西都需要在 .NET 中使用unsafe,因为内存管理就是这个“安全”所指的东西。

【讨论】:

  • 这两个都是如此完美的答案。感谢您付出的努力,我非常感谢这一点以及指向固定大小缓冲区的指针。我需要选择一个作为答案,尽管我想选择两者。所以恐怕这是我的RNG的情况。
【解决方案2】:

您必须在unsafe context 中执行此操作,因为按照这个词的真正含义,它是不安全,因为如果您不够小心,您会在自己的脚下开枪。原因如下:

考虑以下代码:

Span<byte> GiveMeSpan() 
{
    MyLovelyStruct value = new MyLovelyStruct();
    unsafe 
    {
        return new Span<byte>(&value, sizeof(MyLovelyStruct));
    }
}

我们在GiveMeSpan() 中创建的MyLovelyStruct 实例存在于方法的call stack 中,您所做的是获取其地址,将其提供给Span&lt;byte&gt;,然后返回Span&lt;byte&gt;。一旦一个方法返回,它就会弹出它的stack frame,因此你的MyLovelyStruct所在的内存将变得空闲,并且可能被调用者调用的下一个方法回收并破坏它。

但这还不是全部,如果您的 MyLovelyStruct 生活在这样的对象字段中怎么办:

class MyLovelyClass 
{
    private MyLovelyStruct value;

    public void Foo() 
    {
        unsafe 
        {
            var span = new Span(&value, sizeof(MyLovelyStruct));
            Process(span);
        }
    }
}

// Declaration 
Process(Span<byte> span);

Process() 方法正在处理您的MyLovelyStructMyLovelyClass 突然在内存中移动时会发生GC(是的,GC 移动的对象在内存中,read here)?是的,指向MyLovelyStructSpan&lt;byte&gt; 将不再指向新的MyLovelyStruct 地址,您的程序就会损坏。

因此,为了使用Span&lt;byte&gt; 或任何其他指针类型安全地包装struct,您必须确保:

  • 实例位于固定的内存位置(例如在堆栈或非托管内存中,例如由Marshal.AllocHGlobal 分配的内存块)
  • 在您完成指针操作之前,不会占用实例内存

所以unsafe 关键字是必需的,即使你可以绕过它,你也有责任警告你的代码的读者。

【讨论】:

  • 这两个都是如此完美的答案。感谢您付出的努力,我非常感谢这一点以及更清晰的解释/不安全的链接,我需要选择一个作为答案,尽管我想同时选择两者。所以恐怕这是我的RNG的情况。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-09-06
  • 2020-03-20
  • 2014-08-14
  • 1970-01-01
  • 2015-01-06
  • 2013-10-20
  • 2016-12-06
相关资源
最近更新 更多