سؤال

كنت أحاول تحديد النفقات العامة للرأس على صفيف .NET (في عملية 32 بت) باستخدام هذا الرمز:

long bytes1 = GC.GetTotalMemory(false);
object[] array = new object[10000];
    for (int i = 0; i < 10000; i++)
        array[i] = new int[1];
long bytes2 = GC.GetTotalMemory(false);
array[0] = null; // ensure no garbage collection before this point

Console.WriteLine(bytes2 - bytes1);
// Calculate array overhead in bytes by subtracting the size of 
// the array elements (40000 for object[10000] and 4 for each 
// array), and dividing by the number of arrays (10001)
Console.WriteLine("Array overhead: {0:0.000}", 
                  ((double)(bytes2 - bytes1) - 40000) / 10001 - 4);
Console.Write("Press any key to continue...");
Console.ReadKey();

وكانت النتيجة

    204800
    Array overhead: 12.478

في عملية 32 بت ، يجب أن يكون الكائن [1] بنفس حجم int [1] ، ولكن في الواقع يقفز النفقات العامة بمقدار 3.28 بايت إلى

    237568
    Array overhead: 15.755

أي شخص يعرف لماذا؟

(بالمناسبة ، إذا كان أي شخص فضولي ، فإن النفقات العامة للكائنات غير الجيرية ، على سبيل المثال (الكائن) I في الحلقة أعلاه ، تبلغ حوالي 8 بايت (8.384). سمعت أنها 16 بايت في عمليات 64 بت.)

هل كانت مفيدة؟

المحلول

إليك برنامجًا قصيرًا (IMO) قصيرًا ولكنه كامل لإظهار نفس الشيء:

using System;

class Test
{
    const int Size = 100000;

    static void Main()
    {
        object[] array = new object[Size];
        long initialMemory = GC.GetTotalMemory(true);
        for (int i = 0; i < Size; i++)
        {
            array[i] = new string[0];
        }
        long finalMemory = GC.GetTotalMemory(true);
        GC.KeepAlive(array);

        long total = finalMemory - initialMemory;

        Console.WriteLine("Size of each element: {0:0.000} bytes",
                          ((double)total) / Size);
    }
}

لكنني أحصل على نفس النتائج - تكون النفقات العامة لأي صفيف نوع مرجعي هي 16 بايت ، في حين أن النفقات العامة لأي صفيف نوع القيمة هي 12 بايت. ما زلت أحاول معرفة سبب ذلك ، بمساعدة مواصفات CLI. لا تنس أن صفائف النوع المرجعية متغيرة ، والتي قد تكون ذات صلة ...

تحرير: بمساعدة cordbg ، يمكنني تأكيد إجابة براين - مؤشر نوع مجموعة من النوع المرجعي هو نفسه بغض النظر عن نوع العنصر الفعلي. من المفترض أن يكون هناك بعض الفنك في object.GetType() (وهو غير قذر ، تذكر) لحساب هذا.

لذلك ، مع رمز:

object[] x = new object[1];
string[] y = new string[1];
int[] z = new int[1];
z[0] = 0x12345678;
lock(z) {}

ينتهي بنا المطاف بشيء مثل ما يلي:

Variables:
x=(0x1f228c8) <System.Object[]>
y=(0x1f228dc) <System.String[]>
z=(0x1f228f0) <System.Int32[]>

Memory:
0x1f228c4: 00000000 003284dc 00000001 00326d54 00000000 // Data for x
0x1f228d8: 00000000 003284dc 00000001 00329134 00000000 // Data for y
0x1f228ec: 00000000 00d443fc 00000001 12345678 // Data for z

لاحظ أنني ألقيت كلمة الذاكرة 1 قبل قيمة المتغير نفسه.

ل x و y, والقيم هي:

  • كتلة المزامنة ، المستخدمة في قفل رمز التجزئة (أو أ قفل رقيق - انظر تعليق براين)
  • اكتب المؤشر
  • حجم الصفيف
  • مؤشر نوع العنصر
  • مرجع فارغ (العنصر الأول)

