2012-07-03 8 views
8

En nuestra aplicación tenemos algunas estructuras de datos que, entre otras cosas, contienen una lista fragmentada de bytes (actualmente expuesta como List<byte[]>). Recopilamos los bytes porque si permitimos que las matrices de bytes se coloquen en el gran montón de objetos, con el tiempo sufriremos fragmentación de la memoria.Uso de memoria serialización de matrices de bytes fragmentados con Protobuf-net

También hemos comenzado a usar Protobuf-net para serializar estas estructuras, utilizando nuestra propia DLL de serialización generada.

Sin embargo, hemos notado que Protobuf-net crea buffers en memoria muy grandes durante la serialización. Echando un vistazo a través del código fuente, parece que quizás no pueda vaciar su búfer interno hasta que se haya escrito la totalidad de la estructura List<byte[]> porque después necesita escribir la longitud total en la parte delantera del búfer.

Desafortunadamente, deshacemos nuestro trabajo de dividir los bytes en primer lugar, y finalmente nos da OutOfMemoryExceptions debido a la fragmentación de la memoria (la excepción ocurre en el momento en que Protobuf-net intenta expandir el buffer a más de 84k, lo que obviamente lo pone en LOH, y nuestro uso general de la memoria de proceso es bastante bajo).

Si mi análisis de cómo funciona Protobuf-net es correcto, ¿hay alguna forma de solucionar este problema?


actualización

Sobre la base de la respuesta de Marc, esto es lo que he intentado:

[ProtoContract] 
[ProtoInclude(1, typeof(A), DataFormat = DataFormat.Group)] 
public class ABase 
{ 
} 

[ProtoContract] 
public class A : ABase 
{ 
    [ProtoMember(1, DataFormat = DataFormat.Group)] 
    public B B 
    { 
     get; 
     set; 
    } 
} 

[ProtoContract] 
public class B 
{ 
    [ProtoMember(1, DataFormat = DataFormat.Group)] 
    public List<byte[]> Data 
    { 
     get; 
     set; 
    } 
} 

Luego de serializarlo:

var a = new A(); 
var b = new B(); 
a.B = b; 
b.Data = new List<byte[]> 
{ 
    Enumerable.Range(0, 1999).Select(v => (byte)v).ToArray(), 
    Enumerable.Range(2000, 3999).Select(v => (byte)v).ToArray(), 
}; 

var stream = new MemoryStream(); 
Serializer.Serialize(stream, a); 

Sin embargo, si me quedo un punto de interrupción en ProtoWriter.WriteBytes() donde llama DemandSpace() hacia la parte inferior del método y el paso en DemandSpace(), puedo ver que el búfer no se vacía porque writer.flushLock es igual a 1.

Si creo otra clase base para Abase así:

[ProtoContract] 
[ProtoInclude(1, typeof(ABase), DataFormat = DataFormat.Group)] 
public class ABaseBase 
{ 
} 

[ProtoContract] 
[ProtoInclude(1, typeof(A), DataFormat = DataFormat.Group)] 
public class ABase : ABaseBase 
{ 
} 

Entonces writer.flushLock es igual a 2 en DemandSpace().

Supongo que hay un paso obvio que he perdido aquí para hacer con los tipos derivados?

Respuesta

5

Voy a leer entre algunas líneas aquí ... porque List<T> (asignada como repeated en la jerga protobuf) no tiene una longitud de prefijo global, y byte[] (asignada como bytes) tiene una longitud trivial-prefix eso no debería causar buffering adicional. Así que supongo que lo que en realidad tienen es más como:

[ProtoContract] 
public class A { 
    [ProtoMember(1)] 
    public B Foo {get;set;} 
} 
[ProtoContract] 
public class B { 
    [ProtoMember(1)] 
    public List<byte[]> Bar {get;set;} 
} 

Aquí, la necesidad de amortiguar para una longitud de prefijo es en realidad al escribir A.Foo, básicamente a Declaro "los siguientes datos complejos es el valor para A.Foo ").Afortunadamente, existe una solución sencilla:

