Perché l'allineamento della struttura dipende dal fatto che un tipo di campo sia primitivo o definito dall'utente?


121

In Noda Time v2, stiamo passando alla risoluzione in nanosecondi. Ciò significa che non possiamo più utilizzare un numero intero a 8 byte per rappresentare l'intero intervallo di tempo a cui siamo interessati. Questo mi ha spinto a indagare sull'utilizzo della memoria delle (molte) strutture di Noda Time, che a sua volta mi ha portato per scoprire una leggera stranezza nella decisione di allineamento del CLR.

In primo luogo, mi rendo conto che questa è una decisione di implementazione e che il comportamento predefinito potrebbe cambiare in qualsiasi momento. Mi rendo conto che posso modificarlo usando [StructLayout]e [FieldOffset], ma preferirei trovare una soluzione che non lo richiedesse, se possibile.

Il mio scenario principale è che ho un structche contiene un campo di tipo riferimento e altri due campi di tipo valore, in cui quei campi sono semplici wrapper per int. Avevo sperato che sarebbe stato rappresentato come 16 byte sul CLR a 64 bit (8 per il riferimento e 4 per ciascuno degli altri), ma per qualche motivo utilizza 24 byte. Sto misurando lo spazio usando gli array, a proposito: capisco che il layout può essere diverso in situazioni diverse, ma questo mi è sembrato un punto di partenza ragionevole.

Ecco un programma di esempio che dimostra il problema:

using System;
using System.Runtime.InteropServices;

#pragma warning disable 0169

struct Int32Wrapper
{
    int x;
}

struct TwoInt32s
{
    int x, y;
}

struct TwoInt32Wrappers
{
    Int32Wrapper x, y;
}

struct RefAndTwoInt32s
{
    string text;
    int x, y;
}

struct RefAndTwoInt32Wrappers
{
    string text;
    Int32Wrapper x, y;
}    

class Test
{
    static void Main()
    {
        Console.WriteLine("Environment: CLR {0} on {1} ({2})",
            Environment.Version,
            Environment.OSVersion,
            Environment.Is64BitProcess ? "64 bit" : "32 bit");
        ShowSize<Int32Wrapper>();
        ShowSize<TwoInt32s>();
        ShowSize<TwoInt32Wrappers>();
        ShowSize<RefAndTwoInt32s>();
        ShowSize<RefAndTwoInt32Wrappers>();
    }

    static void ShowSize<T>()
    {
        long before = GC.GetTotalMemory(true);
        T[] array = new T[100000];
        long after  = GC.GetTotalMemory(true);        
        Console.WriteLine("{0}: {1}", typeof(T),
                          (after - before) / array.Length);
    }
}

E la compilazione e l'output sul mio laptop:

c:\Users\Jon\Test>csc /debug- /o+ ShowMemory.cs
Microsoft (R) Visual C# Compiler version 12.0.30501.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.


c:\Users\Jon\Test>ShowMemory.exe
Environment: CLR 4.0.30319.34014 on Microsoft Windows NT 6.2.9200.0 (64 bit)
Int32Wrapper: 4
TwoInt32s: 8
TwoInt32Wrappers: 8
RefAndTwoInt32s: 16
RefAndTwoInt32Wrappers: 24

Così:

  • Se non si dispone di un campo del tipo di riferimento, il CLR è felice di Int32Wrapperraggruppare i campi insieme ( TwoInt32Wrappersha una dimensione di 8)
  • Anche con un campo del tipo di riferimento, il CLR è ancora felice di intraggruppare i campi insieme ( RefAndTwoInt32sha una dimensione di 16)
  • Combinando i due, ogni Int32Wrappercampo sembra essere riempito / allineato a 8 byte. ( RefAndTwoInt32Wrappersha una dimensione di 24.)
  • L'esecuzione dello stesso codice nel debugger (ma ancora una build di rilascio) mostra una dimensione di 12.