ل z, والقيم هي:

  • كتلة المزامنة
  • اكتب المؤشر
  • حجم الصفيف
  • 0x12345678 (العنصر الأول)

ينتهي الأمر بمصفوفات نوع القيمة المختلفة (البايت [] ، int [] إلخ) بمؤشرات نوع مختلفة ، في حين تستخدم جميع صفائف نوع المرجع نفس مؤشر النوع ، ولكن لها مؤشر نوع عنصر مختلف. مؤشر نوع العنصر هو نفس القيمة التي تجدها كمؤشر النوع لكائن من هذا النوع. لذلك إذا نظرنا إلى ذاكرة كائن السلسلة في التشغيل أعلاه ، فسيحتوي على مؤشر نوع 0x00329134.

الكلمة قبل مؤشر النوع بالتأكيد شيئا ما لتفعله إما الشاشة أو رمز التجزئة: الاتصال GetHashCode() يملأ هذا الجزء من الذاكرة ، وأعتقد أن الافتراضي object.GetHashCode() يحصل على كتلة المزامنة لضمان تفرد رمز التجزئة لعمر الكائن. ومع ذلك ، مجرد القيام lock(x){} لم تفعل أي شيء ، مما فاجأني ...

كل هذا صالح فقط لأنواع "المتجهات" ، بالمناسبة - في CLR ، نوع "المتجه" هو صفيف أحادي البعد مع وجود ملاعب أقل من 0. ستحصل المصفوفات الأخرى على تخطيط مختلف - لشيء واحد ، سيحتاجون إلى الحد الأدنى المخزن ...

حتى الآن كانت هذه التجريب ، ولكن هذا هو التخمين - سبب تنفيذ النظام كما هو. من هنا فصاعدًا ، أنا حقًا أخمن.

  • الجميع object[] يمكن للصفائف مشاركة نفس رمز JIT. سوف يتصرفون بنفس الطريقة من حيث تخصيص الذاكرة ، وصول الصفيف ، Length الخاصية و (الأهم) تخطيط المراجع لـ GC. قارن ذلك باستخدام صفائف نوع القيمة ، حيث قد يكون لأنواع القيمة المختلفة "آثار أقدام" GC مختلفة (على سبيل المثال ، قد يكون لدى المرء بايت ومن ثم مرجع ، لن يكون لدى الآخرين أي مراجع على الإطلاق ، إلخ).
  • في كل مرة تقوم فيها بتعيين قيمة داخل object[] يحتاج وقت التشغيل إلى التحقق من أنه صالح. يحتاج إلى التحقق من أن نوع الكائن الذي تستخدمه لقيمة العنصر الجديدة متوافق مع نوع عنصر المصفوفة. على سبيل المثال:

    object[] x = new object[1];
    object[] y = new string[1];
    x[0] = new object(); // Valid
    y[0] = new object(); // Invalid - will throw an exception
    

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

  • هل القيمة المراد نسخها مرجعًا فارغًا؟ إذا كان الأمر كذلك ، فلا بأس. (منتهي.)
  • جلب مؤشر نوع الكائن النقاط المرجعية في.
  • هل هذا النوع مؤشر هو نفسه مؤشر نوع العنصر (فحص المساواة الثنائية البسيطة)؟ إذا كان الأمر كذلك ، فلا بأس. (منتهي.)
  • هل هذا النوع متوافق مع مهمة مؤشر مع مؤشر نوع العنصر؟ (فحص أكثر تعقيدًا ، مع وجود الميراث والواجهات المعنية.) إذا كان الأمر كذلك ، فهذا جيد - وإلا ، رمي استثناء.

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

لذلك ، لهذا السبب أعتقد أن صفائف النوع المرجعية أكبر قليلاً من صفائف نوع القيمة.

سؤال رائع - مثير للاهتمام حقًا أن يتعامل فيه :)

نصائح أخرى

