使用Unsafe和Fixed从属性返回Span

3

我在工作中遇到了类似下面这样的东西。我以前从未使用过如此频繁地使用结构体的 C# 代码库。

我之前使用过 fixed ,以防止垃圾回收器在我使用指针进行不安全操作时移动对象。但我从未见过它被用于将指针传递给 Span,然后在 fixed 语句之外使用 Span。

这样可以吗?我猜因为 Span 是托管的,一旦我们将位置传递给它,如果 GC 移动 MyStruct 的位置,那么它应该能正确获取新位置,对吗?

[StructLayout(LayoutKind.Sequential)]
public unsafe struct MyInnerStruct
{
    public uint InnerValueA;
    public uint InnerValueB;
    public float InnerValueC;
    public long InnerValueD;
}

[StructLayout(LayoutKind.Sequential)]
public unsafe struct MyStruct
{
    public MyInnerStruct Value0;
    public MyInnerStruct Value1;
    public MyInnerStruct Value2;
    public MyInnerStruct Value3;
    public MyInnerStruct Value4;
    public MyInnerStruct Value5;
    public MyInnerStruct Value6;
    public MyInnerStruct Value7;
    public MyInnerStruct Value8;
    public MyInnerStruct Value9;

    public int ValidValueCount;

    public Span<MyInnerStruct> Values
    {
        get
        {
            fixed (MyInnerStruct* ptr = &Value0)
            {
                return new Span<MyInnerStruct>(ptr, ValidValueCount);
            }
        }
    }
}

1
从文档中提供了这样的描述:提供了对任意内存连续区域的类型和内存安全的表示。Span<T> 是一个 ref 结构,它是在堆栈上分配而不是在托管堆上分配的。由于 MyStruct 也是一个结构体,因此它也不会在堆上,所以我认为不涉及移动。 - rene
1
我认为@rene所说的第二部分很重要。对于堆类型,这可能不是安全的方法,并且需要额外的GCHandle.Alloc - Charles
2
可以说,这段代码非常危险,只有在使用本地变量或参数时才是安全的。GC 不会正确处理它,Span<T> 只设计用于本地变量,不应该以这种方式使用。 - Charlieface
@Charlieface 这显然对我来说并不是安全的。但为什么把MyStruct作为MyClass的成员放在堆上,然后在某个函数内部将Span用在栈上不起作用呢?一旦创建了指向正确位置的托管Span,并且gc移动了MyClass(以及其中的MyStruct),那么Span中引用的位置也会被更新吧? - Michael Covelli
1
可能是安全的,但是 MyStruct 不知道它的存储位置,所以不能保证。你真的应该从包含对象中执行此操作。你也可以使用 return MemoryMarshal.CreateSpan<MyInnerStruct>(ref Value0, ValidValueCount); 来避免使用 unsafe,尽管同样需要注意:你需要小心不要泄漏堆栈指针。 - Charlieface
@Clarlieface 我喜欢这个。我以前从未见过这个。但是从 https://github.com/dotnet/corert/blob/master/src/System.Private.CoreLib/shared/System/Runtime/InteropServices/MemoryMarshal.cs 看来,它只是实现为public static Span<T> CreateSpan<T>(ref T reference, int length) => new Span<T>(ref reference, length); - Michael Covelli
2个回答

2
这是不安全的。
从方法返回Span<T>在一般情况下是不安全的。只有在保证它所引用的对象不会超出作用域且不会被移动时,才能正常工作。这些条件仅适用于存活时间最长与Span本身相同的堆栈变量。对于上面的代码,请考虑以下示例:
public Span<MyInnerStruct> Foo()
{
    MyStruct s = default;
    s.Value0.InnerValueA = 3;
    s.Values[1].InnerValueB = 4; // this is fine
    return s.Values; // Peng! We're returning a reference to s, which is illegal
}

通过使用指针来初始化 Span,运行时也将失去跟踪其指向的引用的所有手段。因此,如果 MyStruct 是对象的一部分,则在下一次 GC 运行后很容易出现悬挂引用。.NET 5.0 及以上版本有一个名为 MemoryMarshal.CreateSpan 的方法,可以解决一些问题,但仍然存在使用上的危险。
可以在 这里 找到有关此类结构的问题的详细分析。讨论的结构体 ValueArray 看起来与 OP 给出的示例非常相似。最终,决定将其从代码库中删除,因为其使用太危险了。

1

这是.NET 6的源代码

public unsafe Span(void* pointer, int length)
{
    _pointer = new ByReference<T>(ref Unsafe.As<byte, T>(ref *(byte*)pointer));
    _length = length;
}

您的代码:

fixed (MyInnerStruct* ptr = &Value0)
{
    return new Span<MyInnerStruct>(ptr, ValidValueCount);
}

我做了一些测试,像你这样使用span总是安全的,即使PTR被垃圾收集器移动。

我还测试了在ptr移动后向Span写入内容,原始对象仍然被正确更新。

由于ByReferenceSpan将始终返回正确的值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接