Alcuni altri esperimenti hanno prodotto risultati simili:

  • Mettere il campo del tipo di riferimento dopo i campi del tipo di valore non aiuta
  • Usare objectinvece di stringnon aiuta (immagino sia "qualsiasi tipo di riferimento")
  • Usare un'altra struttura come "wrapper" attorno al riferimento non aiuta
  • L'uso di una struttura generica come wrapper attorno al riferimento non aiuta
  • Se continuo ad aggiungere campi (a coppie per semplicità), i intcampi contano ancora per 4 byte e i Int32Wrappercampi contano per 8 byte
  • L'aggiunta [StructLayout(LayoutKind.Sequential, Pack = 4)]a ogni struttura in vista non cambia i risultati

Qualcuno ha qualche spiegazione per questo (idealmente con la documentazione di riferimento) o un suggerimento su come posso ottenere un suggerimento al CLR che vorrei che i campi fossero impacchettati senza specificare un offset di campo costante?


1
In realtà non sembra che tu stia usando Ref<T>ma stai usando stringinvece, non che dovrebbe fare la differenza.
tvanfosson

2
Cosa succede se metti due per creare una struttura con due TwoInt32Wrapperso un Int64e a TwoInt32Wrappers? Che ne dici se crei un generico Pair<T1,T2> {public T1 f1; public T2 f2;}e poi crei Pair<string,Pair<int,int>>e Pair<string,Pair<Int32Wrapper,Int32Wrapper>>? Quali combinazioni costringono il JITter a riempire le cose?
supercat

7
@supercat: E 'probabilmente meglio per copiare il codice e sperimentare di persona - ma Pair<string, TwoInt32Wrappers> non fare solo 16 byte, in modo che sarebbe affrontare la questione. Affascinante.
Jon Skeet

9
@SLaks: A volte, quando una struttura viene passata al codice nativo, il runtime copia tutti i dati in una struttura con un layout diverso. Marshal.SizeOfrestituirà la dimensione della struttura che verrebbe passata al codice nativo, che non necessita di alcuna relazione con la dimensione della struttura nel codice .NET.
supercat

5
L'osservazione interessante: Mono dà risultati corretti. Ambiente: CLR 4.0.30319.17020 su Unix 3.13.0.24 (64 bit) Int32Wrapper: 4 TwoInt32s: 8 TwoInt32Wrappers: 8 RefAndTwoInt32s: 16 RefAndTwoInt32Wrappers: 16
AndreyAkinshin

Risposte:


85

Penso che questo sia un bug. Stai vedendo l'effetto collaterale del layout automatico, gli piace allineare campi non banali a un indirizzo che è un multiplo di 8 byte in modalità a 64 bit. Si verifica anche quando si applica esplicitamente l' [StructLayout(LayoutKind.Sequential)]attributo. Non dovrebbe succedere.

Puoi vederlo rendendo pubblici i membri della struttura e aggiungendo il codice di test in questo modo:

    var test = new RefAndTwoInt32Wrappers();
    test.text = "adsf";
    test.x.x = 0x11111111;
    test.y.x = 0x22222222;
    Console.ReadLine();      // <=== Breakpoint here

Quando il punto di interruzione raggiunge, usa Debug + Windows + Memoria + Memoria 1. Passa a numeri interi a 4 byte e inserisci &testnel campo Indirizzo:

 0x000000E928B5DE98  0ed750e0 000000e9 11111111 00000000 22222222 00000000 

0xe90ed750e0è il puntatore di stringa sulla mia macchina (non la tua). Puoi facilmente vedere il Int32Wrappers, con i 4 byte extra di riempimento che hanno trasformato la dimensione in 24 byte. Torna alla struttura e metti la stringa per ultima. Ripeti e vedrai che il puntatore della stringa è ancora il primo. Violare LayoutKind.Sequential, hai LayoutKind.Auto.

