W Noda Time v2 przechodzimy do rozdzielczości nanosekundowej. Oznacza to, że nie możemy już używać 8-bajtowej liczby całkowitej do reprezentowania całego zakresu czasu, który nas interesuje. To skłoniło mnie do zbadania wykorzystania pamięci przez (wiele) struktur czasu Noda, co z kolei doprowadziło mnie do aby odkryć niewielką dziwność w decyzji dotyczącej wyrównania CLR.
Po pierwsze, zdaję sobie sprawę, że jest to decyzja dotycząca wdrożenia i że domyślne zachowanie może się zmienić w dowolnym momencie. Zdaję sobie sprawę, że mogę to zmodyfikować za pomocą[StructLayout]
i [FieldOffset]
, ale wolałbym wymyślić rozwiązanie, które nie wymagałoby tego, jeśli to możliwe.
Mój podstawowy scenariusz jest taki, że mam pole, struct
które zawiera pole typu referencyjnego i dwa inne pola typu wartości, gdzie te pola są prostymi opakowaniami dlaint
. Miałem nadzieję , że będzie to reprezentowane jako 16 bajtów w 64-bitowym CLR (8 dla odniesienia i 4 dla każdego z pozostałych), ale z jakiegoś powodu używa 24 bajtów. Nawiasem mówiąc, mierzę przestrzeń za pomocą tablic - rozumiem, że układ może być inny w różnych sytuacjach, ale wydawało się to rozsądnym punktem wyjścia.
Oto przykładowy program demonstrujący problem:
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);
}
}
A kompilacja i wyjście na moim laptopie:
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
Więc:
- Jeśli nie masz pola typu referencyjnego, CLR z przyjemnością spakuje
Int32Wrapper
pola razem (TwoInt32Wrappers
ma rozmiar 8) - Nawet z polem typu referencyjnego CLR nadal jest zadowolony z pakowania
int
pola razem (RefAndTwoInt32s
ma rozmiar 16) - Łącząc oba, każde
Int32Wrapper
pole wydaje się być wypełnione / wyrównane do 8 bajtów. (RefAndTwoInt32Wrappers
ma rozmiar 24.) - Uruchomienie tego samego kodu w debugerze (ale nadal kompilacji wydania) pokazuje rozmiar 12.
Kilka innych eksperymentów przyniosło podobne wyniki:
- Umieszczenie pola typu odwołania po polach typu wartości nie pomaga
- Używanie
object
zamiaststring
nie pomaga (spodziewam się, że jest to „dowolny typ referencyjny”) - Używanie innej struktury jako „opakowania” wokół referencji nie pomaga
- Używanie ogólnej struktury jako opakowania wokół odwołania nie pomaga
- Jeśli będę nadal dodawać pola (w parach dla uproszczenia),
int
pola nadal liczą się na 4 bajty, aInt32Wrapper
pola liczą się do 8 bajtów - Dodanie
[StructLayout(LayoutKind.Sequential, Pack = 4)]
do każdej struktury w zasięgu wzroku nie zmienia wyników
Czy ktoś ma na to jakieś wyjaśnienie (najlepiej z dokumentacją referencyjną) lub sugestię, jak mogę uzyskać wskazówkę do CLR, że chciałbym, aby pola były pakowane bez określania stałego przesunięcia pól?
Ref<T>
alestring
zamiast tego używasz , nie żeby to miało coś zmienić.TwoInt32Wrappers
lub anInt64
i aTwoInt32Wrappers
? A co jeśli utworzysz ogólny,Pair<T1,T2> {public T1 f1; public T2 f2;}
a następnie utworzyszPair<string,Pair<int,int>>
iPair<string,Pair<Int32Wrapper,Int32Wrapper>>
? Jakie kombinacje zmuszają JITter do wypełniania elementów?Pair<string, TwoInt32Wrappers>
nie daje zaledwie 16 bajtów, tak by rozwiązać problem. Fascynujący.Marshal.SizeOf
zwróci rozmiar struktury, która zostanie przekazana do kodu natywnego, który nie musi mieć żadnego związku z rozmiarem struktury w kodzie .NET.Odpowiedzi:
Myślę, że to błąd. Widzisz efekt uboczny automatycznego układu, lubi wyrównywać nietrywialne pola do adresu, który jest wielokrotnością 8 bajtów w trybie 64-bitowym. Występuje nawet wtedy, gdy jawnie zastosujesz
[StructLayout(LayoutKind.Sequential)]
atrybut. To nie powinno się wydarzyć.Możesz to zobaczyć, upubliczniając członków struktury i dołączając kod testowy w następujący sposób:
Kiedy punkt przerwania trafi, użyj Debug + Windows + Memory + Memory 1. Przełącz na 4-bajtowe liczby całkowite i wpisz
&test
w polu Adres:0xe90ed750e0
jest wskaźnikiem ciągu na mojej maszynie (nie na twoim). Możesz łatwo zobaczyćInt32Wrappers
, z dodatkowymi 4 bajtami wypełnienia, które zmieniły rozmiar na 24 bajty. Wróć do struktury i umieść strunę jako ostatnią. Powtórz, a zobaczysz, że wskaźnik ciągu jest nadal pierwszy. NaruszaszLayoutKind.Sequential
, maszLayoutKind.Auto
.Trudno będzie przekonać Microsoft, aby to naprawił, działało w ten sposób zbyt długo, więc każda zmiana może coś zepsuć . CLR tylko próbuje uhonorować
[StructLayout]
zarządzaną wersję struktury i uczynić ją zapisywalną, ogólnie szybko się poddaje. Notorycznie dla każdej struktury, która zawiera DateTime. Otrzymujesz tylko prawdziwą gwarancję LayoutKind podczas organizowania struktury. Jak powiesz, wersja zorganizowana ma z pewnością 16 bajtówMarshal.SizeOf()
.Użycie
LayoutKind.Explicit
rozwiązuje problem, a nie to, co chciałeś usłyszeć.źródło
string
innym nowym typem odniesienia (class
), do którego zastosowano[StructLayout(LayoutKind.Sequential)]
, nie wydaje się niczego zmieniać. W przeciwnym kierunku, przy zastosowaniu[StructLayout(LayoutKind.Auto)]
dostruct Int32Wrapper
zmiany zużycia pamięci wTwoInt32Wrappers
.EDYCJA2
Ten kod będzie wyrównany do 8 bajtów, więc struktura będzie miała 16 bajtów. Dla porównania to:
Będzie wyrównany do 4 bajtów, więc ta struktura również będzie miała 16 bajtów. Zatem uzasadnienie jest takie, że wyrównanie struktury w CLR jest określane przez liczbę najbardziej wyrównanych pól, klasy oczywiście nie mogą tego zrobić, więc pozostaną wyrównane 8 bajtów.
Teraz, jeśli połączymy to wszystko i stworzymy strukturę:
Będzie miał 24 bajty {x, y} będzie miał 4 bajty każdy, a {z, s} będzie miał 8 bajtów. Po wprowadzeniu typu ref w strukturze CLR zawsze wyrówna naszą niestandardową strukturę, aby pasowała do wyrównania klasy.
Ten kod będzie miał 24 bajty, ponieważ Int32Wrapper zostanie wyrównany tak samo jak long. Tak więc opakowanie struktury niestandardowej będzie zawsze wyrównywane do najwyższego / najlepiej wyrównanego pola w strukturze lub do własnych wewnętrznych najbardziej znaczących pól. Tak więc w przypadku łańcucha ref, który jest wyrównany do 8 bajtów, opakowanie struktury zostanie wyrównane do tego.
Zamknięcie pola niestandardowej struktury wewnątrz struktury będzie zawsze wyrównane do najwyższego wyrównanego pola wystąpienia w strukturze. Teraz, jeśli nie jestem pewien, czy to błąd, ale bez pewnych dowodów, będę trzymać się mojej opinii, że może to być świadoma decyzja.
EDYTOWAĆ
Rozmiary są w rzeczywistości dokładne tylko wtedy, gdy są przydzielane na stercie, ale same struktury mają mniejsze rozmiary (dokładne rozmiary ich pól). Dalsza analiza sugeruje, że może to być błąd w kodzie CLR, ale musi być poparty dowodami.
Sprawdzę kod CLI i opublikuję dalsze aktualizacje, jeśli znajdzie się coś przydatnego.
Jest to strategia dopasowania używana przez alokator pamięci .NET.
Ten kod skompilowany z .net40 pod x64, w WinDbg wykonaj następujące czynności:
Najpierw znajdźmy typ na stercie:
Kiedy już to zrobimy, zobaczmy, co jest pod tym adresem:
Widzimy, że jest to ValueType i to jest ten, który stworzyliśmy. Ponieważ jest to tablica, musimy pobrać wartość ValueType pojedynczego elementu w tablicy:
Struktura ma w rzeczywistości 32 bajty, ponieważ 16 bajtów jest zarezerwowanych na wypełnienie, więc w rzeczywistości każda struktura ma rozmiar co najmniej 16 bajtów od momentu rozpoczęcia.
jeśli dodasz 16 bajtów z ints i ciąg ref do: 0000000003e72d18 + 8 bajtów EE / dopełnienie, skończysz na 0000000003e72d30 i jest to punkt początkowy dla odniesienia do łańcucha, a ponieważ wszystkie odwołania są wypełnione 8 bajtami od ich pierwszego rzeczywistego pola danych to daje nam 32 bajty dla tej struktury.
Zobaczmy, czy ciąg jest rzeczywiście wypełniony w ten sposób:
Teraz przeanalizujmy powyższy program w ten sam sposób:
Nasza struktura ma teraz 48 bajtów.
Tutaj sytuacja jest taka sama, jeśli dodamy do 0000000003c22d18 + 8 bajtów string ref, skończymy na początku pierwszego opakowania Int, gdzie wartość faktycznie wskazuje na adres, pod którym się znajdujemy.
Teraz widzimy, że każda wartość jest odwołaniem do obiektu, ponownie potwierdźmy to, sprawdzając 0000000003c22d20.
Właściwie to jest poprawne, ponieważ jest to struktura a adres nie mówi nam nic, jeśli jest to obj lub vt.
W rzeczywistości jest to bardziej podobny do typu Union, w którym tym razem zostanie wyrównane 8 bajtów (wszystkie dopełnienia zostaną wyrównane ze strukturą nadrzędną). Gdyby tak nie było, otrzymalibyśmy 20 bajtów, a to nie jest optymalne, więc alokator pamięci nigdy na to nie pozwoli. Jeśli ponownie wykonasz obliczenia, okaże się, że struktura ma rzeczywiście 40 bajtów.
Więc jeśli chcesz być bardziej konserwatywny w kwestii pamięci, nigdy nie powinieneś pakować jej w niestandardowy typ struktury, ale zamiast tego używać prostych tablic. Innym sposobem jest alokacja pamięci poza stertą (np. VirtualAllocEx) w ten sposób, że otrzymasz własny blok pamięci i będziesz nim zarządzać tak, jak chcesz.
Ostatnie pytanie brzmi: dlaczego nagle możemy otrzymać taki układ. Cóż, jeśli porównasz jited kod i wydajność inkrementacji int [] ze strukturą struct [] z inkrementacją pola licznika, druga wygeneruje 8-bajtowy adres wyrównany będący sumą, ale kiedy jited to przekłada się na bardziej zoptymalizowany kod asemblera (singe LEA vs wiele plików MOV). Jednak w opisanym tutaj przypadku wydajność będzie faktycznie gorsza, więc moim zdaniem jest to zgodne z podstawową implementacją CLR, ponieważ jest to typ niestandardowy, który może mieć wiele pól, więc może być łatwiej / lepiej umieścić adres początkowy zamiast value (ponieważ byłoby to niemożliwe) i wykonaj tam dopełnienie struktury, co spowoduje większy rozmiar bajtu.
źródło
RefAndTwoInt32Wrappers
nie wynosi 32 bajty - to 24, czyli to samo, co zgłoszono w moim kodzie. Jeśli spojrzysz na widok pamięci zamiast używaćdumparray
i spojrzysz na pamięć dla tablicy z (powiedzmy) 3 elementami z rozróżnialnymi wartościami, możesz wyraźnie zobaczyć, że każdy element składa się z 8-bajtowego odwołania do łańcucha i dwóch 8-bajtowych liczb całkowitych . Podejrzewam, żedumparray
pokazuje wartości jako odniesienia tylko dlatego, że nie wie, jak wyświetlićInt32Wrapper
wartości. Te „odniesienia” wskazują na siebie; nie są to oddzielne wartości.dumparray
pokazuje.Int32
. Szczerze mówiąc, nie przejmuję się zbytnio tym, co robi na stosie - ale nie sprawdziłem tego.Podsumowanie patrz odpowiedź @Hans Passant prawdopodobnie powyżej. Layout Sequential nie działa
Niektóre testy:
Jest to zdecydowanie tylko na 64-bitowej wersji, a odniesienie do obiektu „zatruwa” strukturę. 32-bitowy robi to, czego oczekujesz:
Gdy tylko odniesienie do obiektu zostanie dodane, wszystkie struktury rozszerzają się do rozmiaru 8 bajtów, a nie 4 bajtów. Rozszerzenie testów:
Jak widać, gdy tylko odniesienie zostanie dodane, każde Int32Wrapper stanie się 8 bajtami, więc nie jest to proste wyrównanie. Zmniejszyłem alokację tablicy, ponieważ była to alokacja LoH, która jest inaczej wyrównana.
źródło
Żeby dodać trochę danych do miksu - stworzyłem jeszcze jeden typ z tych, które masz:
Program pisze:
Wygląda więc na to, że
TwoInt32Wrappers
struktura jest odpowiednio wyrównana w nowejRefAndTwoInt32Wrappers2
strukturze.źródło