الصفيف هو نوع مرجعي. جميع الأنواع المرجعية تحمل حقلين إضافيين. مرجع النوع وحقل فهرس SyncBlock ، والذي يتم استخدامه من بين أشياء أخرى لتنفيذ الأقفال في CLR. وبالتالي فإن النوع النفقات العامة على الأنواع المرجعية هو 8 بايت على 32 بت. علاوة على ذلك ، تخزن الصفيف نفسه أيضًا الطول الذي يبلغ 4 بايتات أخرى. هذا يجلب إجمالي النفقات العامة إلى 12 بايت.

وتعلمت للتو من إجابة Jon Skeet ، صفائف الأنواع المرجعية لديها 4 بايتات إضافية. يمكن تأكيد ذلك باستخدام Windbg. اتضح أن الكلمة الإضافية هي مرجع نوع آخر للنوع المخزن في الصفيف. يتم تخزين جميع صفائف الأنواع المرجعية داخليًا object[], ، مع الإشارة الإضافية إلى كائن النوع من النوع الفعلي. لذلك أ string[] هو حقا مجرد object[] مع إشارة إضافية إلى النوع string. لمزيد من التفاصيل يرجى الاطلاع أدناه.

القيم المخزنة في المصفوفات: تحتوي صفائف الأنواع المرجعية على إشارات إلى الكائنات ، لذلك كل إدخال في الصفيف هو حجم المرجع (أي 4 بايت على 32 بت). تقوم صفائف أنواع القيمة بتخزين القيم المضمنة ، وبالتالي فإن كل عنصر سيشغل حجم النوع المطلوب.

قد يكون هذا السؤال أيضًا مهمًا: C# قائمةu003Cdouble> الحجم مقابل المزدوج [] الحجم

تفاصيل gory

النظر في الرمز التالي

var strings = new string[1];
var ints = new int[1];

strings[0] = "hello world";
ints[0] = 42;

يوضح إرفاق Windbg ما يلي:

أولاً ، دعونا نلقي نظرة على صفيف نوع القيمة.

0:000> !dumparray -details 017e2acc 
Name: System.Int32[]
MethodTable: 63b9aa40
EEClass: 6395b4d4
Size: 16(0x10) bytes
Array: Rank 1, Number of elements 1, Type Int32
Element Methodtable: 63b9aaf0
[0] 017e2ad4
    Name: System.Int32
    MethodTable 63b9aaf0
    EEClass: 6395b548
    Size: 12(0xc) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    63b9aaf0  40003f0        0         System.Int32  1 instance       42 m_value <=== Our value

0:000> !objsize 017e2acc 
sizeof(017e2acc) =           16 (        0x10) bytes (System.Int32[])

0:000> dd 017e2acc -0x4
017e2ac8  00000000 63b9aa40 00000001 0000002a <=== That's the value

أولاً ، نقوم بتفريغ الصفيف والعنصر الواحد بقيمة 42. كما يمكن أن يرى الحجم هو 16 بايت. هذا هو 4 بايت ل int32 القيمة نفسها ، 8 بايت لنوع المرجع العادي النفقات العامة و 4 بايتات أخرى لطول الصفيف.

يوضح تفريغ RAW Syncblock ، جدول الطريقة لـ int[], وطولها وقيمة 42 (2A في HEX). لاحظ أن Syncblock يقع أمام مرجع الكائن مباشرة.

بعد ذلك ، دعونا نلقي نظرة على string[] لمعرفة ما يتم استخدام الكلمة الإضافية.

0:000> !dumparray -details 017e2ab8 
Name: System.String[]
MethodTable: 63b74ed0
EEClass: 6395a8a0
Size: 20(0x14) bytes
Array: Rank 1, Number of elements 1, Type CLASS
Element Methodtable: 63b988a4
[0] 017e2a90
    Name: System.String
    MethodTable: 63b988a4
    EEClass: 6395a498
    Size: 40(0x28) bytes <=== Size of the string
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     hello world    
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    63b9aaf0  4000096        4         System.Int32  1 instance       12 m_arrayLength
    63b9aaf0  4000097        8         System.Int32  1 instance       11 m_stringLength
    63b99584  4000098        c          System.Char  1 instance       68 m_firstChar
    63b988a4  4000099       10        System.String  0   shared   static Empty
    >> Domain:Value  00226438:017e1198 <<
    63b994d4  400009a       14        System.Char[]  0   shared   static WhitespaceChars
    >> Domain:Value  00226438:017e1760 <<