Sarà difficile convincere Microsoft a risolvere questo problema, ha funzionato in questo modo per troppo tempo, quindi qualsiasi cambiamento spezzerà qualcosa . Il CLR fa solo un tentativo di onorare [StructLayout]la versione gestita di una struttura e renderla blitta, in generale si arrende rapidamente. Notoriamente per qualsiasi struttura che contiene un DateTime. Si ottiene solo la vera garanzia LayoutKind quando si effettua il marshalling di una struttura. La versione con marshalling è certamente di 16 byte, come Marshal.SizeOf()ti dirò.

L'utilizzo lo LayoutKind.Explicitrisolve, non quello che volevi sentire.


7
"Sarà difficile convincere Microsoft a risolvere questo problema, ha funzionato in questo modo per troppo tempo, quindi qualsiasi cambiamento spezzerà qualcosa". Il fatto che questo apparentemente non si manifesti in 32 bit o mono può aiutare (come da altri commenti).
NPSF3000

La documentazione di StructLayoutAttribute è piuttosto interessante. Fondamentalmente, solo i tipi copiabili sono controllati tramite StructLayout nella memoria gestita. Interessante, non l'ho mai saputo.
Michael Stum

@ Soner no non lo risolve. Hai messo il Layout su entrambi i campi da offset 8? Se è così allora xey sono la stessa cosa e cambiando uno cambia l'altro. Chiaramente non quello che sta cercando Jon.
BartoszAdamczewski

La sostituzione stringcon un altro nuovo tipo di riferimento ( class) a cui è stato applicato [StructLayout(LayoutKind.Sequential)]non sembra cambiare nulla. Nella direzione opposta, applicando [StructLayout(LayoutKind.Auto)]alle struct Int32Wrappermodifiche l'utilizzo della memoria in TwoInt32Wrappers.
Jeppe Stig Nielsen

1
"Sarà difficile convincere Microsoft a risolvere questo problema, ha funzionato in questo modo per troppo tempo, quindi qualsiasi cambiamento spezzerà qualcosa". xkcd.com/1172
iCodeSometime

19

EDIT2

struct RefAndTwoInt32Wrappers
{
    public int x;
    public string s;
}

Questo codice sarà allineato a 8 byte, quindi la struttura avrà 16 byte. In confronto questo:

struct RefAndTwoInt32Wrappers
{
    public int x,y;
    public string s;
}

Sarà allineato a 4 byte, quindi anche questa struttura avrà 16 byte. Quindi la logica qui è che l'allineamento della struttura in CLR è determinato dal numero di campi più allineati, i clasi ovviamente non possono farlo quindi rimarranno allineati a 8 byte.

Ora se combiniamo tutto questo e creiamo struct:

struct RefAndTwoInt32Wrappers
{
    public int x,y;
    public Int32Wrapper z;
    public string s;
}

Avrà 24 byte {x, y} avrà 4 byte ciascuno e {z, s} avrà 8 byte. Una volta introdotto un tipo ref nella struttura, CLR allineerà sempre la nostra struttura personalizzata in modo che corrisponda all'allineamento della classe.

struct RefAndTwoInt32Wrappers
{
    public Int32Wrapper z;
    public long l;
    public int x,y;  
}

Questo codice avrà 24 byte poiché Int32Wrapper sarà allineato lo stesso long. Quindi lo struct wrapper personalizzato si allineerà sempre al campo più alto / meglio allineato nella struttura o ai propri campi interni più significativi. Quindi, nel caso di una stringa di riferimento allineata a 8 byte, lo struct wrapper si allineerà a quella.

La conclusione del campo della struttura personalizzata all'interno della struttura sarà sempre allineata al campo dell'istanza allineato più in alto nella struttura. Ora, se non sono sicuro che si tratti di un bug ma senza alcune prove, mi atterrò alla mia opinione che questa potrebbe essere una decisione consapevole.


MODIFICARE

Le dimensioni sono effettivamente accurate solo quando allocate su un heap, ma le strutture stesse hanno dimensioni più piccole (le dimensioni esatte dei suoi campi). Ulteriori analisi sembrano suggerire che questo potrebbe essere un bug nel codice CLR, ma deve essere supportato da prove.

