เหตุใดการจัดแนวโครงสร้างจึงขึ้นอยู่กับว่าประเภทฟิลด์เป็นแบบดั้งเดิมหรือผู้ใช้กำหนด


121

ในNoda Time v2 เรากำลังเคลื่อนไปสู่ความละเอียดระดับนาโนวินาที นั่นหมายความว่าเราไม่สามารถใช้จำนวนเต็ม 8 ไบต์เพื่อแสดงช่วงเวลาทั้งหมดที่เราสนใจได้อีกต่อไปนั่นทำให้ฉันต้องตรวจสอบการใช้หน่วยความจำของโครงสร้าง (จำนวนมาก) ของ Noda Time ซึ่งทำให้ฉัน เพื่อเปิดเผยความแปลกประหลาดเล็กน้อยในการตัดสินใจจัดตำแหน่งของ CLR

ประการแรกผมรู้ว่านี่คือการตัดสินใจการดำเนินงานและการทำงานเริ่มต้นอาจมีการเปลี่ยนแปลงได้ตลอดเวลา ฉันรู้ว่าฉันสามารถแก้ไขได้โดยใช้[StructLayout]และ[FieldOffset]แต่ฉันอยากจะหาวิธีแก้ปัญหาที่ไม่ต้องการสิ่งนั้นถ้าเป็นไปได้

สถานการณ์หลักของฉันที่ฉันมีstructซึ่งมีข้อมูลอ้างอิงและแบบสองช่องค่าชนิดอื่น ๆ intที่มีเขตข้อมูลเหล่านั้นห่อง่ายสำหรับ ฉันหวังว่าจะแสดงเป็น 16 ไบต์บน 64-bit CLR (8 สำหรับการอ้างอิงและ 4 สำหรับแต่ละอัน) แต่ด้วยเหตุผลบางประการจึงใช้ 24 ไบต์ ฉันกำลังวัดพื้นที่โดยใช้อาร์เรย์ - ฉันเข้าใจว่าการจัดวางอาจแตกต่างกันในสถานการณ์ที่แตกต่างกัน แต่นี่เป็นจุดเริ่มต้นที่สมเหตุสมผล

นี่คือโปรแกรมตัวอย่างที่แสดงให้เห็นถึงปัญหา:

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);
    }
}

และการรวบรวมและผลลัพธ์บนแล็ปท็อปของฉัน:

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

ดังนั้น:

  • หากคุณไม่มีฟิลด์ประเภทการอ้างอิง CLR ยินดีที่จะInt32Wrapperรวมฟิลด์เข้าด้วยกัน ( TwoInt32Wrappersมีขนาด 8)
  • แม้ว่าจะมีฟิลด์ประเภทการอ้างอิง CLR ก็ยังยินดีที่จะintรวมฟิลด์เข้าด้วยกัน ( RefAndTwoInt32sมีขนาด 16)
  • เมื่อรวมทั้งสองInt32Wrapperฟิลด์แต่ละฟิลด์จะมีเบาะ / จัดแนวเป็น 8 ไบต์ ( RefAndTwoInt32Wrappersมีขนาด 24. )
  • การรันโค้ดเดียวกันในดีบักเกอร์ (แต่ยังคงเป็นรุ่นที่วางจำหน่าย) จะแสดงขนาดเป็น 12

การทดลองอื่น ๆ บางส่วนให้ผลลัพธ์ที่คล้ายกัน:

  • การใส่ฟิลด์ประเภทการอ้างอิงหลังฟิลด์ประเภทค่าไม่ได้ช่วยอะไร
  • การใช้objectแทนstringไม่ช่วยอะไร (ฉันคาดว่าเป็น "ประเภทอ้างอิงใด ๆ ")
  • การใช้โครงสร้างอื่นเป็น "wrapper" รอบการอ้างอิงไม่ได้ช่วยอะไร
  • การใช้โครงสร้างทั่วไปเป็นกระดาษห่อหุ้มรอบการอ้างอิงไม่ได้ช่วยอะไร
  • ถ้าฉันเพิ่มฟิลด์ (เป็นคู่ ๆ เพื่อความเรียบง่าย) intฟิลด์จะยังคงนับเป็น 4 ไบต์และInt32Wrapperฟิลด์จะนับเป็น 8 ไบต์
  • การเพิ่ม[StructLayout(LayoutKind.Sequential, Pack = 4)]ทุกโครงสร้างที่มองเห็นไม่ได้ทำให้ผลลัพธ์เปลี่ยนไป

