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

时间:2015-05-19 18:44:17

标签: c# struct

我有一个数组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;
    }
}

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

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

4 个答案:

答案 0 :(得分:3)

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

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#中的类型。最接近的是使用不安全的代码并获取实际的浮点数*,但即使这样你也无法对待指针就像一个安全数组,用于传递给接受数组的方法。

我做了一些实验,并且有可能使用不安全的代码来“转换”类型,但这是一个可怕的,可怕的黑客,你不应该真的这样做。然而,它演示了一些有关CLR对象标题数据的有趣内容:

    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();
    }

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

答案 1 :(得分:1)

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

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;
            }
        }
    }
}

答案 2 :(得分:1)

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

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#中调用一些函数。

答案 3 :(得分:0)

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

也许尝试一次。

也许你应该尝试从阵列中取出1f,2f,3f和4f,而不是将其放入浮点阵列中。