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?
Ref<T>
tetapistring
malah menggunakannya , bukan berarti itu membuat perbedaan.TwoInt32Wrappers
, atauInt64
dan danTwoInt32Wrappers
? Bagaimana jika Anda membuat generikPair<T1,T2> {public T1 f1; public T2 f2;}
dan kemudian membuatPair<string,Pair<int,int>>
danPair<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.Jawaban:
Saya pikir ini adalah bug. Anda melihat efek samping dari tata letak otomatis, ia suka menyelaraskan bidang non-sepele ke alamat yang kelipatan 8 byte dalam mode 64-bit. Itu terjadi bahkan ketika Anda menerapkan
[StructLayout(LayoutKind.Sequential)]
atribut secara eksplisit . Itu tidak seharusnya terjadi.Anda dapat melihatnya dengan membuat anggota struct publik dan menambahkan kode pengujian seperti ini:
Ketika breakpoint mencapai, gunakan Debug + Windows + Memory + Memory 1. Beralih ke integer 4-byte dan masukkan
&test
ke kolom Address:0xe90ed750e0
adalah penunjuk string di mesin saya (bukan milik Anda). Anda dapat dengan mudah melihatInt32Wrappers
, dengan tambahan 4 byte padding yang mengubah ukuran menjadi 24 byte. Kembali ke struct dan letakkan string terakhir. Ulangi dan Anda akan melihat penunjuk string masih pertama. MelanggarLayoutKind.Sequential
, Anda punyaLayoutKind.Auto
.Akan sulit untuk meyakinkan Microsoft untuk memperbaikinya, ini telah bekerja terlalu lama sehingga setiap perubahan akan merusak sesuatu . CLR hanya melakukan upaya untuk menghormati
[StructLayout]
versi terkelola dari sebuah struct dan membuatnya menjadi blittable, secara umum dengan cepat menyerah. Terkenal untuk setiap struct yang berisi DateTime. Anda hanya mendapatkan jaminan LayoutKind yang sebenarnya saat menyusun struct. Versi marshaled pasti adalah 16 byte, seperti yangMarshal.SizeOf()
akan memberi tahu Anda.Menggunakan
LayoutKind.Explicit
perbaikan itu, bukan apa yang ingin Anda dengar.sumber
string
dengan jenis referensi baru lainnya (class
) yang telah diterapkan[StructLayout(LayoutKind.Sequential)]
tampaknya tidak mengubah apa pun. Di arah berlawanan, menerapkan[StructLayout(LayoutKind.Auto)]
denganstruct Int32Wrapper
mengubah penggunaan memori diTwoInt32Wrappers
.EDIT2
Kode ini akan selaras 8 byte sehingga struct akan memiliki 16 byte. Sebagai perbandingan ini:
Akan menjadi 4 byte sejajar sehingga struct ini juga akan memiliki 16 byte. Jadi alasannya di sini adalah bahwa struktur struktur di CLR ditentukan oleh jumlah bidang yang paling selaras, klas jelas tidak dapat melakukannya sehingga mereka akan tetap selaras 8 byte.
Sekarang jika kita menggabungkan semua itu dan membuat struct:
Ini akan memiliki 24 byte {x, y} akan memiliki 4 byte masing-masing dan {z, s} akan memiliki 8 byte. Setelah kami memperkenalkan tipe ref di struct CLR akan selalu menyelaraskan struct kustom kami agar sesuai dengan perataan kelas.
Kode ini akan memiliki 24 byte karena Int32Wrapper akan disejajarkan dengan panjang yang sama. Jadi, pembungkus struct kustom akan selalu sejajar dengan bidang paling tinggi / paling rata dalam struktur atau ke bidang paling signifikan internalnya sendiri. Jadi dalam kasus string ref yang sejajar 8 byte, pembungkus struct akan sejajar dengan itu.
Penutup bidang struct kustom di dalam struct akan selalu disejajarkan dengan bidang contoh rata tertinggi dalam struktur. Sekarang jika saya tidak yakin apakah ini bug tetapi tanpa beberapa bukti, saya akan tetap berpegang pada pendapat saya bahwa ini mungkin keputusan sadar.
EDIT
Ukuran sebenarnya akurat hanya jika dialokasikan di heap tetapi struct itu sendiri memiliki ukuran yang lebih kecil (ukuran persis bidangnya). Analisis lebih lanjut menunjukkan bahwa ini mungkin bug dalam kode CLR, tetapi perlu didukung oleh bukti.
Saya akan memeriksa kode cli dan memposting pembaruan lebih lanjut jika sesuatu yang berguna akan ditemukan.
Ini adalah strategi penyelarasan yang digunakan oleh pengalokasi mem .NET.
Kode ini dikompilasi dengan .net40 di bawah x64, Di WinDbg mari lakukan hal berikut:
Mari kita temukan jenisnya di Heap terlebih dahulu:
Setelah kita memilikinya, mari kita lihat apa yang ada di bawah alamat itu:
Kami melihat bahwa ini adalah ValueType dan yang kami buat. Karena ini adalah array, kita perlu mendapatkan def ValueType dari satu elemen dalam array:
Struktur sebenarnya 32 byte karena 16 byte dicadangkan untuk padding sehingga pada kenyataannya setiap struktur berukuran setidaknya 16 byte sejak awal.
jika Anda menambahkan 16 byte dari int dan string ref ke: 0000000003e72d18 + 8 byte EE / padding Anda akan berakhir di 0000000003e72d30 dan ini adalah titik pandang untuk referensi string, dan karena semua referensi adalah 8 byte empuk dari bidang data aktual pertama mereka ini membuat 32 byte kami untuk struktur ini.
Mari kita lihat apakah stringnya dilapisi seperti itu:
Sekarang mari kita menganalisis program di atas dengan cara yang sama:
Sekarang struct kami adalah 48 byte.
Di sini situasinya sama, jika kita menambahkan ke 0000000003c22d18 + 8 byte string ref, kita akan berakhir di awal pembungkus Int pertama di mana nilainya sebenarnya menunjuk ke alamat tempat kita berada.
Sekarang kita dapat melihat bahwa setiap nilai adalah referensi objek lagi memungkinkan konfirmasi dengan mengintip 0000000003c22d20.
Sebenarnya itu benar karena ini adalah sebuah struct, alamat tidak memberi tahu kita apa pun jika ini adalah obj atau vt.
Jadi sebenarnya ini lebih seperti tipe Union yang akan mendapatkan 8 byte sejajar kali ini (semua paddings akan disejajarkan dengan struct induk). Jika tidak maka kita akan berakhir dengan 20 byte dan itu tidak optimal sehingga pengalokasi mem tidak akan pernah mengizinkannya terjadi. Jika Anda menghitungnya lagi, ternyata struct tersebut berukuran 40 byte.
Jadi jika Anda ingin lebih konservatif dengan memori, Anda tidak boleh mengemasnya dalam tipe struct kustom struct melainkan menggunakan array sederhana. Cara lain adalah dengan mengalokasikan memori dari heap (misalnya VirtualAllocEx) dengan cara ini Anda diberikan blok memori sendiri dan Anda mengelolanya sesuai keinginan.
Pertanyaan terakhir di sini adalah mengapa tiba-tiba kita bisa mendapatkan layout seperti itu. Nah, jika Anda membandingkan kode jited dan kinerja inkrementasi int [] dengan struct [] dengan inkrementasi bidang penghitung, yang kedua akan menghasilkan alamat selaras 8 byte menjadi satu kesatuan, tetapi ketika jited ini diterjemahkan menjadi kode assembly yang lebih dioptimalkan (singe LEA vs beberapa MOV). Namun dalam kasus yang dijelaskan di sini, kinerja sebenarnya akan lebih buruk jadi pendapat saya adalah bahwa ini konsisten dengan implementasi CLR yang mendasarinya karena ini adalah tipe khusus yang dapat memiliki banyak bidang sehingga mungkin lebih mudah / lebih baik untuk meletakkan alamat awal daripada a nilai (karena itu tidak mungkin) dan melakukan padding struct di sana, sehingga menghasilkan ukuran byte yang lebih besar.
sumber
RefAndTwoInt32Wrappers
bukan 32 byte - ini 24, yang sama dengan yang dilaporkan dengan kode saya. Jika Anda melihat di tampilan memori daripada menggunakandumparray
, dan melihat memori untuk larik dengan (katakanlah) 3 elemen dengan nilai yang dapat dibedakan, Anda dapat dengan jelas melihat bahwa setiap elemen terdiri dari referensi string 8-byte dan dua bilangan bulat 8-byte . Saya menduga itudumparray
menunjukkan nilai sebagai referensi hanya karena tidak tahu cara menampilkanInt32Wrapper
nilai. "Referensi" itu menunjuk pada diri mereka sendiri; mereka bukanlah nilai yang terpisah.dumparray
ditampilkan.Int32
. Saya tidak terlalu peduli tentang apa yang dilakukannya di tumpukan, jujur - tetapi saya belum memeriksanya.Ringkasan lihat jawaban @Hans Passant mungkin di atas. Layout Sequential tidak berfungsi
Beberapa pengujian:
Ini pasti hanya pada 64bit dan referensi objek "meracuni" struct tersebut. 32 bit melakukan apa yang Anda harapkan:
Segera setelah referensi objek ditambahkan, semua struct diperluas menjadi 8 byte, bukan ukuran 4 byte. Memperluas tes:
Seperti yang Anda lihat segera setelah referensi ditambahkan, setiap Int32Wrapper menjadi 8 byte, jadi bukan perataan sederhana. Saya mengecilkan alokasi array jika itu alokasi LoH yang selaras secara berbeda.
sumber
Hanya untuk menambahkan beberapa data ke dalam campuran - Saya membuat satu jenis lagi dari yang Anda miliki:
Program itu menulis:
Jadi sepertinya
TwoInt32Wrappers
struct sejajar dengan benar diRefAndTwoInt32Wrappers2
struct baru .sumber