[ProtoMember(1, DataFormat=DataFormat.Group)] 
public B Foo {get;set;} 

Esto cambia entre las 2 técnicas de embalaje en protobuf:

  • el valor por defecto (preferencias declaradas de Google) es la longitud prefijada, así que le darán un marcador que indica la longitud de el mensaje a seguir, a continuación, la carga útil de sub-mensaje
  • pero también hay una opción para utilizar un marcador de inicio, la carga útil de sub-mensaje, y un marcador final

Al utilizar la segunda técnica no es necesario almacenar en el buffer, entonces: no. Esto significa que escribirá bytes ligeramente diferentes para los mismos datos, pero protobuf-net es muy indulgente y deserializará felizmente los datos del formato aquí. Significado: si realiza este cambio, aún puede leer sus datos existentes, pero los datos nuevos utilizarán la técnica de inicio/finalización.

Esto exige la pregunta: ¿por qué google prefiere el enfoque de prefijo de longitud? Probablemente esto es porque es más eficiente al leer para omitir los campos (ya sea mediante una API de lector sin procesar, o como datos no deseados/inesperados) cuando se utiliza el método de prefijo de longitud, ya que puede leer el prefijo de longitud , y luego solo progresa la secuencia [n] bytes; por el contrario, para omitir datos con un marcador de inicio/finalización, debe seguir rastreando la carga, omitiendo los subcampos individualmente. Por supuesto, esta diferencia teórica en el rendimiento de lectura no se aplica si usted espera ese dato y desea leerlo en su objeto, lo cual es casi seguro que haga. Además, en la implementación de protobuf de Google, debido a que no está funcionando con un modelo POCO regular, el tamaño de las cargas útiles ya se conoce, por lo que realmente no ven el mismo problema al escribir.

+0

Gracias por la respuesta rápida. Su suposición acerca de nuestra estructura de datos era correcta. ¿Tendría razón al decir que necesitamos cambiar el DataFormat a Group para cualquier propiedad que contenga una referencia a A, y así sucesivamente hasta la raíz del gráfico de objetos? ¿Y este cambio también debería estar en los atributos relevantes de ProtoInclude también? –

+0

@Juegos esencialmente, sí. Hmmm ... ¡Tal vez debería agregar un valor predeterminado de modelo para eso! –

+0

He actualizado mi pregunta con mi intento de utilizar DataFormat.Group para resolver el problema, pero sigo teniendo problemas para que el buffer se descargue. Disculpas si soy un idiota ... –

2

Adicional a su edición; el [ProtoInclude(..., DataFormat=...)] parece que simplemente no se estaba procesando. He añadido una prueba de esto en mi actual de la compilación local, y que ahora pasa:

[Test] 
public void Execute() 
{ 

    var a = new A(); 
    var b = new B(); 
    a.B = b; 

    b.Data = new List<byte[]> 
    { 
     Enumerable.Range(0, 1999).Select(v => (byte)v).ToArray(), 
     Enumerable.Range(2000, 3999).Select(v => (byte)v).ToArray(), 
    }; 

    var stream = new MemoryStream(); 
    var model = TypeModel.Create(); 
    model.AutoCompile = false; 
#if DEBUG // this is only available in debug builds; if set, an exception is 
    // thrown if the stream tries to buffer 
    model.ForwardsOnly = true; 
#endif 
    CheckClone(model, a); 
    model.CompileInPlace(); 
    CheckClone(model, a); 
    CheckClone(model.Compile(), a); 
} 
void CheckClone(TypeModel model, A original) 
{ 
    int sum = original.B.Data.Sum(x => x.Sum(b => (int)b)); 
    var clone = (A)model.DeepClone(original); 
    Assert.IsInstanceOfType(typeof(A), clone); 
    Assert.IsInstanceOfType(typeof(B), clone.B); 
    Assert.AreEqual(sum, clone.B.Data.Sum(x => x.Sum(b => (int)b))); 
} 

Este cometen está ligado a algunos otros refactorizaciones, no relacionados (algunos de retrabajo para WinRT/IKVM), pero deberían comprometerse lo antes posible.

Cuestiones relacionadas