ใครมีคำอธิบายเกี่ยวกับเรื่องนี้ (ควรมีเอกสารอ้างอิง) หรือคำแนะนำว่าฉันจะบอกใบ้ CLR ได้อย่างไรว่าฉันต้องการให้ฟิลด์ถูกบรรจุโดยไม่ระบุค่าชดเชยฟิลด์คงที่หรือไม่?


1
ดูเหมือนคุณจะไม่ได้ใช้จริงRef<T>แต่กำลังใช้stringแทนไม่ใช่ว่าควรสร้างความแตกต่าง
tvanfosson

2
จะเกิดอะไรขึ้นถ้าคุณใส่สองสร้าง struct กับสองTwoInt32WrappersหรือInt64และTwoInt32Wrappers? แล้วถ้าคุณสร้างแบบทั่วไปPair<T1,T2> {public T1 f1; public T2 f2;}แล้วสร้างPair<string,Pair<int,int>>และPair<string,Pair<Int32Wrapper,Int32Wrapper>>ล่ะ? ชุดค่าผสมใดที่บังคับให้ JITter วางแผ่นสิ่งต่างๆ
supercat

7
@supercat: มันน่าจะดีที่สุดสำหรับคุณที่จะคัดลอกโค้ดและการทดสอบด้วยตัวคุณเอง - แต่Pair<string, TwoInt32Wrappers> ไม่ให้เพียง 16 ไบต์เพื่อที่จะแก้ไขปัญหา มโนหร
จอน Skeet

9
@SLaks: บางครั้งเมื่อโครงสร้างถูกส่งผ่านไปยังโค้ดเนทีฟ Runtime จะคัดลอกข้อมูลทั้งหมดไปยังโครงสร้างที่มีเลย์เอาต์ที่แตกต่างกัน Marshal.SizeOfจะส่งคืนขนาดของโครงสร้างที่จะส่งผ่านไปยังโค้ดเนทีฟซึ่งไม่จำเป็นต้องมีความสัมพันธ์ใด ๆ กับขนาดของโครงสร้างในโค้ด. NET
supercat

5
ข้อสังเกตที่น่าสนใจ: โมโนให้ผลลัพธ์ที่ถูกต้อง สภาพแวดล้อม: CLR 4.0.30319.17020 บน Unix 3.13.0.24 (64 บิต) Int32Wrapper: 4 TwoInt32s: 8 TwoInt32Wrappers: 8 RefAndTwoInt32s: 16 RefAndTwoInt32Wrappers: 16
AndreyAkinshin

คำตอบ:


85

ฉันคิดว่านี่เป็นจุดบกพร่อง คุณกำลังเห็นผลข้างเคียงของรูปแบบอัตโนมัติมันชอบจัดแนวเขตข้อมูลที่ไม่สำคัญให้ตรงกับที่อยู่ที่มีขนาด 8 ไบต์หลายเท่าในโหมด 64 บิต มันเกิดขึ้นแม้ว่าคุณจะใช้[StructLayout(LayoutKind.Sequential)]แอตทริบิวต์อย่างชัดเจน นั่นเป็นสิ่งที่ไม่ควรเกิดขึ้น

คุณสามารถดูได้โดยทำให้สมาชิกโครงสร้างเป็นสาธารณะและต่อท้ายรหัสทดสอบดังนี้:

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

เมื่อเบรกพอยต์ได้รับความนิยมให้ใช้ Debug + Windows + Memory + Memory 1 เปลี่ยนเป็นจำนวนเต็ม 4 ไบต์และใส่&testในช่อง Address:

 0x000000E928B5DE98  0ed750e0 000000e9 11111111 00000000 22222222 00000000 

0xe90ed750e0เป็นตัวชี้สตริงบนเครื่องของฉัน (ไม่ใช่ของคุณ) คุณสามารถดูได้อย่างง่ายดายInt32Wrappersด้วยช่องว่างภายใน 4 ไบต์ที่เปลี่ยนขนาดเป็น 24 ไบต์ กลับไปที่โครงสร้างและวางสตริงสุดท้าย ทำซ้ำแล้วคุณจะเห็นว่าตัวชี้สตริงยังคงเป็นอันดับแรก ละเมิดคุณมีLayoutKind.SequentialLayoutKind.Auto

