سؤال

قبل أن تتفاعل من القناة الهضمية، كما فعلت في البداية، اقرأ السؤال بأكمله من فضلك.أعلم أنهم يجعلونك تشعر بالقذارة، وأعلم أننا جميعًا قد تعرضنا للحرق من قبل وأعلم أن هذا ليس "أسلوبًا جيدًا" ولكن، هل الملاعب العامة على ما يرام على الإطلاق؟

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

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

إليك نموذجًا لتطبيق الاختبار:


struct Point {
    public Point(double x, double y, double z) {
        _x = x;
        _y = y;
        _z = z;
    }

    public readonly double _x;
    public readonly double _y;
    public readonly double _z;

    public double X { get { return _x; } }
    public double Y { get { return _y; } }
    public double Z { get { return _z; } }
}

class Program {
    static void Main(string[] args) {
        const int loopCount = 10000000;

        var point = new Point(12.0, 123.5, 0.123);

        var sw = new Stopwatch();
        double x, y, z;
        double calculatedValue;
        sw.Start();
        for (int i = 0; i < loopCount; i++) {
            x = point._x;
            y = point._y;
            z = point._z;
            calculatedValue = point._x * point._y / point._z;
        }
        sw.Stop();
        double fieldTime = sw.ElapsedMilliseconds;
        Console.WriteLine("Direct field access: " + fieldTime);

        sw.Reset();
        sw.Start();
        for (int i = 0; i < loopCount; i++) {
            x = point.X;
            y = point.Y;
            z = point.Z;
            calculatedValue = point.X * point.Y / point.Z;
        }
        sw.Stop();
        double propertyTime = sw.ElapsedMilliseconds;
        Console.WriteLine("Property access: " + propertyTime);

        double totalDiff = propertyTime - fieldTime;
        Console.WriteLine("Total difference: " + totalDiff);
        double averageDiff = totalDiff / loopCount;
        Console.WriteLine("Average difference: " + averageDiff);

        Console.ReadLine();
    }
}

نتيجة:
الوصول المباشر للميدان:3262
الوصول إلى الممتلكات:24248
الفرق الإجمالي:20986
متوسط ​​الفرق:0.00020986


إنه فقط 21 ثانية، ولكن لماذا لا؟

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

المحلول

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

في المثال الخاص بك، تم تحسين نص الحلقة بالكامل لإصدار الوصول الميداني، ليصبح فقط:

for (int i = 0; i < loopCount; i++)
00000025  xor         eax,eax 
00000027  inc         eax  
00000028  cmp         eax,989680h 
0000002d  jl          00000027 
}

بينما الإصدار الثاني، يقوم فعليًا بإجراء تقسيم الفاصلة العائمة في كل تكرار:

for (int i = 0; i < loopCount; i++)
00000094  xor         eax,eax 
00000096  fld         dword ptr ds:[01300210h] 
0000009c  fdiv        qword ptr ds:[01300218h] 
000000a2  fstp        st(0) 
000000a4  inc         eax  
000000a5  cmp         eax,989680h 
000000aa  jl          00000096 
}

إن إجراء تغييرين صغيرين فقط على تطبيقك لجعله أكثر واقعية يجعل العمليتين متطابقتين عمليًا في الأداء.

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

تغيير من:

Point point = new Point(12.0, 123.5, 0.123);

ل:

Random r = new Random();
Point point = new Point(r.NextDouble(), r.NextDouble(), r.NextDouble());

ثانيًا، تأكد من استخدام نتائج كل تكرار للحلقة في مكان ما:

قبل كل حلقة، قم بتعيين CalcatedValue = 0 بحيث يبدأ كلاهما من نفس النقطة.بعد كل حلقة، قم باستدعاء Console.WriteLine(calculatedValue.ToString()) للتأكد من أن النتيجة "مستخدمة" حتى لا يقوم المترجم بتحسينها.وأخيرًا، قم بتغيير نص الحلقة من "calculatedValue = ..." إلى "calculatedValue += ..." بحيث يتم استخدام كل تكرار.

على جهازي، تؤدي هذه التغييرات (مع إصدار الإصدار) إلى النتائج التالية:

Direct field access: 133
Property access: 133
Total difference: 0
Average difference: 0

وكما نتوقع، فإن x86 لكل من هذه الحلقات المعدلة متطابق (باستثناء عنوان الحلقة)

000000dd  xor         eax,eax 
000000df  fld         qword ptr [esp+20h] 
000000e3  fmul        qword ptr [esp+28h] 
000000e7  fdiv        qword ptr [esp+30h] 
000000eb  fstp        st(0) 
000000ed  inc         eax  
000000ee  cmp         eax,989680h 
000000f3  jl          000000DF (This loop address is the only difference) 

نصائح أخرى

نظرًا لأنك تتعامل مع كائنات غير قابلة للتغيير بحقول للقراءة فقط، أود أن أقول إنك وصلت إلى الحالة الوحيدة عندما لا أجد الحقول العامة عادة قذرة.

IMO، قاعدة "عدم وجود حقول عامة" هي واحدة من تلك القواعد الصحيحة من الناحية الفنية، ولكن ما لم تكن تصمم مكتبة مخصصة للاستخدام من قبل الجمهور فمن غير المرجح أن تسبب لك أي مشكلة إذا قمت بخرقها.

قبل أن يتم التصويت ضدي بشكل كبير جدًا، يجب أن أضيف ذلك التغليف هو شيء جيد.بالنظر إلى الثابت "يجب أن تكون خاصية القيمة فارغة إذا كانت قيمة HasValue خاطئة"، فإن هذا التصميم معيب:

