c# - لماذا تعتمد محاذاة البنية على ما إذا كان نوع الحقل بدائيًا أم معرفًا من قبل المستخدم؟




.net struct (3)

في Noda Time v2 ، ننتقل إلى حل nanosecond. هذا يعني أنه لم يعد بإمكاننا استخدام عدد صحيح من 8 بايت لتمثيل النطاق الكامل للوقت الذي نحن مهتمون به. وقد دفعني ذلك إلى التحقق من استخدام الذاكرة للبنى (العديدة) لـ Noda Time ، والذي أدى بدوره إلى للكشف عن غرابة طفيفة في قرار محاذاة CLR.

أولاً ، أدرك أن هذا قرار تنفيذ ، وأن السلوك الافتراضي قد يتغير في أي وقت. أدرك أنه بإمكاني تعديله باستخدام [StructLayout] و [FieldOffset] ، ولكنني أفضل الخروج بحل لا يتطلب ذلك إن أمكن.

السيناريو الأساسي لدي هو أن لدي struct تحتوي على حقل من نوع مرجع واثنين من حقول نوع القيمة الأخرى ، حيث تلك الحقول عبارة عن أغلفة بسيطة لـ int . كنت آمل أن يتم تمثيل ذلك في 16 بايت على CLR 64 بت (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 (أتوقع أنه "أي نوع من أنواع المراجع")
  • استخدام بنية أخرى كـ "مجمّع" حول المرجع لا يساعد
  • لا يساعد استخدام بنية عامة كملف حول المرجع
  • إذا استمر في إضافة الحقول (في أزواج من أجل البساطة) ، لا تزال الحقول int تحسب لـ 4 بايت ، Int32Wrapper الحقول Int32Wrapper لـ 8 بايت
  • إضافة [StructLayout(LayoutKind.Sequential, Pack = 4)] إلى كل بنية في الأفق لا يغير النتائج

هل هناك أي تفسير لهذا (من الناحية المثالية مع الوثائق المرجعية) أو اقتراح حول كيف يمكنني الحصول على تلميح إلى CLR التي أود أن يتم تعبئتها الحقول دون تحديد إزاحة حقل ثابت؟


أظن أن هذه حشرة. أنت تشاهد التأثير الجانبي للتخطيط التلقائي ، فهي تحب محاذاة الحقول غير العادية إلى عنوان مضاعف 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 في حقل العنوان:

 0x000000E928B5DE98  0ed750e0 000000e9 11111111 00000000 22222222 00000000 

0xe90ed750e0 هو مؤشر السلسلة على الجهاز الخاص بي (وليس ملكك). يمكنك بسهولة مشاهدة Int32Wrappers ، مع 4 بايت إضافية من الحشو التي حولت الحجم إلى 24 بايت. العودة إلى الهيكلة ووضع السلسلة الأخيرة. كرر وسترى مؤشر السلسلة لا يزال الأول. خرق LayoutKind.Sequential ، كنت قد حصلت على LayoutKind.Auto .

سيكون من الصعب إقناع Microsoft بإصلاح هذه المشكلة ، وقد نجحت في العمل بهذه الطريقة لفترة طويلة جدًا ، لذا فإن أي تغيير سيكون هو كسر شيء ما . يحاول CLR فقط تكريم [StructLayout] للنسخة المدارة من البنية وجعلها غير قابلة للتبديل ، فإنه بشكل عام يستسلم بسرعة. مشهور لأي بنية تحتوي على DateTime. تحصل فقط على ضمان LayoutKind صحيح عند تنظيم بنية. النسخة المتمرسة هي بالتأكيد 16 بايت ، كما Marshal.SizeOf() .

باستخدام LayoutKind.Explicit ، وليس ما تريد سماعه.


الملخص راجع إجابةHans Passant على الأرجح. تخطيط تسلسلي لا يعمل

بعض الاختبارات:

فمن المؤكد فقط على 64bit والمرجع الكائن "السموم" الهيكل. 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 الذي تمت محاذاته بشكل مختلف.


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 بايت {س ، ص} سيكون 4 بايت لكل و {z، s} سيكون 8 بايت. بمجرد تقديم نوع المرجع في بنية CLR ، ستقوم دائمًا بمحاذاة البنية المخصصة لتطابق محاذاة الفصل.

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

سيكون هذا الرمز 24 بايت منذ سيتم محاذاة Int32Wrapper بنفس طول. لذلك ، سيتم محاذاة غلاف المجموعة المخصصة دائمًا إلى أعلى / أفضل محاذاة للحقل في البنية أو إلى أهم الحقول الداخلية الخاصة بها. لذلك في حالة سلسلة المرجع التي هي 8 بايت تتم محاذاة المجمع المجمع إلى ذلك.

دائمًا ما يتم محاذاة حقل الإنشاء المخصص داخل البنية إلى أعلى حقل مثيل محاكي في البنية. الآن إذا لم أكن متأكداً إذا كان هذا خطأ ولكن بدون بعض الأدلة ، فسألتزم برأيي بأن هذا قد يكون قراراً واعياً.

تصحيح

تكون الأحجام في الواقع دقيقة فقط عند تخصيصها على كومة الذاكرة المؤقتة ولكن البنية نفسها لها أحجام أصغر (الأحجام الدقيقة لحقولها). مزيد من التحليل التماس لاقتراح أن هذا قد يكون خطأ في قانون 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 يتيح القيام بما يلي:

يتيح العثور على النوع على الكومة أولاً:

    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 و سلسلة ref: 0000000003e72d18 + 8 bytes 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 حيث تشير القيمة بالفعل إلى العنوان الذي نحن فيه.

الآن يمكننا أن نرى أن كل قيمة هي مرجع كائن مرة أخرى يتيح لك تأكيد ذلك عن طريق النظر إلى 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

حتى في الواقع هذا هو أشبه نوع الاتحاد الذي سيتم الحصول على 8 بايت محاذاة هذه المرة (سيتم محاذاة كافة paddings مع بنية الأصل). إذا لم يكن الأمر كذلك ، فسنحصل على 20 بايت وهذا ليس الأمثل ، لذا لن يسمح مميّز الذاكرة بحدوث ذلك. إذا قمت بإجراء الحساب مرة أخرى فإنه سيظهر أن البنية بالفعل 40 بايت من الحجم.

لذا ، إذا كنت تريد أن تكون أكثر تحفظًا مع الذاكرة ، فلا يجب عليك أبدًا أن تحزمها في بنية بنية مخصصة بنية ، بل تستخدم صفائف بسيطة بدلاً من ذلك. طريقة أخرى هي تخصيص الذاكرة إيقاف كومة الذاكرة المؤقتة (VirtualAllocEx على سبيل المثال) بهذه الطريقة يتم منحك كتلة الذاكرة الخاصة بك وإدارتها بالطريقة التي تريدها.

السؤال الأخير هنا هو لماذا فجأة قد نحصل على تخطيط مثل هذا. حسناً ، إذا قمت بمقارنة الشفرة jited وأداء int = مع زيادة البنية [] مع زيادة حقل العداد ، فستقوم الثانية بتوليد عنوان محاذاة 8 بايت كونه اتحادًا ، ولكن عندما يتحول ذلك إلى شفرة تجميع أكثر أمناً (سينغ LEA vs multiple MOV). ولكن في الحالة الموصوفة هنا ، سيكون الأداء أسوأ في الواقع ، لذا فإن ما أتخذه هو أن هذا يتماشى مع تنفيذ CLR الأساسي نظرًا لأنه نوع مخصص يمكن أن يحتوي على حقول متعددة لذا قد يكون من الأسهل / الأفضل وضع عنوان البدء بدلاً من القيمة (حيث سيكون من المستحيل) والقيام بالبنية التحتية هناك ، مما يؤدي إلى حجم بايت أكبر.





memory-alignment