【问题标题】:How to get a ReadOnlySpan<byte> from a readonly struct?如何从只读结构中获取 ReadOnlySpan<byte>?
【发布时间】:2020-03-20 01:43:51
【问题描述】:

以下代码导致编译器在属性 getter 的第一行抛出错误 CS1605(“无法将 'var' 作为 ref 或 out 参数传递,因为它是只读的”)。

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct MyStruct
{
    public readonly int Field1;
    public readonly int Field2;

    public MyStruct(int field1, int field2) => (Field1, Field2) = (field1, field2);

    public ReadOnlySpan<byte> Span
    {
        get
        {
            // This code only works when MyStruct is not read only
            ReadOnlySpan<MyStruct> temp = MemoryMarshal.CreateReadOnlySpan(ref this, 1);
            return MemoryMarshal.Cast<MyStruct, byte>(temp);
        }
    }
}

public readonly struct MyStruct 行中删除readonly 会使代码工作,但出于性能原因,我真的希望结构为readonly。它使代码比一直传递结构为ref 更简洁。

有没有办法从只读结构中获取ReadOnlySpan&lt;byte&gt;

编辑: ... 不创建结构的隐式或显式副本?

【问题讨论】:

  • 我认为你的意思是“ref struct”(它在堆栈上)。我认为“只读结构”不会给您带来任何性能提升。
  • 我假设结构只是一个示例,但一般来说,您不想特意阻止复制 8 字节结构,因为指针只是一样大(在 64 位系统上),并且跳过间接可能会产生更好的代码。 Span 和 co 的最大胜利是避免堆分配和复制大型数据结构,而不是避免复制小型数据结构(有更好的技术,比如将大量数据打包成数组)。当然,在任何情况下,它们都不会让代码“干净”——在性能分析表明重要的地方使用它们。
  • 谢谢 Mario Vernari 和 Jeroen Mostert。实际的结构要大得多。我在内存映射文件中有数百万个这个结构的“实例”,我需要尽快访问它。

标签: c# .net-core


【解决方案1】:

看起来像这样:

// The following code will work from C# 7.3 and up, no unsafe keyword required
Span<MyStruct> span = stackalloc MyStruct[1];
span[0] = new MyStruct(3, 4);
var bytes = MemoryMarshal.Cast<MyStruct, byte>(span);

如果我们想将其作为属性公开,我们可以尝试以下方法:

// Will not work at runtime
public ReadOnlySpan<byte> Span
{
    get
    {
        unsafe
        {
            fixed (MyStruct* ptr = &this)
            {
                return new ReadOnlySpan<byte>(ptr, sizeof(MyStruct)); // If on the heap, we're doomed as returning will unpin the memory.
            }
        }
    }
}

并将结构标记为readonly ref struct,这再次保护了我们在堆上的结构。这会编译,但不会在运行时获得AccessViolationException 时运行。我会做更多的挖掘,看看它是否可能,这在逻辑上应该是安全的,但今天可能不可能。

另一种折衷的解决方案是将其保留为readonly struct(而不是ref struct)并添加此静态方法:

public static unsafe ReadOnlySpan<byte> GetSpan(ref MyStruct myStruct)
{
    return new ReadOnlySpan<byte>(Unsafe.AsPointer(ref myStruct), sizeof(MyStruct));
}

然后从调用代码:

var myStruct = new MyStruct(1, 2);
var span = MyStruct.GetSpan(ref myStruct);