มันเป็นไปได้ยากที่จะโน้มน้าวให้ไมโครซอฟท์ในการแก้ไขปัญหานี้จะได้ทำงานแบบนี้นานเกินไปดังนั้นการเปลี่ยนแปลงใด ๆ ที่เป็นไปได้ที่จะหมดบางสิ่งบางอย่าง CLR พยายามให้เกียรติ[StructLayout]สำหรับโครงสร้างเวอร์ชันที่มีการจัดการและทำให้เป็นแบบ blittable โดยทั่วไปจะยอมแพ้อย่างรวดเร็ว มีชื่อเสียงสำหรับโครงสร้างใด ๆ ที่มี DateTime คุณจะได้รับการรับประกัน LayoutKind ที่แท้จริงเมื่อจัดโครงสร้าง รุ่น marshaled มีขนาด 16 ไบต์อย่างที่Marshal.SizeOf()จะบอกคุณ

การใช้การLayoutKind.Explicitแก้ไขไม่ใช่สิ่งที่คุณต้องการได้ยิน


7
"มันจะเป็นเรื่องยากที่จะโน้มน้าวให้ Microsoft แก้ไขปัญหานี้ แต่ก็ใช้วิธีนี้มานานเกินไปดังนั้นการเปลี่ยนแปลงใด ๆ จะทำลายบางสิ่งบางอย่าง" ความจริงที่เห็นได้ชัดว่าสิ่งนี้ไม่ปรากฏใน 32 บิตหรือโมโนอาจช่วยได้ (ตามความคิดเห็นอื่น ๆ )
NPSF3000

เอกสารของ StructLayoutAttribute น่าสนใจทีเดียว โดยทั่วไปแล้วจะมีการควบคุมเฉพาะประเภท blittable ผ่าน StructLayout ในหน่วยความจำที่มีการจัดการ น่าสนใจไม่เคยรู้มาก่อน
Michael Stum

@Soner ไม่มันไม่ได้แก้ไข คุณวาง Layout บนทั้งสองช่องให้เป็นออฟเซ็ต 8 หรือไม่? ถ้าเป็นอย่างนั้น x และ y ก็เหมือนกันและการเปลี่ยนอันหนึ่งจะเปลี่ยนอีกอัน ไม่ชัดเจนว่าจอนเป็นอย่างไร
BartoszAdamczewski

การแทนที่stringด้วยประเภทการอ้างอิงใหม่อื่น ( class) ที่มีการใช้งาน[StructLayout(LayoutKind.Sequential)]ดูเหมือนจะไม่เปลี่ยนแปลงอะไรเลย ในทางตรงกันข้ามใช้[StructLayout(LayoutKind.Auto)]กับการstruct Int32Wrapperเปลี่ยนแปลงการใช้หน่วยความจำในTwoInt32Wrappers.
Jeppe Stig Nielsen

1
"มันจะเป็นเรื่องยากที่จะโน้มน้าวให้ Microsoft แก้ไขปัญหานี้ แต่ก็ใช้วิธีนี้มานานเกินไปดังนั้นการเปลี่ยนแปลงใด ๆ จะทำลายบางสิ่งบางอย่าง" xkcd.com/1172
iCodeSometime

19

EDIT2

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

รหัสนี้จะอยู่ในแนว 8 ไบต์ดังนั้นโครงสร้างจะมี 16 ไบต์ โดยการเปรียบเทียบสิ่งนี้:

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

จะอยู่ในแนว 4 ไบต์ดังนั้นโครงสร้างนี้จะมี 16 ไบต์ด้วย ดังนั้นเหตุผลที่นี่คือการจัดโครงสร้างใน CLR ถูกกำหนดโดยจำนวนของเขตข้อมูลที่จัดแนวมากที่สุด clases ไม่สามารถทำได้อย่างชัดเจนดังนั้นพวกเขาจะยังคงอยู่ในแนวเดียวกัน 8 ไบต์

ตอนนี้ถ้าเรารวมสิ่งนั้นเข้าด้วยกันและสร้างโครงสร้าง:

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