class A {
    public bool HasValue;
    public object Value;
}

ومع ذلك، نظرًا لهذا الثبات، فإن هذا التصميم معيب بنفس القدر:

class A {
    public bool HasValue { get; set; }
    public object Value { get; set; }
}

التصميم الصحيح هو

class A {
    public bool HasValue { get; private set; }
    public object Value { get; private set; }

    public void SetValue(bool hasValue, object value) {
        if (!hasValue && value != null)
            throw new ArgumentException();
        this.HasValue = hasValue;
        this.Value    = value;
    }
}

(والأفضل من ذلك هو توفير مُنشئ التهيئة وجعل الفصل غير قابل للتغيير).

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

المغزى من القصة هو أنها قد تتعارض مع كل ما تعلمته أو نصحتك به، لكن المعايير لا تكذب.إذا كان يعمل بشكل أفضل، فقط افعل ذلك.

إذا كنت حقًا بحاجة إلى هذا الأداء الإضافي، فهو كذلك من المحتمل الشيء الصواب لفعله.إذا لم تكن بحاجة إلى الأداء الإضافي، فمن المحتمل ألا يكون كذلك.

لدى ريكو مارياني بعض المشاركات ذات الصلة:

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

وفي أحيان أخرى، يبدو الأمر "خطأ" للغاية للقيام بذلك.

سوف يعتني CLR بالأداء من خلال تحسين الطريقة/الخاصية (في إصدارات الإصدار) بحيث لا ينبغي أن يكون ذلك مشكلة.

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

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

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

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

يحرر: حسنًا، استخدمت اختباراتي الأولية نوع بيانات int بدلاً من double.أرى فرقًا كبيرًا عند استخدام الزوجي.مع ints المباشر مقابل.الملكية هي نفسها تقريبا.مع الوصول إلى الخاصية المزدوجة، يكون الوصول أبطأ بحوالي 7 مرات من الوصول المباشر على جهازي.وهذا أمر محير بعض الشيء بالنسبة لي.

ومن المهم أيضًا إجراء الاختبارات خارج مصحح الأخطاء.حتى في إصدار الإصدار، يضيف مصحح الأخطاء عبئًا مما يؤدي إلى تحريف النتائج.

فيما يلي بعض السيناريوهات التي يكون فيها الأمر مقبولاً (من كتاب إرشادات تصميم الإطار):

  • استخدم الحقول الثابتة للثوابت التي لن تتغير أبدًا.
  • لا تستخدم حقول القراءة الثابتة العامة لحالات كائن محددة مسبقًا.

وأين لا يكون:

  • لا تُحدد مثيلات من أنواع قابلة للتغيير إلى الحقول القراءة.

مما ذكرته، لا أفهم لماذا لا يتم تضمين خصائصك التافهة بواسطة JIT؟

إذا قمت بتعديل الاختبار الخاص بك لاستخدام المتغيرات المؤقتة التي تقوم بتعيينها بدلاً من الوصول مباشرة إلى الخصائص الموجودة في حسابك، فسوف ترى تحسنًا كبيرًا في الأداء:

        sw.Start();
        for (int i = 0; i < loopCount; i++)
        {
            x = point._x;
            y = point._y;
            z = point._z;
            calculatedValue = x * y / z;
        }
        sw.Stop();
        double fieldTime = sw.ElapsedMilliseconds;
        Console.WriteLine("Direct field access: " + fieldTime);

        sw.Reset();
        sw.Start();
        for (int i = 0; i < loopCount; i++)
        {
            x = point.X;
            y = point.Y;
            z = point.Z;
            calculatedValue = x * y / z;
        }
        sw.Stop();

ربما سأكرر كلام شخص آخر، ولكن هذه نقطتي أيضًا إذا كان من الممكن أن تساعد.

تهدف التعاليم إلى تزويدك بالأدوات التي تحتاجها لتحقيق مستوى معين من السهولة عند مواجهة مثل هذه المواقف.

تنص منهجية تطوير البرمجيات Agile على أنه يتعين عليك أولاً تسليم المنتج إلى عميلك بغض النظر عن الشكل الذي قد يبدو عليه الكود الخاص بك.ثانيًا، يمكنك تحسين التعليمات البرمجية الخاصة بك وجعلها "جميلة" أو وفقًا لحالات البرمجة الفنية.

هنا، تحتاج أنت أو عميلك إلى الأداء.في مشروعك، يعد الأداء أمرًا بالغ الأهمية، إذا فهمت ذلك بشكل صحيح.

لذا، أعتقد أنك ستوافقني على أننا لا نهتم بالشكل الذي قد يبدو عليه الكود أو ما إذا كان يحترم "الفن".افعل ما عليك لجعله فعالاً وقويًا!تسمح الخصائص للتعليمات البرمجية الخاصة بك "بتنسيق" إدخال/إخراج البيانات إذا لزم الأمر.تحتوي الخاصية على عنوان الذاكرة الخاص بها، ثم تبحث عن عنوان العضو الخاص بها عند إرجاع قيمة العضو، لذلك حصلت على بحثين عن العنوان.إذا كان الأداء بالغ الأهمية، فما عليك سوى القيام بذلك، واجعل أعضائك غير القابلين للتغيير علنيين.:-)

وهذا يعكس وجهة نظر البعض الآخر أيضًا، إذا قرأت بشكل صحيح.:)

اتمنى لك يوم جيد!

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

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