2012-07-03 7 views
8

Nella nostra applicazione abbiamo alcune strutture di dati che tra le altre cose contengono una lista chunk di byte (attualmente esposta come List<byte[]>). Abbiamo ridotto il numero di byte perché se permettiamo che gli array di byte siano messi sull'heap di oggetti grandi, nel tempo soffriremo della frammentazione della memoria.Utilizzo della memoria serializzazione di array di byte chunked con Protobuf-net

Abbiamo anche iniziato a utilizzare Protobuf-net per serializzare queste strutture, utilizzando la nostra DLL di serializzazione generata.

Tuttavia, abbiamo notato che Protobuf-net sta creando buffer di memoria molto grandi durante la serializzazione. Osservando il codice sorgente, sembra che forse non è in grado di svuotare il buffer interno fino a quando non è stata scritta l'intera struttura List<byte[]>, poiché in seguito è necessario scrivere la lunghezza totale nella parte anteriore del buffer.

Questo purtroppo annulla il nostro lavoro con il chunking dei byte in primo luogo, e alla fine ci fornisce OutOfMemoryExceptions a causa della frammentazione della memoria (l'eccezione si verifica nel momento in cui Protobuf-net sta tentando di espandere il buffer a oltre 84k, che ovviamente lo mette sul LOH e il nostro utilizzo complessivo della memoria di processo è piuttosto basso).

Se la mia analisi di come funziona Protobuf-net è corretta, c'è un modo per aggirare questo problema?


Aggiornamento

in base alla risposta di Marc, ecco che cosa ho provato:

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

Poi per serializzare esso:

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

Tuttavia, se mi attengo un punto di interruzione in ProtoWriter.WriteBytes() dove chiama DemandSpace() verso il basso del metodo e passaggio in DemandSpace(), posso vedere che il buffer non viene svuotato perché writer.flushLock equivale a 1.

Se creo un'altra classe di base per umiliare in questo modo:

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

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

Poi writer.flushLock uguale 2 in DemandSpace().

Immagino che ci sia un passo ovvio che mi è mancato fare con i tipi derivati?

risposta

5

ho intenzione di leggere tra alcune righe qui ... perché List<T> (mappata come repeated in protobuf gergo) non dispone di una lunghezza prefisso complessiva, e byte[] (mappata come bytes) ha una banale lunghezza del prefisso quello non dovrebbe causare il buffering supplementare. Così sto cercando di indovinare quello che in realtà avere è più simile a:

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

Qui, la necessità di buffer per una lunghezza prefisso è in realtà quando si scrive A.Foo, fondamentalmente per Dichiaro "i seguenti dati complesso è il valore per A.Foo ").Fortunatamente c'è una soluzione semplice:

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

Questo cambia tra 2 tecniche di imballaggio in protobuf:

  • il default (preferenza dichiarato di Google) è lunghezza prefissata, il che significa che si ottiene un indicatore che indica la lunghezza del il messaggio da seguire, poi il sub-messaggio payload
  • ma c'è anche la possibilità di utilizzare una start-marcatore, il carico utile sub-messaggio e una soluzione end-marcatore

Quando si utilizza la seconda tecnica non è necessario bufferizzare, quindi: non lo fa. Ciò significa che scriverà byte leggermente diversi per gli stessi dati, ma protobuf-net è molto indulgente e deserializzerà felicemente i dati dal formato o qui. Significato: se apporti questa modifica, puoi comunque leggere i tuoi dati esistenti, ma i nuovi dati useranno la tecnica del marker iniziale/finale.

Ciò richiede la domanda: perché google preferisce l'approccio con prefisso di lunghezza? Probabilmente questo è perché è più efficiente quando si legge per saltare i campi (tramite un'API di lettore grezzo o come dati indesiderati/imprevisti) quando si utilizza l'approccio del prefisso di lunghezza, come si può semplicemente leggere il prefisso di lunghezza , e quindi basta far avanzare il flusso [n] byte; Al contrario, per saltare i dati con un indicatore di inizio/fine è ancora necessario eseguire la scansione attraverso il payload, ignorando i campi secondari singolarmente. Naturalmente, questa differenza teorica nelle prestazioni di lettura non si applica se si si aspetta quei dati e si desidera leggerli nel proprio oggetto, cosa che quasi certamente si fa. Inoltre, nell'implementazione di google protobuf, poiché non funziona con un modello POCO regolare, le dimensioni dei payload sono già note, quindi non vedono lo stesso problema durante la scrittura.

+0

Grazie per la rapida risposta. La tua ipotesi sulla nostra struttura dati era corretta. Avrei ragione nel dire che abbiamo bisogno di cambiare il DataFormat in Group per tutte le proprietà che contengono un riferimento ad A, e così via fino alla radice del grafico dell'oggetto? E questo cambiamento dovrebbe anche essere sugli attributi ProtoInclude rilevanti? –

+0

@James essenzialmente, sì. Hmmm ... Forse dovrei aggiungere un default a livello di modello per questo! –

+0

Ho aggiornato la mia domanda con il mio tentativo di utilizzare DataFormat.Group per risolvere il problema, ma ho ancora problemi a scaricare il buffer. Mi scuso se sono un idiota .. –

2

Ulteriori informazioni sulla modifica; il [ProtoInclude(..., DataFormat=...)] sembra che semplicemente non sia stato elaborato. Ho aggiunto un test per questo nella mia attuale accumulo locale, ed è ora passa:

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

Questa commettere è legato in qualche altro, refactoring non collegati (alcuni rilavorazione per WinRT/IKVM), ma dovrebbe essere impegnata al più presto.