จะมี 24 ไบต์ {x, y} จะมี 4 ไบต์แต่ละอันและ {z, s} จะมี 8 ไบต์ เมื่อเราแนะนำประเภทอ้างอิงในโครงสร้าง CLR จะจัดโครงสร้างที่กำหนดเองของเราให้ตรงกับการจัดตำแหน่งคลาสเสมอ

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

โค้ดนี้จะมี 24 ไบต์เนื่องจาก Int32Wrapper จะถูกจัดแนวให้ยาวเหมือนเดิม ดังนั้นตัวคลุมโครงสร้างที่กำหนดเองจะจัดแนวให้อยู่ในเขตข้อมูลที่จัดแนวสูงสุด / ดีที่สุดในโครงสร้างหรือตามช่องที่สำคัญที่สุดภายในของตัวเองเสมอ ดังนั้นในกรณีของสตริงการอ้างอิงที่มีความยาว 8 ไบต์ที่จัดเรียงโครงสร้างของ wrapper จะจัดแนวตามนั้น

การสรุปฟิลด์โครงสร้างที่กำหนดเองภายในโครงสร้างจะถูกจัดให้ตรงกับฟิลด์อินสแตนซ์ที่จัดชิดสูงสุดในโครงสร้างเสมอ ตอนนี้ถ้าฉันไม่แน่ใจว่านี่เป็นข้อผิดพลาด แต่ไม่มีหลักฐานฉันจะยึดตามความเห็นของฉันว่านี่อาจเป็นการตัดสินใจอย่างมีสติ


แก้ไข

ขนาดจะถูกต้องจริงก็ต่อเมื่อจัดสรรบนฮีป แต่โครงสร้างเองมีขนาดที่เล็กกว่า (ขนาดที่แน่นอนของฟิลด์) การวิเคราะห์เพิ่มเติมเพื่อชี้ให้เห็นว่านี่อาจเป็นข้อบกพร่องในรหัส CLR แต่จำเป็นต้องมีหลักฐานสำรอง

ฉันจะตรวจสอบรหัส cli และโพสต์การอัปเดตเพิ่มเติมหากพบสิ่งที่เป็นประโยชน์


นี่คือกลยุทธ์การจัดตำแหน่งที่ใช้โดยตัวจัดสรร. NET mem

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

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

    Console.ReadKey();
}

รหัสนี้รวบรวมด้วย. net40 ภายใต้ x64 ใน WinDbg ให้ทำสิ่งต่อไปนี้:

ให้ค้นหาประเภทบน 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

เมื่อเรามีแล้วให้ดูว่ามีอะไรอยู่ภายใต้ที่อยู่นั้น:

    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

เราเห็นว่านี่คือ ValueType และเป็นประเภทที่เราสร้างขึ้น เนื่องจากนี่คืออาร์เรย์เราจึงต้องได้รับ ValueType def ขององค์ประกอบเดียวในอาร์เรย์:

    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

จริงๆแล้วโครงสร้างมีขนาด 32 ไบต์เนื่องจากมีการสงวนไว้สำหรับการขยายขนาด 16 ไบต์ดังนั้นในความเป็นจริงทุกโครงสร้างมีขนาดอย่างน้อย 16 ไบต์จากการเริ่มต้น

หากคุณเพิ่ม 16 ไบต์จาก ints และสตริงที่อ้างถึง: 0000000003e72d18 + 8 ไบต์ EE / padding คุณจะจบลงที่ 0000000003e72d30 และนี่คือจุดเริ่มต้นสำหรับการอ้างอิงสตริงและเนื่องจากการอ้างอิงทั้งหมดเป็น 8 ไบต์ที่เติมจากฟิลด์ข้อมูลจริงแรก สิ่งนี้ประกอบขึ้นเป็น 32 ไบต์สำหรับโครงสร้างนี้

มาดูกันว่าสตริงนั้นถูกบุด้วยวิธีนี้หรือไม่:

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  <<

ตอนนี้ให้วิเคราะห์โปรแกรมข้างต้นในลักษณะเดียวกัน:

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

โครงสร้างของเราตอนนี้คือ 48 ไบต์

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