0:000> !objsize 017e2ab8 
sizeof(017e2ab8) =           60 (        0x3c) bytes (System.Object[]) <=== Notice the underlying type of the string[]

0:000> dd 017e2ab8 -0x4
017e2ab4  00000000 63b74ed0 00000001 63b988a4 <=== Method table for string
017e2ac4  017e2a90 <=== Address of the string in memory

0:000> !dumpmt 63b988a4
EEClass: 6395a498
Module: 63931000
Name: System.String
mdToken: 02000024  (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0x10
ComponentSize: 0x2
Number of IFaces in IFaceMap: 7
Slots in VTable: 196

أولا نقوم بإلقاء المصفوفة والسلسلة. بعد ذلك ، نلتف بحجم string[]. لاحظ أن Windbg يسرد النوع AS System.Object[] هنا. يتضمن حجم الكائن في هذه الحالة السلسلة نفسها ، وبالتالي فإن الحجم الإجمالي هو 20 من الصفيف بالإضافة إلى 40 للسلسلة.

من خلال إلقاء البايتات الأولية للمثال ، يمكننا أن نرى ما يلي: أولاً لدينا syncblock ، ثم يتبع جدول الطريقة لـ object[], ثم طول الصفيف. بعد ذلك ، نجد 4 بايت إضافية مع الإشارة إلى جدول الطريقة للسلسلة. يمكن التحقق من ذلك بواسطة أمر dumpmt كما هو موضح أعلاه. أخيرًا ، نجد الإشارة الفردية إلى مثيل السلسلة الفعلي.

ختاماً

يمكن تقسيم النفقات العامة للصفائف على النحو التالي (على 32 بت)

  • 4 بايت Syncblock
  • 4 بايت لجدول الطريقة (مرجع النوع) للمصفوفة نفسها
  • 4 بايت لطول الصفيف
  • تضيف صفائف الأنواع المرجعية 4 بايت أخرى للاحتفاظ بجدول الطريقة لنوع العنصر الفعلي (صفائف نوع المرجع هي object[] تحت الغطاء)

أي النفقات العامة 12 بايت لصفائف نوع القيمة و 16 بايت للحصول على صفائف النوع المرجعي.

أعتقد أنك تقوم ببعض الافتراضات المعيبة أثناء القياس ، حيث قد يكون تخصيص الذاكرة (عبر getTotalMemory) أثناء حلقتك مختلفة عن الذاكرة المطلوبة الفعلية للمصفوفات فقط - قد يتم تخصيص الذاكرة في كتل أكبر ، وقد تكون هناك كائنات أخرى في الذاكرة التي تم استصلاحها أثناء الحلقة ، إلخ.

إليك بعض المعلومات لك على صفيف النفقات العامة:

نظرًا لأن إدارة الكومة (نظرًا لأنك تتعامل مع getTotalMemory) يمكنها فقط تخصيص كتل كبيرة إلى حد ما ، والتي يتم تخصيصها الأخيرة بواسطة قطع أصغر لأغراض البرمجة بواسطة CLR.

أنا آسف على Offtopic ولكني وجدت معلومات مثيرة للاهتمام حول الذاكرة التي تكتسبها اليوم فقط صباح اليوم.

لدينا مشروع يدير كمية هائلة من البيانات (حتى 2 جيجابايت). كمساعد رئيسي نستخدمه Dictionary<T,T>. يتم إنشاء الآلاف من القواميس في الواقع. بعد تغييره إلى List<T> للمفاتيح و List<T> للقيم (قمنا بتنفيذها IDictionary<T,T> أنفسنا) انخفض استخدام الذاكرة في حوالي 30-40 ٪.

لماذا ا؟

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top