الهياكل مقابل الفصول

منذ البداية ، عندما بدأت البرمجة ، ظهر سؤال حول ما يجب استخدامه لتحسين الأداء: هيكل أو صف ؛ المصفوفات التي هي أفضل للاستخدام وكيف. فيما يتعلق بالهياكل ، ترحب Apple باستخدامها ، موضحة أنها أفضل في التحسين ، وجوهر اللغة Swift هو الهياكل. ولكن هناك أولئك الذين لا يوافقون على ذلك ، لأنه يمكنك تبسيط الشفرة بشكل جميل من خلال وراثة فئة من فئة أخرى والعمل مع مثل هذه الفئة. لتسريع العمل مع الفصول الدراسية ، أنشأنا معدلات وكائنات مختلفة تم تحسينها خصيصًا للفئات ، ومن الصعب بالفعل تحديد ما سيكون أسرع وفي هذه الحالة.

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

ملاحظة: لقد بدأت العمل على مقال حول Xcode 10.3 وفكرت في محاولة مقارنة سرعته مع Xcode 11 ، ولكن لا يزال المقال لا يتعلق بمقارنة تطبيقين ، بل يتعلق بسرعة تطبيقاتنا. ليس لدي أدنى شك في أن وقت تشغيل الوظائف سوف ينخفض ​​، وأن ما تم تحسينه بشكل سيئ سيصبح أسرع. نتيجة لذلك ، انتظرت الإصدار 5.1 الجديد من Swift وقررت اختبار الفرضيات في الممارسة العملية. هل لديك قراءة لطيفة.

اختبار 1: مقارنة المصفوفات في الهياكل والفئات


لنفترض أن لدينا فصلًا ، ونريد أن نضع كائنات هذه الفئة في صفيف ، فإن الإجراء المعتاد في الصفيف هو تنفيذ ذلك.

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

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


التين. 1: مقارنة الحصول على متغير من المصفوفات على أساس الهياكل والطبقات

اختبار 2. مقارنة ContiguousArray مقابل صفيف


ما هو أكثر إثارة للاهتمام هو مقارنة أداء صفيف (صفيف) مع صفيف مرجعي (ContiguousArray) ، وهو أمر ضروري على وجه التحديد للعمل مع الفئات المخزنة في الصفيف.

لنفحص الأداء للحالات التالية:

ContiguousArray تخزين بنية مع نوع القيمة
ContiguousArray تخزين الهيكل مع سلسلة
تخزين الفئة ContiguousArray بنوع القيمة
ContiguousArray تخزين الطبقة مع سلسلة
صفيف تخزين الهيكل مع نوع القيمة
صفيف تخزين الهيكل مع سلسلة
صف تخزين الطبقة مع نوع القيمة
صف تخزين الطبقة مع سلسلة

نظرًا لأن نتائج الاختبار (الاختبارات: الانتقال إلى وظيفة مع إيقاف تشغيل التحسين المضمّن في وضع إيقاف التشغيل ، فإن الانتقال إلى وظيفة مع تشغيل التحسين المضمّن قيد التشغيل ، وحذف العناصر ، وإضافة العناصر ، والوصول التسلسلي إلى عنصر في حلقة) سيشمل عددًا كبيرًا من الاختبارات (لـ 8 صفائف من 5 اختبارات لكل) ، سأقدم أهم النتائج:

  1. إذا قمت بالاتصال بوظيفة وقمت بتمرير صفيف إلى داخلها ، وإيقاف تشغيلها داخل السطر ، فستكون هذه المكالمة مكلفة للغاية (بالنسبة للفئات المستندة إلى السلسلة المرجعية ، تكون أبطأ بمقدار 20 ألف مرة ، وبالنسبة للفئات المستندة إلى القيمة ، يكون النوع 60،000 مرة ، وأسوأ من ذلك مع إيقاف تشغيل المحسن المضمن) .
  2. إذا كانت عملية التحسين (مضمنة) مناسبة لك ، فيجب توقع تدهورها مرتين فقط ، اعتمادًا على نوع البيانات التي تتم إضافتها إلى الصفيف. الاستثناء الوحيد هو نوع القيمة ، ملفوف في بنية ملقاة في ContiguousArray - دون تدهور الوقت.
  3. الإزالة - كان الفارق بين المصفوفة المرجعية والمعتادة حوالي 20 ٪ (لصالح المصفوفة المعتادة).
  4. إلحاق - عند استخدام كائنات ملفوفة في الفئات ، كانت سرعة ContiguousArray أسرع بنحو 20٪ من Array مع نفس الكائنات ، بينما كانت Array أسرع عند العمل مع بنيات من ContiguousArray مع بنيات.
  5. تبين أن الوصول إلى عناصر الصفيف عند استخدام wrappers من الهياكل يكون أسرع من أي wrappers على الفئات ، بما في ذلك ContiguousArray (حوالي 500 مرة أسرع).

في معظم الحالات ، يكون استخدام المصفوفات العادية للعمل مع الكائنات أكثر كفاءة. تستخدم من قبل ، ونحن نستخدم كذلك.

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

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

اختبار 3: مقارنة أداء الهياكل والفئات تخزين الطبقات الكبيرة


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


التين. 2: أداء الهياكل والفئات من المادة "التوقف عن استخدام الهياكل"