ที่นี่สถานการณ์จะเหมือนกันถ้าเราเพิ่มเป็น 0000000003c22d18 + 8 ไบต์ของการอ้างอิงสตริงเราจะจบลงที่จุดเริ่มต้นของ Int wrapper แรกซึ่งค่าจริงชี้ไปยังที่อยู่ที่เราอยู่

ตอนนี้เราจะเห็นว่าแต่ละค่าเป็นการอ้างอิงวัตถุอีกครั้งให้ยืนยันโดยการมอง 0000000003c22d20

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

อันที่จริงมันถูกต้องเนื่องจากโครงสร้างที่อยู่บอกเราว่าไม่มีอะไรถ้านี่คือ obj หรือ 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

ดังนั้นในความเป็นจริงนี่เป็นเหมือนประเภท Union ที่จะได้รับ 8 ไบต์ที่จัดตำแหน่งในครั้งนี้ (paddings ทั้งหมดจะสอดคล้องกับโครงสร้างหลัก) ถ้าไม่เป็นเช่นนั้นเราจะได้ 20 ไบต์และนั่นไม่ดีที่สุดดังนั้นตัวจัดสรร mem จะไม่ยอมให้มันเกิดขึ้น หากคุณคำนวณอีกครั้งจะพบว่าโครงสร้างมีขนาด 40 ไบต์

ดังนั้นหากคุณต้องการใช้หน่วยความจำแบบอนุรักษ์นิยมมากขึ้นคุณไม่ควรบรรจุไว้ในประเภทโครงสร้างแบบกำหนดเองของ struct แต่ให้ใช้อาร์เรย์ธรรมดาแทน อีกวิธีหนึ่งคือการจัดสรรหน่วยความจำออกจากฮีป (VirtualAllocEx สำหรับเช่น) วิธีนี้คุณจะได้รับบล็อกหน่วยความจำของคุณเองและคุณจัดการได้ตามที่คุณต้องการ

คำถามสุดท้ายที่นี่คือทำไมเราถึงได้เลย์เอาต์แบบนั้นในทันใด ถ้าคุณเปรียบเทียบโค้ด jited กับประสิทธิภาพของการเพิ่ม int [] กับ struct [] ด้วยการเพิ่มฟิลด์ตัวนับอันที่สองจะสร้างแอดเดรสที่จัดชิด 8 ไบต์ซึ่งเป็นยูเนี่ยน แต่เมื่อเปรียบเทียบสิ่งนี้จะแปลเป็นโค้ดแอสเซมบลีที่ดีที่สุด (singe LEA เทียบกับหลาย MOV) อย่างไรก็ตามในกรณีที่อธิบายไว้ที่นี่ประสิทธิภาพจะแย่ลงจริง ๆ ดังนั้นสิ่งที่ฉันทำก็คือสิ่งนี้สอดคล้องกับการใช้งาน CLR พื้นฐานเนื่องจากเป็นประเภทที่กำหนดเองที่สามารถมีหลายฟิลด์ดังนั้นจึงอาจง่ายกว่า / ดีกว่าที่จะใส่ที่อยู่เริ่มต้นแทนที่จะเป็น ค่า (เนื่องจากเป็นไปไม่ได้) และทำโครงสร้างช่องว่างที่นั่นจึงส่งผลให้ขนาดไบต์ใหญ่ขึ้น


1
เมื่อดูตัวเองแล้วขนาดRefAndTwoInt32Wrappers ไม่ใช่ 32 ไบต์ - คือ 24 ซึ่งเท่ากับรายงานด้วยรหัสของฉัน หากคุณดูในมุมมองหน่วยความจำแทนที่จะใช้dumparrayและดูหน่วยความจำสำหรับอาร์เรย์ที่มี (พูด) 3 องค์ประกอบที่มีค่าแยกแยะได้คุณจะเห็นได้อย่างชัดเจนว่าแต่ละองค์ประกอบประกอบด้วยการอ้างอิงสตริง 8 ไบต์และจำนวนเต็ม 8 ไบต์สองตัว . ฉันสงสัยว่าdumparrayกำลังแสดงค่าเป็นข้อมูลอ้างอิงเพียงเพราะไม่รู้วิธีแสดงInt32Wrapperค่า "ข้อมูลอ้างอิง" เหล่านั้นชี้ไปที่ตัวเอง ไม่ใช่ค่าที่แยกจากกัน
Jon Skeet

