就地使用 Vector2f 结构数组作为浮点数数组

Use an array of Vector2f structs as an array of floats in-place

我有一个数组 Vector2f 结构,每个结构包含两个浮点数,我想将它传递给一个接受浮点数数组的函数。这些结构表示 2d 坐标,我希望最终结果为 [x0, y0, x1, y1, ... xn, yn]。一些代码来演示:

using System;
using System.Runtime.InteropServices;

public class Test
{
    [StructLayout(LayoutKind.Sequential)]
    public struct Vector2f
    {
        float x;
        float y;

        public Vector2f(float x, float y)
        {
            this.x = x;
            this.y = y;
        }
    }

    public static void Main()
    {
        Vector2f[] structs = new Vector2f[]
        {
            new Vector2f(1f, 2f),
            new Vector2f(3f, 4f)
        };

        // I want this to contain 1f, 2f, 3f, 4f
        // But Syntax error, cannot convert type!
        float[] floats = (float[])structs;
    }
}

通过将内容复制到一个新的浮点数组中很容易,但是数据会变大,最好不要复制它。

由于内存布局,这可能无法实现。

    Vector2f[] structs = new Vector2f[]
    {
        new Vector2f(1f, 2f),
        new Vector2f(3f, 4f)
    }

也许试试这个。

也许您应该尝试从数组中取出 1f、2f、3f 和 4f,然后将其放入浮点数组中。

转换它的一种方法是实现枚举:

IEnumerable<Single> GetFloats(IEnumerable<Vector2f> vectorList)
{
    foreach (var vect in vectorList)
    {
        yield return vect.x;
        yield return vect.y;
    }
}

但这仍然会产生很多副本:一份到 var 一份到每个值(当屈服时)。

Struct 是值类型,因此无论如何您都必须进行复制。

我了解到您希望能够将同一块内存转换为一种新类型。但不幸的是,对于您的情况,C# 中的数组不仅仅是指向一块内存的指针。还有一些与之关联的元数据(如数组的 Length )和一些特殊的内存对齐方式。所以我认为不可能这样做。

编辑: 即使你能做到(这远非我的专业知识,但也许有一些 IL 代码发出它是可能的),你可能会遇到问题托管内存已处理:它可以移动很多次。它在内存中没有静态位置。垃圾收集器在每次生成收集后压缩内存(不是每次,但仍然)。

虽然这是一个有趣的问题。我希望其他专家在这里给我们一些启发 ;)

编辑: 一种更有效的避免垃圾收集的方法,就像评论中建议的那样:

float[] GetFloats(Vector2f[] vectorArray)
{
    var floats = new float[vectorArray.Length*2];
    for (int i = 0; i < vectorArray.Length; ++i)
    {
        floats[i*2] = vectorArray[i].x;
        floats[i*2 + 1] = vectorArray[i].y;
    }

    return floats;
}

编辑: 仍然不是您问题的直接答案,而是使用指针的更有效的副本,改编自 Unsafe Code Tutorial(请注意,这需要使用 /unsafe):

static unsafe void FastCopy(Vector2f[] src, float[] dst, Int32 count)
{
    if (src == null || dst == null)
    {
        throw new ArgumentException();
    }

    int srcLen = src.Length;
    int dstLen = dst.Length;
    if (srcLen < count ||
        dstLen < count*2)
    {
        throw new ArgumentException();
    }
    // The following fixed statement pins the location of
    // the src and dst objects in memory so that they will
    // not be moved by garbage collection.          
    fixed (Vector2f* pSrc = src)
    {
        fixed (float* pDst = dst)
        {
            byte* ps = (byte*)pSrc;
            byte* pd = (byte*)pDst;
            count *= 8;

            // Loop over the count in blocks of 4 bytes, copying a
            // float (4 bytes) at a time:
            for (int n = 0; n < count/4; n++)
            {
                *((float*)pd) = *((float*)ps);
                pd += 4;
                ps += 4;
            }
        }
    }
}

如果您实际上不需要传递真正的数组,而只是需要像数组一样可以访问的东西,您可以这样做(未经测试):