Controllerò il codice cli e posterò ulteriori aggiornamenti se verrà trovato qualcosa di utile.


Questa è una strategia di allineamento utilizzata dall'allocatore di mem .NET.

public static RefAndTwoInt32s[] test = new RefAndTwoInt32s[1];

static void Main()
{
    test[0].text = "a";
    test[0].x = 1;
    test[0].x = 1;

    Console.ReadKey();
}

Questo codice compilato con .net40 in x64, in WinDbg consente di eseguire le operazioni seguenti:

Troviamo prima il tipo sull'Heap:

    0:004> !dumpheap -type Ref
       Address               MT     Size
0000000003e72c78 000007fe61e8fb58       56    
0000000003e72d08 000007fe039d3b78       40    

Statistics:
              MT    Count    TotalSize Class Name
000007fe039d3b78        1           40 RefAndTwoInt32s[]
000007fe61e8fb58        1           56 System.Reflection.RuntimeAssembly
Total 2 objects

Una volta ottenuto, vediamo cosa c'è sotto quell'indirizzo:

    0:004> !do 0000000003e72d08
Name:        RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass:     000007fe039d3ad0
Size:        40(0x28) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Fields:
None

Vediamo che questo è un ValueType ed è quello che abbiamo creato. Poiché questo è un array, dobbiamo ottenere il valore ValueType def di un singolo elemento dell'array:

    0:004> !dumparray -details 0000000003e72d08
Name:        RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass:     000007fe039d3ad0
Size:        40(0x28) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3a58
[0] 0000000003e72d18
    Name:        RefAndTwoInt32s
    MethodTable: 000007fe039d3a58
    EEClass:     000007fe03ae2338
    Size:        32(0x20) bytes
    File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        000007fe61e8c358  4000006        0            System.String      0     instance     0000000003e72d30     text
        000007fe61e8f108  4000007        8             System.Int32      1     instance                    1     x
        000007fe61e8f108  4000008        c             System.Int32      1     instance                    0     y

La struttura è in realtà di 32 byte poiché è di 16 byte riservati per il riempimento, quindi in realtà ogni struttura ha una dimensione di almeno 16 byte dall'inizio.

se aggiungi 16 byte da int e una stringa ref a: 0000000003e72d18 + 8 byte EE / padding finirai in 0000000003e72d30 e questo è il punto di partenza per il riferimento di stringa, e poiché tutti i riferimenti sono riempiti di 8 byte dal loro primo campo dati effettivo questo compensa i nostri 32 byte per questa struttura.

Vediamo se la stringa è effettivamente imbottita in questo modo:

0:004> !do 0000000003e72d30    
Name:        System.String
MethodTable: 000007fe61e8c358
EEClass:     000007fe617f3720
Size:        28(0x1c) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      a
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fe61e8f108  40000aa        8         System.Int32  1 instance                1 m_stringLength
000007fe61e8d640  40000ab        c          System.Char  1 instance               61 m_firstChar
000007fe61e8c358  40000ac       18        System.String  0   shared           static Empty
                                 >> Domain:Value  0000000001577e90:NotInit  <<

Ora analizziamo il programma precedente allo stesso modo:

public static RefAndTwoInt32Wrappers[] test = new RefAndTwoInt32Wrappers[1];

static void Main()
{
    test[0].text = "a";
    test[0].x.x = 1;
    test[0].y.x = 1;

    Console.ReadKey();
}

0:004> !dumpheap -type Ref
     Address               MT     Size
0000000003c22c78 000007fe61e8fb58       56    
0000000003c22d08 000007fe039d3c00       48    

Statistics:
              MT    Count    TotalSize Class Name
000007fe039d3c00        1           48 RefAndTwoInt32Wrappers[]
000007fe61e8fb58        1           56 System.Reflection.RuntimeAssembly
Total 2 objects

La nostra struttura ora è di 48 byte.