1
ฉันไม่ค่อยแน่ใจว่าคุณได้ "ช่องว่าง 16 ไบต์" มาจากที่ใด แต่ฉันสงสัยว่าอาจเป็นเพราะคุณกำลังดูขนาดของวัตถุอาร์เรย์ซึ่งจะเป็น "16 ไบต์ + ขนาดองค์ประกอบจำนวน *" ดังนั้นอาร์เรย์ที่มีจำนวน 2 จึงมีขนาด 72 (16 + 2 * 24) ซึ่งเป็นสิ่งที่dumparrayแสดง
Jon Skeet

@jon คุณทิ้งโครงสร้างของคุณและตรวจสอบว่ามีพื้นที่ว่างบนฮีปหรือไม่? โดยปกติขนาดอาร์เรย์จะถูกเก็บไว้ที่จุดเริ่มต้นของอาร์เรย์ซึ่งสามารถตรวจสอบได้
BartoszAdamczewski

@jon ขนาดที่รายงานยังมีค่าชดเชยของสตริงซึ่งเริ่มต้นที่ 8 ฉันไม่คิดว่า 8 ไบต์พิเศษที่กล่าวถึงนั้นมาจากอาร์เรย์เนื่องจากอาร์เรย์ส่วนใหญ่อยู่ก่อนแอดเดรสองค์ประกอบแรก แต่ฉันจะตรวจสอบอีกครั้งและ แสดงความคิดเห็นเกี่ยวกับเรื่องนั้น
BartoszAdamczewski

1
ไม่ ThreeInt32Wrappers ลงท้ายด้วย 12 ไบต์, FourInt32Wrappers เป็น 16, FiveInt32Wrappers เป็น 20 ฉันไม่เห็นอะไรที่สมเหตุสมผลเกี่ยวกับการเพิ่มฟิลด์ประเภทการอ้างอิงที่เปลี่ยนเค้าโครงอย่างมาก และทราบว่ามันเป็นความสุขมากที่จะไม่สนใจการจัดตำแหน่ง 8 Int32ไบต์เมื่อฟิลด์เป็นประเภท ฉันไม่ค่อยใส่ใจกับสิ่งที่มันทำในสแต็กพูดตามตรง - แต่ฉันยังไม่ได้ตรวจสอบ
Jon Skeet

9

สรุปดูคำตอบของ @Hans Passant อาจจะอยู่ด้านบน ลำดับการออกแบบไม่ทำงาน


การทดสอบบางอย่าง:

แน่นอนเฉพาะใน 64 บิตและการอ้างอิงวัตถุ "พิษ" โครงสร้าง 32 บิตทำในสิ่งที่คุณคาดหวัง:

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

ทันทีที่มีการเพิ่มการอ้างอิงวัตถุโครงสร้างทั้งหมดจะขยายเป็น 8 ไบต์แทนที่จะเป็นขนาด 4 ไบต์ ขยายการทดสอบ:

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

อย่างที่คุณเห็นทันทีที่มีการเพิ่มข้อมูลอ้างอิงทุกๆ Int32Wrapper จะกลายเป็น 8 ไบต์ดังนั้นจึงไม่ใช่การจัดแนวแบบธรรมดา ฉันย่อการจัดสรรอาร์เรย์ลงในกรณีที่เป็นการจัดสรร LoH ซึ่งจัดแนวต่างกัน


4

เพียงเพื่อเพิ่มข้อมูลบางส่วนลงในส่วนผสม - ฉันสร้างขึ้นอีกหนึ่งประเภทจากที่คุณมี:

struct RefAndTwoInt32Wrappers2
{
    string text;
    TwoInt32Wrappers z;
}

โปรแกรมเขียนว่า:

RefAndTwoInt32Wrappers2: 16

ดังนั้นดูเหมือนว่าTwoInt32Wrappersสอดคล้อง struct ถูกต้องในใหม่RefAndTwoInt32Wrappers2struct


คุณใช้งาน 64 บิตหรือไม่ การจัดตำแหน่งทำได้ดีใน 32 บิต
Ben Adams

การค้นพบของฉันเหมือนกับคนอื่น ๆ สำหรับสภาพแวดล้อมต่างๆ
Jesse C. Slicer
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.