我们可以通过将其移出到 ref 扩展方法(C# 7.2 的一个特性)来改进它的使用:

class Program
{
    static void Main()
    {
        var myStruct = new MyStruct(1, 2);
        var span = myStruct.GetSpan();
    }
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct MyStruct
{
    public readonly int Field1;
    public readonly int Field2;

    public MyStruct(int field1, int field2) => (Field1, Field2) = (field1, field2);
}

public static class MyStructExtensions
{
    public static unsafe ReadOnlySpan<byte> GetSpan(ref this MyStruct myStruct)
    {
        return new ReadOnlySpan<byte>(Unsafe.AsPointer(ref myStruct), sizeof(MyStruct));
    }
}

【讨论】:

  • 这确实创建了一个 ReadOnlyStruct,但不幸的是,这在这里不是很有帮助,因为它创建了结构的副本,我想避免使用 readonly 结构。我需要 ReadOnlyStruct 直接作为结构底层内存的视图。
  • 这样做是非常不安全的,因为你不知道结构在哪里(堆栈与堆),如果你把它设为ref struct,你可以保证它只是堆栈,并且不假设您已经被一些不安全的代码预先固定,这样做是安全的。
  • 已更新,我会继续调查。
  • @StefanSchultze 你认为复制发生在哪里,我查看了Memory.Cast&lt;&gt;,看起来那里没有复制?
  • 不在 Memory.Cast 中,而是在设置 stackalloc 数组的第一个元素时。我的问题示例代码中的 Span getter 在没有“unsafe”关键字的情况下工作,所以我认为它是安全的,即 ReadOnlySpan 永远不会超过结构的生命周期。所以我假设我可以对 readonly 结构做同样的事情,因为 ReadOnlySpan 不允许修改底层内存。
【解决方案2】:

以下代码不安全。

见斯图尔特评论:

这样做是非常不安全的,因为你不知道结构在哪里 生活(堆栈与堆),如果你把它做成一个 ref 结构,你可以 保证它只是堆栈,并且在不假设的情况下是安全的 你已经被一些不安全的代码预先锁定了。

MemoryMarshal.CreateReadOnlySpan 方法has a warning too.

现在是代码

您可以分两步实现。

  • 将您的结构放在span&lt;T&gt; 中,其中T 是您的结构的类型。
  • span&lt;T&gt; 转换为span&lt;byte&gt;

    var s = new MutableStruct();
    var span = MemoryMarshal.CreateReadOnlySpan(ref s, 1);
    var byteSpan = MemoryMarshal.Cast<MutableStruct, byte>(span);
    
    Console.WriteLine(string.Join("", byteSpan.ToArray().Select(v => $"{v:X2}"))); // 00000000
    
    s.SetX(42);
    
    Console.WriteLine(string.Join("", byteSpan.ToArray().Select(v => $"{v:X2}"))); // 2A0000000
    
    Console.ReadKey();
    
    // here the used struct
    struct MutableStruct
    {
        private int _x;
        public int X => _x;
        public void SetX(int value) => _x = value;
    }
    

你可以测试一下here

【讨论】:

    【解决方案3】:

    如果您查看MemoryMarshal.CreateReadOnlySpan 的源代码,您会看到,它只是通过传递 reference 创建了一个新的 ReadOnlySpan 实例长度参数。

    简单的解决方案是通过传递数组自己创建 ReadOnlySpan 实例:

    ReadOnlySpan<MyStruct> temp = new ReadOnlySpan<MyStruct>(new MyStruct[] { this });
    

    这将使用不同的ctor,但实现是相似的,你会得到相同的结果。

    编辑:

    或者使用不安全:

    public ReadOnlySpan<byte> Span
    {
        get
        {
            unsafe
            {
    
                return new ReadOnlySpan<byte>(Unsafe.AsPointer(ref this), 1);
            }
        }
    }
    

    【讨论】:

    • 这将创建一个堆分配数组
    • 您的更新是我的第三条建议的不太正确的版本?,您希望长度达到MyStruct 的大小(以字节为单位)。
    • 其实不是。长度是 ReadOnlySpan 将包含的项目数。请阅读源代码。
    • 我很抱歉?
    • 但是 ReadOnlySpan 的类型是byte,所以它的字节大小是MyStruct
    【解决方案4】:

    只是在这里添加一个通用的解决方案,基于之前的答案:

    public class StructExtensions
    {
        public static unsafe ReadOnlySpan<byte> AsSpan<T>(ref T myStruct) where T : struct
        {
            return MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateReadOnlySpan(ref myStruct, 1));
        }
    }
    

    你确实像这样使用它:

    var p = StructExtensions.AsSpan<MyStruct>(ref this /* or whatever your variable is */);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-23
      • 2022-11-15
      • 2011-07-26
      相关资源
      最近更新 更多