0:004> !dumparray -details 0000000003c22d08
Name:        RefAndTwoInt32Wrappers[]
MethodTable: 000007fe039d3c00
EEClass:     000007fe039d3b58
Size:        48(0x30) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3ae0
[0] 0000000003c22d18
    Name:        RefAndTwoInt32Wrappers
    MethodTable: 000007fe039d3ae0
    EEClass:     000007fe03ae2338
    Size:        40(0x28) bytes
    File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        000007fe61e8c358  4000009        0            System.String      0     instance     0000000003c22d38     text
        000007fe039d3a20  400000a        8             Int32Wrapper      1     instance     0000000003c22d20     x
        000007fe039d3a20  400000b       10             Int32Wrapper      1     instance     0000000003c22d28     y

Qui la situazione è la stessa, se aggiungiamo a 0000000003c22d18 + 8 byte di stringa ref finiremo all'inizio del primo wrapper Int dove il valore punta effettivamente all'indirizzo a cui ci troviamo.

Ora possiamo vedere che ogni valore è di nuovo un riferimento a un oggetto, confermiamolo sbirciando 0000000003c22d20.

0:004> !do 0000000003c22d20
<Note: this object has an invalid CLASS field>
Invalid object

In realtà è corretto poiché è uno struct l'indirizzo non ci dice nulla se questo è un obj o vt.

0:004> !dumpvc 000007fe039d3a20   0000000003c22d20    
Name:        Int32Wrapper
MethodTable: 000007fe039d3a20
EEClass:     000007fe03ae23c8
Size:        24(0x18) bytes
File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fe61e8f108  4000001        0         System.Int32  1 instance                1 x

Quindi in realtà questo è più simile a un tipo Union che questa volta verrà allineato a 8 byte (tutti i padding saranno allineati con la struttura padre). Se non lo fosse, finiremmo con 20 byte e questo non è ottimale, quindi l'allocatore di mem non permetterà mai che accada. Se fai di nuovo i calcoli, risulterà che la struttura ha davvero 40 byte di dimensione.

Quindi, se vuoi essere più conservatore con la memoria, non dovresti mai impacchettarla in un tipo di struttura personalizzata, ma invece usare semplici array. Un altro modo è allocare la memoria fuori dall'heap (VirtualAllocEx per esempio) in questo modo ti viene dato il blocco di memoria e lo gestisci nel modo desiderato.

L'ultima domanda qui è perché all'improvviso potremmo ottenere un layout del genere. Bene, se confronti il ​​codice jited e le prestazioni di un incremento int [] con struct [] con un incremento del campo contatore, il secondo genererà un indirizzo allineato a 8 byte essendo un'unione, ma quando jited questo si traduce in un codice assembly più ottimizzato (singe LEA vs MOV multipli). Tuttavia, nel caso qui descritto le prestazioni saranno effettivamente peggiori, quindi la mia opinione è che questo è coerente con l'implementazione CLR sottostante poiché è un tipo personalizzato che può avere più campi, quindi potrebbe essere più facile / migliore mettere l'indirizzo di partenza invece di un value (poiché sarebbe impossibile) e fai il padding della struttura lì, risultando in una dimensione di byte più grande.


1
Guardando questo da solo, la dimensione di RefAndTwoInt32Wrappers non è di 32 byte - è di 24, che è la stessa riportata con il mio codice. Se guardi nella vista della memoria invece di usare dumparraye guardi la memoria per un array con (diciamo) 3 elementi con valori distinguibili, puoi vedere chiaramente che ogni elemento è costituito da un riferimento a una stringa di 8 byte e due interi da 8 byte . Sospetto che dumparraymostri i valori come riferimenti semplicemente perché non sa come visualizzare i Int32Wrappervalori. Quei "riferimenti" puntano a se stessi; non sono valori separati.
Jon Skeet