الاختلافات في نتائج الاختبار لا تذكر.

اختبار 4: وظيفة قبول عام ، بروتوكول ، ووظيفة دون عام


إذا أخذنا وظيفة عامة وقمنا بتمرير قيمتين هناك ، متحدين فقط من خلال القدرة على مقارنة هذه القيم (func min) ، فسوف يتحول رمز ثلاثة أسطر إلى كود من ثمانية (كما تقول Apple). ولكن هذا لا يحدث دائمًا ، فإن Xcode له طرق تحسين حيث يرى أنه يتم تمرير قيمتين هيكليتين إليها عندما تستدعي الوظيفة ، فإنها تنشئ تلقائيًا وظيفة تأخذ بنائين ولا تنسخ القيم بعد الآن.


التين. 3: وظيفة عامة نموذجية

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


التين. 4: مقارنة بين وظائف المضيف عام وبروتوكول.

اختبار 5: قارن بين استدعاء الأسلوب الأصل والطريقة الأصلية ، وفي الوقت نفسه تحقق من الدرجة النهائية لمثل هذه المكالمة


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


التين. 5: المكالمات طريقة الطبقة ، لاختبار الإرسال

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

اختبار 6: استدعاء متغير مع المعدل النهائي مقابل متغير فئة العادية


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


التين. 6: استدعاء المتغير النهائي

من الواضح أن المُعدّل لم يستفد من المتغير ، وهو دائمًا أبطأ من منافسه.

اختبار 7: مشكلة تعدد الأشكال وبروتوكولات للهياكل. أو أداء الحاوية الوجودية


المشكلة: إذا أخذنا بروتوكولًا يدعم طريقة معينة والعديد من الهياكل الموروثة من هذا البروتوكول ، فما الذي سيفكر به مترجمنا عندما نضع هياكل ذات أحجام مختلفة من القيم المخزنة في صفيف واحد ، موحدًا بالبروتوكول الأصلي؟

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

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


الشكل 7. مقارنة أداء صفيف يقوم بإنشاء ارتباط لكائن والذي يحتوي عليه

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

قليلا من الناحية النظرية لتحسين مشاريعك


يمكن أن تؤثر العوامل التالية على أداء الهياكل:

  • حيث يتم تخزين المتغيرات الخاصة به (كومة الذاكرة المؤقتة / مكدس) ؛
  • الحاجة إلى حساب مرجعي للخصائص ؛
  • طرق الجدولة (ثابت / ديناميكي) ؛
  • يستخدم Copy-On-Write فقط من خلال هياكل البيانات التي هي أنواع مرجعية تتظاهر بأنها هياكل (سلسلة ، صفيف ، مجموعة ، قاموس) أسفل الغطاء.

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

من الطبقات هي سيئة وخطيرة بالمقارنة مع الهياكل



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

فهي ليست سريعة مثل الهياكل.

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

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

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

رمز التعقيد يتزايد بسبب هذا.

المزيد من الكود = المزيد من الأخطاء ، دائما!

النتائج


اعتقدت أن الاستنتاجات الواردة في هذه المقالة ضرورية ببساطة ، لأنني لا أريد أن أقرأ المقال من وقت لآخر ، وأن قائمة النقاط الموحدة ضرورية بكل بساطة. لخص الخطوط تحت الاختبارات ، أود أن أبرز ما يلي:

  1. من الأفضل وضع المصفوفات في صفيف.
  2. إذا كنت تريد إنشاء صفيف من الفئات ، فمن الأفضل اختيار Array عادي ، نظرًا لأن ContiguousArray نادراً ما يوفر مزايا ، وهي ليست عالية جدًا.
  3. يعمل التحسين المضمّن على تسريع العمل ، ولا تقم بإيقاف تشغيله.
  4. يكون الوصول إلى عناصر Array دائمًا أسرع من الوصول إلى عناصر ContiguousArray.
  5. تكون الهياكل دائمًا أسرع من الفئات (إلا إذا قمت بالطبع بتمكين تحسين الوحدة بالكامل أو تحسين مماثل).
  6. عند تمرير كائن إلى دالة واستدعاء خصائصه ، بدءًا من الثالثة ، تكون البنية أسرع من الفئات.
  7. عندما تقوم بتمرير قيمة إلى دالة مكتوبة لـ Generic and Protocol ، فإن Generic سيكون أسرع.
  8. مع وراثة فئة متعددة ، تتناقص سرعة استدعاء الوظيفة.
  9. تميزت المتغيرات العمل النهائي أكثر ببطء من الفلفل العادية.
  10. إذا قبلت إحدى الكائنات كائنًا يجمع بين عدة كائنات مع البروتوكول ، فستعمل بسرعة إذا تم تخزين خاصية واحدة فقط فيها ، وستتحلل بدرجة كبيرة عند إضافة المزيد من الخصائص.

المراجع:
medium.com/@vhart/protocols-generics-and-existential-containers-wait-what-e2e698262ab1
developer.apple.com/videos/play/wwdc2016/416
developer.apple.com/videos/play/wwdc2015/409
developer.apple.com/videos/play/wwdc2016/419
medium.com/commencis/stop-using-structs-e1be9a86376f
اختبار شفرة المصدر

Source: https://habr.com/ru/post/ar472290/


All Articles