Di Noda Time v2, kami beralih ke resolusi nanodetik. Itu berarti kita tidak dapat lagi menggunakan integer 8-byte untuk mewakili seluruh rentang waktu yang kita minati. Itu mendorong saya untuk menyelidiki penggunaan memori dari (banyak) struct Waktu Noda, yang pada gilirannya membawa saya untuk mengungkap sedikit keanehan dalam keputusan penyelarasan CLR.
Pertama, saya menyadari bahwa ini adalah keputusan implementasi, dan perilaku default dapat berubah kapan saja. Saya menyadari bahwa saya dapat memodifikasinya menggunakan [StructLayout]
dan [FieldOffset]
, tetapi saya lebih suka menemukan solusi yang tidak memerlukannya jika memungkinkan.
Skenario inti saya adalah bahwa saya memiliki bidang struct
yang berisi jenis referensi dan dua bidang jenis nilai lainnya, di mana bidang tersebut adalah pembungkus sederhana int
. Saya berharap itu akan direpresentasikan sebagai 16 byte pada CLR 64-bit (8 untuk referensi dan 4 untuk masing-masing yang lain), tetapi untuk beberapa alasan itu menggunakan 24 byte. Ngomong-ngomong, saya mengukur ruang menggunakan array - saya mengerti bahwa tata letaknya mungkin berbeda dalam situasi yang berbeda, tetapi ini terasa seperti titik awal yang masuk akal.
Berikut adalah contoh program yang mendemonstrasikan masalah tersebut:
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);
}
}
Dan kompilasi dan output di laptop saya:
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
Begitu:
- Jika Anda tidak memiliki bidang tipe referensi, CLR dengan senang hati akan mengemas
Int32Wrapper
bidang bersama-sama (TwoInt32Wrappers
memiliki ukuran 8) - Bahkan dengan bidang tipe referensi, CLR masih senang untuk mengemas
int
bidang bersama-sama (RefAndTwoInt32s
memiliki ukuran 16) - Menggabungkan keduanya, setiap
Int32Wrapper
bidang tampak empuk / sejajar dengan 8 byte. (RefAndTwoInt32Wrappers
memiliki ukuran 24.) - Menjalankan kode yang sama di debugger (tetapi masih berupa rilis build) menunjukkan ukuran 12.
Beberapa eksperimen lain memberikan hasil yang serupa:
- Menempatkan bidang tipe referensi setelah bidang tipe nilai tidak membantu
- Menggunakan
object
bukannyastring
tidak membantu (saya rasa ini adalah "jenis referensi apa pun") - Menggunakan struct lain sebagai "pembungkus" di sekitar referensi tidak membantu
- Menggunakan struct generik sebagai pembungkus di sekitar referensi tidak membantu
- Jika saya terus menambahkan bidang (berpasangan untuk kesederhanaan),
int
bidang masih dihitung untuk 4 byte, danInt32Wrapper
bidang dihitung untuk 8 byte - Menambahkan
[StructLayout(LayoutKind.Sequential, Pack = 4)]
ke setiap struct yang terlihat tidak mengubah hasil
Apakah ada yang punya penjelasan untuk ini (idealnya dengan dokumentasi referensi) atau saran tentang bagaimana saya bisa mendapatkan petunjuk ke CLR bahwa saya ingin field dikemas tanpa menentukan offset field yang konstan?
TwoInt32Wrappers
, atau Int64
dan dan TwoInt32Wrappers
? Bagaimana jika Anda membuat generik Pair<T1,T2> {public T1 f1; public T2 f2;}
dan kemudian membuat Pair<string,Pair<int,int>>
dan Pair<string,Pair<Int32Wrapper,Int32Wrapper>>
? Kombinasi mana yang memaksa JITter untuk pad sesuatu?
Pair<string, TwoInt32Wrappers>
tidak memberikan hanya 16 byte, sehingga akan mengatasi masalah ini. Menarik.
Marshal.SizeOf
akan mengembalikan ukuran struktur yang akan diteruskan ke kode asli, yang tidak perlu berhubungan dengan ukuran struktur dalam kode .NET.
Ref<T>
tetapistring
malah menggunakannya , bukan berarti itu membuat perbedaan.