1
Non sono abbastanza sicuro da dove si ottiene il "riempimento a 16 byte", ma sospetto che possa essere perché stai osservando la dimensione dell'oggetto array, che sarà "16 byte + conteggio * dimensione dell'elemento". Quindi un array con conteggio 2 ha una dimensione di 72 (16 + 2 * 24), che è ciò che dumparraymostra.
Jon Skeet

@jon hai scaricato la tua struttura e controllato quanto spazio occupa nell'heap? Normalmente la dimensione dell'array viene mantenuta all'inizio dell'array, anche questo può essere verificato.
BartoszAdamczewski

@jon la dimensione riportata contiene anche l'offset della stringa che inizia da 8. Non credo che quegli 8 byte extra menzionati provengano da array poiché la maggior parte delle cose dell'array risiede prima dell'indirizzo del primo elemento, ma controllerò due volte e commento su quello.
BartoszAdamczewski

1
No, ThreeInt32Wrappers finisce come 12 byte, FourInt32Wrappers come 16, FiveInt32Wrappers come 20. Non vedo nulla di logico sull'aggiunta di un campo del tipo di riferimento che cambia il layout in modo così drastico. E nota che è abbastanza felice di ignorare l'allineamento a 8 byte quando i campi sono di tipo Int32. Non sono molto preoccupato per quello che fa in pila, ad essere onesti, ma non l'ho controllato.
Jon Skeet

9

Riepilogo vedi la risposta di @Hans Passant probabilmente sopra. Layout sequenziale non funziona


Alcuni test:

È sicuramente solo a 64 bit e il riferimento all'oggetto "avvelena" la struttura. 32 bit fa quello che ti aspetti:

Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (32 bit)
ConsoleApplication1.Int32Wrapper: 4
ConsoleApplication1.TwoInt32s: 8
ConsoleApplication1.TwoInt32Wrappers: 8
ConsoleApplication1.ThreeInt32Wrappers: 12
ConsoleApplication1.Ref: 4
ConsoleApplication1.RefAndTwoInt32s: 12
ConsoleApplication1.RefAndTwoInt32Wrappers: 12
ConsoleApplication1.RefAndThreeInt32s: 16
ConsoleApplication1.RefAndThreeInt32Wrappers: 16

Non appena viene aggiunto il riferimento all'oggetto, tutte le strutture si espandono per essere 8 byte invece della loro dimensione di 4 byte. Espansione dei test:

Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (64 bit)
ConsoleApplication1.Int32Wrapper: 4
ConsoleApplication1.TwoInt32s: 8
ConsoleApplication1.TwoInt32Wrappers: 8
ConsoleApplication1.ThreeInt32Wrappers: 12
ConsoleApplication1.Ref: 8
ConsoleApplication1.RefAndTwoInt32s: 16
ConsoleApplication1.RefAndTwoInt32sSequential: 16
ConsoleApplication1.RefAndTwoInt32Wrappers: 24
ConsoleApplication1.RefAndThreeInt32s: 24
ConsoleApplication1.RefAndThreeInt32Wrappers: 32
ConsoleApplication1.RefAndFourInt32s: 24
ConsoleApplication1.RefAndFourInt32Wrappers: 40

Come puoi vedere non appena il riferimento viene aggiunto ogni Int32Wrapper diventa 8 byte, quindi non è un semplice allineamento. Ho ridotto l'allocazione dell'array nel caso fosse un'allocazione LoH che è allineata in modo diverso.


4

Solo per aggiungere alcuni dati al mix, ho creato un altro tipo da quelli che avevi:

struct RefAndTwoInt32Wrappers2
{
    string text;
    TwoInt32Wrappers z;
}

Il programma scrive:

RefAndTwoInt32Wrappers2: 16

Quindi sembra che la TwoInt32Wrappersstruttura si allinei correttamente nella nuova RefAndTwoInt32Wrappers2struttura.


Stai utilizzando 64 bit? L'allineamento va bene a 32 bit
Ben Adams

I miei risultati sono gli stessi di tutti gli altri per i vari ambienti.
Jesse C. Slicer
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.