使用protobuf-net序列化巨大的复合图表列表,导致内存异常

时间:2013-04-03 17:40:06

标签: c# serialization protobuf-net large-data-volumes

我正在尝试使用Protobuf-net序列化包含非常大的复合对象图(~200000个或更多节点)列表的对象。基本上我想要实现的是将整个对象尽可能快速和紧凑地保存到单个文件中。

我的问题是我在尝试序列化对象时遇到了内存异常。在我的机器上,当文件大小约为1.5GB时抛出异常。我正在运行64位进程并使用StreamWriter作为protobuf-net的输入。由于我直接写入文件,我怀疑protobuf-net中发生了某种缓冲,导致异常。我曾尝试使用DataFormat = DataFormat.Group属性但到目前为止没有运气。

我可以通过将列表中的每个复合数据序列化为单独的文件来避免异常,但我希望尽可能一次完成所有这些。

我做错了什么或者根本不可能实现我想要的东西?

用于说明问题的代码:

class Program
{
    static void Main(string[] args)
    {
        int numberOfTrees = 250;
        int nodesPrTree = 200000;

        var trees = CreateTrees(numberOfTrees, nodesPrTree);
        var forest = new Forest(trees);

        using (var writer = new StreamWriter("model.bin"))
        {
            Serializer.Serialize(writer.BaseStream, forest);
        }

        Console.ReadLine();
    }

    private static Tree[] CreateTrees(int numberOfTrees, int nodesPrTree)
    {
        var trees = new Tree[numberOfTrees];
        for (int i = 0; i < numberOfTrees; i++)
        {
            var root = new Node();
            CreateTree(root, nodesPrTree, 0);
            var binTree = new Tree(root);
            trees[i] = binTree;
        }
        return trees;
    }

    private static void CreateTree(INode tree, int nodesPrTree, int currentNumberOfNodes)
    {
        Queue<INode> q = new Queue<INode>();
        q.Enqueue(tree);
        while (q.Count > 0 && currentNumberOfNodes < nodesPrTree)
        {
            var n = q.Dequeue();
            n.Left = new Node();
            q.Enqueue(n.Left);
            currentNumberOfNodes++;

            n.Right = new Node();
            q.Enqueue(n.Right);
            currentNumberOfNodes++;
        }
    }
}

[ProtoContract]
[ProtoInclude(1, typeof(Node), DataFormat = DataFormat.Group)]
public interface INode
{
    [ProtoMember(2, DataFormat = DataFormat.Group, AsReference = true)]
    INode Parent { get; set; }
    [ProtoMember(3, DataFormat = DataFormat.Group, AsReference = true)]
    INode Left { get; set; }
    [ProtoMember(4, DataFormat = DataFormat.Group, AsReference = true)]        
    INode Right { get; set; }
}

[ProtoContract]
public class Node : INode
{
    INode m_parent;
    INode m_left;
    INode m_right;

    public INode Left
    {
        get
        {
            return m_left;
        }
        set
        {
            m_left = value;
            m_left.Parent = null;
            m_left.Parent = this;
        }
    }

    public INode Right
    {
        get
        {
            return m_right;
        }
        set
        {
            m_right = value;
            m_right.Parent = null;
            m_right.Parent = this;
        }
    }

    public INode Parent
    {
        get
        {
            return m_parent;
        }
        set
        {
            m_parent = value;
        }
    }
}

[ProtoContract]
public class Tree
{
    [ProtoMember(1, DataFormat = DataFormat.Group)]
    public readonly INode Root;

    public Tree(INode root)
    {
        Root = root;
    }
}

[ProtoContract]
public class Forest
{
    [ProtoMember(1, DataFormat = DataFormat.Group)]
    public readonly Tree[] Trees;

    public Forest(Tree[] trees)
    {
        Trees = trees;
    }
}

抛出异常时的堆栈跟踪:

at System.Collections.Generic.Dictionary`2.Resize(Int32 newSize, Boolean forceNewHashCodes)
at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
at ProtoBuf.NetObjectCache.AddObjectKey(Object value, Boolean& existing) in NetObjectCache.cs:line 154
at ProtoBuf.BclHelpers.WriteNetObject(Object value, ProtoWriter dest, Int32 key, NetObjectOptions options) BclHelpers.cs:line 500
at proto_5(Object , ProtoWriter )

我正在尝试使用SerializeWithLengthPrefix方法将一个树的数组一次序列化为一个文件。序列化似乎工作 - 我可以看到在列表中的每个树添加到文件后文件大小增加。但是,当我尝试反序列化树时,我得到了无效的线型异常。当我序列化树时,我正在创建一个新文件,因此该文件应该是无垃圾的 - 除非我写的是垃圾原因;-)。我的序列化和反序列化方法如下所列:

using (var writer = new FileStream("model.bin", FileMode.Create))
{
    foreach (var tree in trees)
    {
        Serializer.SerializeWithLengthPrefix(writer, tree, PrefixStyle.Base128);
    }
}

using (var reader = new FileStream("model.bin", FileMode.Open))
{
    var trees = Serializer.DeserializeWithLengthPrefix<Tree[]>>(reader, PrefixStyle.Base128);
}

我是否以错误的方式使用该方法?

1 个答案:

答案 0 :(得分:0)

帮助 AsReference代码只是尊重默认数据格式,这意味着它试图将数据保存在内存中,以便它可以写入对象长度前缀回数据流,这正是我们想要的(因此您正确使用DataFormat.Group)。这将解释树的单个分支的缓冲。我已经在本地调整了它,我肯定可以确认它现在只是向前编写(调试版本有一个方便的ForwardsOnly标志,我可以启用它来检测这个并发出呼喊声。)

通过这个调整,我已经让它工作了250 x 20,000,但是当我处理250 x 200,000时,我的字典调整大小(即使在x64中)也会出现问题 - 就像你说的那样,大约1.5GB水平。但是,我发现在执行每个序列化/反序列化时,我可以分别丢弃其中一个(转发或反向)。我会对堆栈跟踪感兴趣 - 如果最终字典调整大小,我可能需要考虑转移到的字典...