使用MemoryMappedViewAccessor写入压缩结构体时出现问题?

7
我有一个结构体,它是用Pack=1定义的,长度为29个字节。如果不打包,则长度为32个字节。
  • Marshal.SizeOf(TypeOf(StructName)) 返回29。

  • StructName struct; sizeof(struct) 返回32。

当我使用MemoryMappedViewAccessor写出这个结构体时,它会写出32个字节,而不是29个字节。

所以,除了将结构体编组为字节数组并以这种方式写出之外,是否有任何方法可以使其正确地写出该结构体呢?

更多细节:如果使用显式布局,则Write实际上只会写出29个字节。但是,WriteArray对于每个元素都会写出32个字节。

而且,是的,仔细的字节序列化可能有效,但(我没有对其进行分析,但我猜测)它可能比WriteArray慢数个数量级,不是吗?


你是否定义了布局属性 S.A [StructLayout(LayoutKind.Explicit)]?你必须这样做以避免填充。 - avishayp
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 1)] - 根据文档,这应该消除填充(并确实消除了),但是无论是 sizeof 还是 MemoryMappedViewAccessor 都没有承认。顺序布局的文档说:Sequential对象的成员按照它们在导出到非托管内存时出现的顺序依次排列。成员根据 StructLayoutAttribute.Pack 中指定的对齐方式进行布局,并且可以是不连续的。 - MikeMedved
这是关于MMFs的实现细节,它使用Marshal.AlignedSizeOf<T>(),而不考虑打包。 - Hans Passant
@Hans 但实际的内存映射也不是紧凑的。 - avishayp
嗯,内存映射是由你自己决定的。除非你尝试将映射细分并使子部分对齐,否则它是32字节而不是29字节并不重要。是的,在Pack=1的情况下会出现问题,它不能假设如果一个结构体具有Pack=1,则所有其他结构体也具有它。总是像那样打包是个坏主意,8的默认值有很好的理由。我想OP被留下了一个他无法控制的糟糕选择。你重新陈述了他已经知道的内容,请自行编排。 - Hans Passant
Hans,这个结构体紧密打包的原因是我将在文件中存储数百万个它们。结构体中多出来的3个字节会转化为兆字节的磁盘空间。另外,“AlignedSizeOf”——它在哪里有文档记录?我找不到。 - MikeMedved
1个回答

6

编辑:好的,我终于明白你真正想问什么了。我们通常不使用MemoryMappedViewAccessor来序列化对象,现在你也知道原因了。

下面的内容将会给你期望的结果。

public static class ByteSerializer
{
    public static Byte[] Serialize<T>(IEnumerable<T> msg) where T : struct
    {
        List<byte> res = new List<byte>();
        foreach (var s in msg)
        {
            res.AddRange(Serialize(s));
        }
        return res.ToArray();
    }

    public static Byte[] Serialize<T>(T msg) where T : struct
    {
        int objsize = Marshal.SizeOf(typeof(T));
        Byte[] ret = new Byte[objsize];

        IntPtr buff = Marshal.AllocHGlobal(objsize);
        Marshal.StructureToPtr(msg, buff, true);
        Marshal.Copy(buff, ret, 0, objsize);
        Marshal.FreeHGlobal(buff);
        return ret;
    }
}

class Program
{
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    struct Yours
    {
        public Int64 int1;
        public DateTime dt1;
        public float f1;
        public float f2;
        public float f3;
        public byte b;
    }

    static void Main()
    {
        var file = @"c:\temp\test.bin";
        IEnumerable<Yours> t = new Yours[3];
        File.WriteAllBytes(file, ByteSerializer.Serialize(t));

        using (var stream = File.OpenRead(file))
        {
            Console.WriteLine("file size: " + stream.Length);
        }
    }
}

编辑: 看起来DateTime非常喜欢对齐内存地址。虽然您可以定义显式布局,但我认为更简单的方法是:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Test
{
    private long dt1; 
    public byte b;
    public Int64 int1;
    public float f1;
    public float f2;
    public float f3;

    public DateTime DT
    {
        get { return new DateTime(dt1); }
        set { dt1 = value.Ticks; }
    }
}

我不太明白为什么你会关心托管内存表示。

或者,[StructLayout(LayoutKind.Explicit)] 应该可以防止内存对齐。

例如('托管 sizeof' 取自这篇文章

[StructLayout(LayoutKind.Explicit, Size = 9)]
public struct Test
{
    [FieldOffset(0)]
    public DateTime dt1;
    [FieldOffset(8)]
    public byte b;
}

class Program
{
    static readonly Func<Type, uint> SizeOfType = (Func<Type, uint>)Delegate.CreateDelegate(typeof(Func<Type, uint>), typeof(Marshal).GetMethod("SizeOfType", BindingFlags.NonPublic | BindingFlags.Static));

    static void Main()
    {
        Test t = new Test() { dt1 = DateTime.MaxValue, b = 42 };
        Console.WriteLine("Managed size: " + SizeOfType(typeof(Test)));
        Console.WriteLine("Unmanaged size: " + Marshal.SizeOf(t));
        using (MemoryMappedFile file = MemoryMappedFile.CreateNew(null, 1))
        using (MemoryMappedViewAccessor accessor = file.CreateViewAccessor())
        {
            accessor.Write(0L, ref t);
            long pos = 0;

            for (int i = 0; i < 9; i++)
                Console.Write("|" + accessor.ReadByte(pos++));
            Console.Write("|\n");
        }
    }
}

输出:

Managed size: 9
Unmanaged size: 9
|255|63|55|244|117|40|202|43|42|   // managed memory layout is as expected

顺便说一下,DateTime似乎破坏了连续性协议 - 但请记住,该协议仅适用于内存映射。对于托管内存布局,没有任何规范。

[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 9)]
public struct Test
{
    public DateTime dt1;
    public byte b;
}

以上代码的输出结果为:

Managed size: 12
Unmanaged size: 9
|42|0|0|0|255|63|55|244|117|40|202|43|   // finally found those 3 missing bytes :-)

谢谢avip,我确实意识到了这一点,它提供了另一种避免将数据转换为字节数组的方法,我现在正在使用它,但是按照文档,为什么顺序布局和Pack=1不像应该那样工作呢? - MikeMedved
你的结构体里面有没有隐藏着4个字符? - avishayp
公共 Int64 int1; 公共 DateTime dt1; 公共 float f1; 公共 float f2; 公共 float f3; 公共 byte b; - MikeMedved
但这是一个3字节的间隙 :) 29与32 - MikeMedved
“虽然我不明白为什么你要关心托管内存表示。” - 我并不是真的关心,我只是想让它真正成为29个字节,以便在空间上节省(文件中将有数百万个这样的内容)。正如我在原始帖子中指出的那样,实际上写出了32个字节。 - MikeMedved

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