Сериализовать список огромных составных графов, используя protobuf-net, вызывая исключение нехватки памяти

Я пытаюсь сериализовать объект, содержащий список очень больших составных графов объектов (~200000 узлов или более), используя Protobuf-net. По сути, я хочу сохранить объект целиком в один файл как можно быстрее и компактнее.

Моя проблема заключается в том, что я получаю исключение нехватки памяти при попытке сериализации объекта. На моей машине выдается исключение, когда размер файла составляет около 1,5 ГБ. Я использую 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;
    }
}

Stack-trace при возникновении исключения:

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. Сериализация, кажется, работает - я вижу, что размер файла увеличивается после добавления каждого дерева в списке в файл. Однако, когда я пытаюсь десериализовать деревья, я получаю исключение Invalid проводного типа. Я создаю новый файл, когда я сериализую деревья, поэтому файл должен быть без мусора - если я не пишу мусор по причине;-). Мои методы сериализации и десериализации перечислены ниже:

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 ответ

Это не помогло, что AsReference В коде учитывался только формат данных по умолчанию, что означает, что он пытался сохранить данные в памяти, чтобы он мог записать префикс длины объекта обратно в поток данных, а это именно то, чего мы здесь не хотим (следовательно, ваш правильное использование DataFormat.Group). Это будет учитывать буферизацию для отдельной ветви дерева. Я настроил его локально, и я точно могу подтвердить, что теперь он пишет только для пересылки (отладочная сборка имеет удобную ForwardsOnly флаг, который я могу включить, который обнаруживает это и кричит).

Благодаря этой настройке у меня это работает на 250 x 20000, но у меня возникают второстепенные проблемы с изменением размера словаря (даже в x64) при работе с 250 x 200 000 - как вы говорите, на уровне около 1,5 ГБ. Однако мне приходит в голову, что я мог бы отказаться от одного из них (прямого или обратного) соответственно при выполнении каждой из сериализации / десериализации. Я был бы заинтересован в трассировке стека, когда он ломается для вас - если в конечном итоге это изменение размера словаря, мне, возможно, придется подумать о переходе на группу словарей...

Другие вопросы по тегам