public sealed class FloatArrayAdaptor : IReadOnlyList<float>
{
    private Vector2f[] _data;

    public FloatArrayAdaptor(Vector2f[] data)
    {
        _data = data;
    }

    public IEnumerator<float> GetEnumerator()
    {
        for (int i = 0; i < _data.Length; i++)
        {
            yield return _data[i].x;
            yield return _data[i].y;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public int Count
    {
        get { return 2*_data.Length; }
    }

    public float this[int index]
    {
        get
        {
            //TODO: Add appropriate range checking and whatnot
            int i = index>>1;
            bool isX = (index & 0x1) == 0;
            return isX ? _data[i].x : _data[i].y;
        }
    }
}

您将无法像在 C 中那样在 C# 中 'recast' 类型。最接近的方法是使用不安全代码并获取实际的浮点数*,但即便如此您也可以' 将该指针视为传递给采用数组的方法的安全数组。


我做了一些实验,'convert' 类型的不安全代码是可能的,但这是一个非常非常糟糕的 hack,你实际上不应该这样做。然而,它展示了一些关于 CLR object header data:

的有趣的事情
    public static unsafe void Main()
    {
        Vector2f[] data = new Vector2f[10];
        float[] dummy = new float[1];

        //NOTE: This is horrible and you should never actually do it
        //After this code, the original data array cannot be used safely anymore
        fixed (void* rawData = &data[0])
        fixed (void* rawDummy = &dummy[0])
        {
            int* intData = (int*)rawData;
            int* intDummy = (int*)rawDummy;

            //method table pointer is at X-4-sizeof(IntPtr)
            //This is what identifies the type via RTTI
            //We're going to forge our identity and change our size to change our type
            //This assumes x86
            intData[-2] = intDummy[-2];

            //Our length is doubled
            intData[-1] = 2*intData[-1];
        }

        if (data.GetType() == typeof(float[]))
            Console.WriteLine("Type is now float[]!");

        float[] floatData = (float[])(object)data;

        Console.ReadLine();
    }

基本上我们替换了方法 table 指针,这样类型现在看起来是 float[],然后我们将数组的长度字段加倍以进行补偿。这会编译、运行并报告类型现在是 float[]。也就是说,这很可能会在以后以某种惊人的方式破坏 GC,而且它肯定非常依赖于实现,而且这不涉及 x64 与 x86。尽管如此,还是很有趣...不过,将其称为 'unsafe' 是有原因的。希望这有助于说明为什么不能以安全的方式支持它,因为 RTTI(通过方法 table 指针)被烘焙到存储数据的内存中。

现在尝试回答为什么不可能。

C# 是一种类型安全的语言。这意味着在兼容类型上只允许某些转换(强制转换)。这解释了为什么不允许使用此代码:

Vector2f[] structs;
float[] floats = (float[])structs;

的确,C# 使用引用而不是指针。其中一个区别是引用不是内存中的静态位置。垃圾回收期间可以移动对象。

但是,C# 允许使用 unsafe 代码进行一些指针运算。为此,必须通知垃圾收集器不应为所考虑的对象移动内存(并且不得使引用无效)。这是通过 fixed 关键字完成的。

换句话说,要获得指向引用对象的指针(结构体的相同逻辑),您首先需要冻结内存中的对象位置(这也称为固定内存):

fixed (Vector2f* pStructs = structs)
fixed (float* pFloats = floats)
{
    ...

现在一切都已修复,您不能更改这些指针的地址。例如,这是不允许的:

pFloats = (float*)pStructs // this will change the address of pFloats which is fixed: illegal

此外,您不能将指针转换回引用:

float[] floats = (float[])pFloats; // not allowed

总之,一旦获得指针,就可以将一些字节从一个位置移动到另一个位置,但不能更改相应引用的位置(只能移动数据)。

希望这能回答您的问题。

旁注,如果你有很多性能关键的操作,你可以考虑用C++实现,暴露一些高级函数并从中调用一些函数C#.