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?
TwoInt32Wrappers
lub an Int64
i a TwoInt32Wrappers
? A co jeśli utworzysz ogólny, Pair<T1,T2> {public T1 f1; public T2 f2;}
a następnie utworzysz Pair<string,Pair<int,int>>
i Pair<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.
Ref<T>
alestring
zamiast tego używasz , nie żeby to miało coś zmienić.