"إذا وجدت أن تكاليف التطوير الخاصة بالعمارة مفرطة ، ففكر في التكلفة التي يمكن أن تكلفك بها الهندسة المعمارية الخاطئة"
- لا أستطيع تذكر المصدر بالضبط
ذات مرة ، "منذ فترة طويلة ، في مجرة بعيدة" ، اشتريت كتاب تشارلز ويذرلي الرائع Etudes for Programmers ، في المقدمة التي أثبت فيها المؤلف الحاجة إلى دراسة الأمثلة والمهام التعليمية قبل بدء البرمجة المستقلة. أوصي بشدة بالعثور على هذا الكتاب وقراءة المقدمة (وبدون التوقف عنده ، وقراءة الباقي وحل المشكلات الواردة فيه) ، حيث لا يمكنني إثبات الحاجة إلى مثل هذه الممارسة بشكل أفضل. حتى إذا اتبعت توصيتي ، واكتسبت الكثير من المعرفة والمهارات العملية عند قراءة الكتاب ، فيمكنك العودة وقراءة هذا المنشور ، لأنه مخصص للعديد من القضايا الأخرى. وإذا لم تتبع توصياتي ، فيجب عليك أن تذهب تحت القطة.
منذ وقت ليس ببعيد في منشور قمت بتوبيخه ، عبرت عن رأيي حول RTOS محلي واحد ، ذكرت أن تنفيذ المخزن المؤقت الحلقي في مكتبة mcucpp المعروفة (وفي بعض الجوانب ، رائعة للغاية) لا يمكن اعتباره مثاليًا. سأحاول شرح وجهة نظري وتخيل التنفيذ المثالي (قدر الإمكان في العالم الحقيقي). ملاحظة - يكمن النص المقدم انتباهك في "غير المكتمل" لبعض الوقت ، ثم تظهر هذه الحالة المريحة.
نحن مستمرون في تطوير مكتبة للعمل مع جهاز طرفي ، ونحن في الصف التالي لإدارة الذاكرة والتخزين المؤقت (نعم ، نحن لا نزال نواصل العمليات التحضيرية ، ولكن بدونها بأي شكل من الأشكال). من أين تأتي الحاجة إلى تنظيم المخازن المؤقتة وما نوع الحيوانات الموجودة بها؟ الحقيقة هي أن جزءًا كبيرًا من المحيط له سرعة محدودة وأن عملية النقل ، التي بدأت بطريقة أو بأخرى ، تستغرق وقتًا معينًا ، وأحيانًا مهمًا جدًا مقارنة بإنشاء جزء آخر من المعلومات للإرسال. بالطبع ، قبل مرور هذا الوقت ، لا يمكن إجراء الإرسال التالي ، وبالتالي ، لا يمكن بدء تشغيله.
لدينا حالة كلاسيكية لزوج الكاتب والقارئ بسرعات مختلفة. من المستحيل ببساطة حل هذه المشكلة بشكل عام ، حيث "مع وجود زيادة تعسفية صغيرة ، ولكن ليس صفريًا ، في تدفق الطلبات على تدفق الخدمة ، يميل حجم قائمة الانتظار إلى ما لا نهاية" ، كما أن اللانهاية أمر مستحيل بشكل أساسي. ولكن هناك حالة خاصة للمشكلة ، عندما يكون لدينا رشقات محلية من الطلبات ، ولكن في المتوسط يكون تدفق الخدمة قادرًا على التعامل مع الحمل ، يمكن حل ذاكرة مؤقتة ذات سعة كافية. دعنا ننتبه إلى عبارة "القدرة الكافية" ، وسنتعلم فيما بعد كيفية حسابها ، طالما أن هذا ممكن بشكل أساسي يكفي بالنسبة لنا.
ما إذا كانت الذاكرة العازلة مطلقة هو بالطبع لا. بالنسبة إلى المعلومات المرسلة ، يمكنك استخدام سجل للحظر ، ولكن مع المعلومات المستلمة ، سيكون الأمر أسوأ إلى حد ما ، يجب إضافته في مكان ما قبل المعالجة ، إذا لم تتخذ التدابير المناسبة في بروتوكول المستوى الأعلى (لم يتم إنشاء التعبير السحري xon / xoff من الصفر) ، وهو أمر غير ممكن دائمًا وعلى أي حال ، عادة ما يؤدي ذلك إلى وجود قيود كبيرة على معدل الإرسال. يوجد أيضًا تطبيق للأجهزة من المخازن المؤقتة الداخلية في الأجهزة الطرفية (على الأقل لعنصر واحد) ، ولكن هذا لا يتم دائمًا وحجم المخزن المؤقت محدود تمامًا من أعلاه.
لذلك ، ما زلنا ننفذ المخزن المؤقت للبرنامج ، والذي سيكون من الطبيعي أن نستخدم طريقة FIFO (أي ، قائمة الانتظار) لتنظيم مثل هذا المخزن المؤقت ، وقائمة الانتظار ، بدورها ، يتم تنفيذها في المخزن المؤقت الحلقي مع مؤشرين. عندما أكتب "الأفضل" ، فإن هذا لا يعني على الإطلاق أن التطبيقات الأخرى (على سبيل المثال ، قائمة انتظار مرجعية) مستحيلة ، أو بها عيوب قاتلة غير مميتة. هذا التعبير يعني فقط أن التنفيذ لن يكون معقدًا وفعالًا للغاية ، على الرغم من أن البعض الآخر قد يكون له مزايا لا يمكن إنكارها عليها ، والتي سيتعين عليهم دفع ثمنها ، لأن DarZaNeBy.
نظرًا لأنه من غير المحتمل أن يكون لدى طراز MK تطبيق للأجهزة لمثل هذا الجهاز للأغراض العامة (يمكن للوحدات الطرفية الفردية أن يكون لها مخازن مؤقتة للحلقات الخاصة بها ، ولكن ليس لها علاقة بموضوع هذا المنشور) ، فسيتعين علينا إنشاء مخزن مؤقت للحلقات في الذاكرة الخطية (تطبيق على المتجه ، هذا هو ، بشكل عام ، الكائن الطبيعي الوحيد في الذاكرة القابلة للعنونة) ، ولهذا السبب ، سيكون هناك حاجة إلى فهرس مؤقت (أو ربما حتى فهرسان ، لكن المزيد عن ذلك لاحقًا). في رأيي ، فإن المخزن المؤقت الدائري مع اثنين من المؤشرات (الأرقام القياسية) هو الطريقة الوحيدة المقبولة لتنفيذ قائمة انتظار على المتجه ، ولكن هناك وجهات نظر مختلفة حول هذه المشكلة ورأيت بأم عيني تطبيقًا بأسلوب "x1 = x2 ؛ x2 = x3 ؛ ... x8 = رمز جديد "، إذا صح التعبير ، فلن أضع في الإعتبار هذه الغريبة. حقيقة أن الجزء المعطى قد يكون له الحق في الوجود في موقف محدد ومحدود للغاية لا يجعله مقبولاً بشكل عام.
سننظر في التنفيذ الصحيح لوحدة البرنامج لتنظيم المؤشر ، ولنبدأ ، مع الانتباه إلى الكلمة الأولى في التعريف. الفرق بين رمز صحيح ورمز خاطئ ليس فقط لأن الرمز الصحيح خالي من الأخطاء ، على الرغم من أن هذا مطلب مطلق. حتى الرمز الذي يؤدي وظائفه بالكامل قد يكون غير صحيح إذا كان غير مفهوم ، أو إذا كان هناك خيار لا يقل وضوحًا ، لكنه يعمل بشكل أسرع ، أو يتم تنفيذه بالسرعة ، ولكن مكتوب بشكل أكثر وضوحًا ، لذا فإن مفهوم الصواب نسبيًا إلى حد ما. نواصل نظرنا في مثال تنفيذ المخزن المؤقت لدينا ، والذي سيتيح لنا إظهار الفرق بين درجات مختلفة من الصحة.
قبل أن نصل إلى النقطة ، نقطة واحدة مهمة حول ما يلي. أعني أن المترجم الخاص بك يتم تشغيله دائمًا بمستوى otpimization غير صفري (-O2) ، لذلك لا يتعين علينا التفكير في إدخال تحسينات بسيطة مثل 1) تعديل البادئة مقابل postfix ، أو 2) باستخدام نتائج العملية السابقة ، أو 3) الفرق بين الزيادة والإضافة الوحدات وما إلى ذلك - نحن نفترض أن المترجم سوف يفعل الكثير بالنسبة لنا. بالطبع ، هذا ليس افتراضًا صارمًا ، لكننا سنضطر إلى الغطس في أحشاء المجمّع ، والتي في عصرنا ليست هي السائدة.
واسمحوا لي أن أذكرك أننا تلقينا تعليمات بتنفيذ فهرس (مؤشر) المخزن المؤقت الحلقي ، أي أننا نحتاج إلى إنشاء سلوك متغير
يعمل بالتسلسل خلال سلسلة من القيم ، من بعضها أولي إلى آخر نهائي . افترض على الفور أن القيمة الأولية ستكون صفرية ، وإلا فسوف يتعين علينا على الفور كتابة رمز صحيح إلى حد ما ، وهذا يتعارض مع الأهداف التعليمية ونحن لسنا في عجلة من أمرنا ، والقيمة النهائية هي Max.
يمكن تنفيذ هذا السلوك للمتغير باستخدام الإنشاء التالي:
volatile int Counter = 0; Counter = (++Counter) % (Max+1);
وهذا هو بالضبط الرمز الذي يمكننا رؤيته في العديد من الحالات (وهذا هو ، في كثير من الأحيان). ما الخطأ - حسنًا ، أولاً ، لبعض الوقت (من إجراء عملية الزيادة إلى تعيين النتيجة) ، سيكون المتغير لدينا أكبر من الحد الأقصى للقيمة المسموح بها ، وإذا حدث انقطاع في تلك اللحظة يجب أن يأخذ في الاعتبار قيمة هذا المتغير ، فأنا أتوقع شخصياً أنا لا أفترض النتائج. لذلك ، نعيد كتابة البرنامج:
int Counter=0; Counter = (Counter + 1) % (Max + 1);
لقد أزلنا خطأً واحداً ، والرمز (المشار إليه فيما يلي "الرمز" يعني "الرمز" يعني أن الشفرة القابلة للتنفيذ التي أنشأها المترجم) لم تعد طويلة ولم تعد تنفذ (في الواقع ، يتم تنفيذها بشكل أسرع ، ولكن فقط لأنه في الإصدار الأول يتم استخدام كلمة متقلبة زائدة عن الحاجة في هذه الحالة) ، ولم تصبح أقل وضوحًا (بدلاً من ذلك ، أكثر وضوحًا ، ولكن هذه مسألة ذوق).
ملاحظة ضرورية حول التقلب - هناك حاجة إلى هذا التوجيه إذا أردنا تجنب تحسين الكود الذي يؤدي إلى تنفيذ غير صحيح ، وفي هذه الحالة بالذات (عندما لا تتغير قيمة المتغير خارج نطاق الوحدة النمطية ولا توجد إدخالات متسلسلة فيه) (التوجيه) ) زائدة عن الحاجة تماما. أوصي بشدة أن تنظر إلى الشفرة التي تم إنشاؤها لكلا الخيارين على godbolt.org. لماذا لا يجب إساءة استخدام التوجيه المتقلب ، على عكس الكلمة الأساسية الثابتة ، التي يوصى باستخدامها كلما أمكن ذلك. حسنًا ، أولاً ، نحن نحظر التحسين ، أي أن الكود لن يصبح بالتأكيد أسرع (على الأرجح ، سيكون أكبر وأبطأ ، لكننا نفضل الصيغ الصارمة). وثانياً ، في هذه الحالة بالذات ، هذه الكلمة مضللة ، لأنه فيما يتعلق ببرنامجنا ، لا يمكن بأي حال تغيير قيمة العداد خارج نطاق سيطرتنا. في البرنامج الذي يقرأ قيمته - أي عند تنفيذ المخزن المؤقت الحلقي نفسه ، يمكنك اعتبار العداد قابلاً للتغيير خارج الوحدة النمطية ، وهناك مشكوك فيه ، وبالتالي فإن هذه السمة ببساطة لا تنطبق على العداد. إذا كان يجب تفسير متغير واحد بشكل مختلف في وحدات مختلفة ، فيجب دمج خدماتنا ، إذا كنا نتحدث عن تنظيم قسم مهم ، على سبيل المثال ، عند تنفيذ معاملة أو عمليات ذرية ، فإن هذا التوجيه لا يعطي أي شيء على الإطلاق.
نعود إلى الكود ونرى أن البرنامج لا يزال خاطئًا - ما هو الأمر - والحقيقة أنه لا يحتاج إلى ما نحتاج إليه (راجع وصف المهمة) ، ولكن هناك شيئًا آخر (يحسب باقي التقسيم) ، فقط النتائج تطابق حسنًا ، نعتقد ذلك (لا أعتقد ذلك ، لكن مؤلفي الكود بالتأكيد) ، أن النتائج تتزامن ، في الواقع ، في الحالة العامة ، لا تتزامن ، كنا محظوظين فقط بنطاق المتغير (القيم الإيجابية). علاوة على ذلك ، فإن عملية تنفيذ الكود أطول مما يمكن القيام به ، لأنه في أفضل الأحوال لدينا عملية تقسيم عدد صحيح (إذا كانت جزءًا من أوامر هيكلنا) ، ولا يتم تنفيذها بأي حال في دورة واحدة للمعالج (قيمة مميزة 10 دورات بالنسبة للعمارة ذات 8 بتات) ، وفي أسوأ الحالات ، سنرى استدعاء إجراء القسمة من المكتبة القياسية (وكذلك ، إذا كانت القسمة القصيرة) ، فسيكون وقت التنفيذ عشرات دورات الساعة.
فلماذا هذا النهج الخاطئ تماما لا يزال من الممكن الوفاء به في كثير من الأحيان. هنا يطالبونني من الجمهور أنه إذا كانت قيمة Max + 1 ، وهي قوة لاثنين ، فإن المترجم سيخمن بدلاً من عملية القسمة ، وضع عملية الضرب في اتجاه البت على القناع المقابل (يساوي Max) ، والذي سيتم تنفيذه بسرعة كبيرة وسيكون كل شيء على ما يرام.
أوافق على هذا البيان وأتبع هذا النهج ، إن لم يكن للظروف التالية:
- هذا ممكن فقط لماخ محددة بشكل ثابت في مرحلة التجميع ،
- يحدث هذا فقط عند تمكين التحسين ،
- هذا يحدث فقط عندما يلبي ماخ هذا الشرط ،
- هذا لا يحدث لجميع أنواع الكاردينال.
علاوة على ذلك ، فإنه في هذه الحالة بالذات (عندما يتم تعريف المتغير كعلامة) ، بالإضافة إلى أمر الضرب (منطقي) بواسطة القناع ، سيتم إنشاء أمر مقارنة بصفر وفرع للقيم السلبية ، وعلى الرغم من أن هذا الفرع لن يكون أبدًا لنطاقنا سيتم تنفيذه ، وسوف يستغرق مساحة في الذاكرة (وفي حالة وظيفة قابلة للاستبدال ، سوف يستغرق عدة مرات) وسيستغرق الأمر بعض الوقت لإجراء عملية المقارنة ، إذا لم تصدقها ، نذهب مرة أخرى إلى الموقع المحدد ونرى بنفسك. حجة أخرى لصالح الكرادلة غير الموقعة ، والتي كرست لها مؤخرا وظيفة كاملة.
لذلك ، إذا أردنا استخدام الضرب المنطقي مع قناع (تم الحصول عليه عن طريق تحسين حساب الباقي) ، فعلينا إعادة كتابة الوحدة النمطية وفقًا لذلك:
typedef uint8_t Counter_t; typedef int8_t sCounter_t; static inline Counter_t NextCounter(const Counter_t Counter) { #if IS_POWER2(Max + 1) return (Counter + 1) & Max #else return (Counter + 1) % (Max + 1); #endif };
في هذا الإصدار ، كل شيء واضح تمامًا ويمكن التحكم فيه وكل شيء صحيح (على الرغم من وجود عدد من أوجه القصور ، لكنها أصبحت الآن واضحة وغير ملثمين) ، وبالتالي فهي صحيحة ، على الرغم من أنها أكثر صحة وسوف نبحث عنها الآن. العيب الرئيسي ، في رأيي ، هو انتهاك لمبدأ KISS ، لأن استخدام العملية الباقية عن طريق الانقسام يهمل هذا المبدأ تمامًا. لذلك ، سنقوم الآن بتدمير جميع أوجه القصور بضربة واحدة (لا تقلق بشأن مصيرهم ، فسوف تولد من جديد 100،500 مرة ، لأنه لا يقرأ جميع مبرمجي Arduino مشاركاتي).
ولكن أولا ، انحراف طفيف إلى الجانب. كيف يمكننا تطبيق فحص لقدرة اثنين (يمكن تمثيل الرقم الثنائي باسم {0} 1 {0}) الذي استخدمناه للتو
لا تجسس#define IS_POWER2 (N) ((((((N) - 1) & (N)) == 0
وكيف يمكننا تطبيق التحقق من أن الرقم هو التسلسل الصحيح للوحدات {0} 1 {1} في التدوين الثنائي - خيار واحد واضح
#define IsRightSequence(N) IsPower2 ((N) + 1)
والثاني تافه
#define IsRightSequence(N) ( (((N) + 1) & (N)) == 0)
ملاحظة: لا يسعني إلا أن أتذكر النظرية الرائعة: "الرقم التجاوزي في الدرجة التجاوزي دائمًا ما يكون متجاوزًا ، إلا إذا كان العكس واضحًا أو تافهًا."
وكيف يمكننا التحقق من أن الرقم هو تسلسل للوحدات {0} 1 {1} {0}
#define IsSequence(N) IsPower2( (N) ^ ((N) << 1))
وأخيرًا - كيفية اختيار أقل جزء من الرقم (لا أعرف لماذا قد يكون ذلك ضروريًا ، لكنه سيكون مفيدًا)
#define LowerBit(N) ((((N) - 1) ^ (N)) & (N)).
لكنه توصل إلى ما يمكن أن يكون في متناول اليدين
#define IsRightSequence(N) (IsSequence(N) && (LowerBit(N) == 1))
ملاحظة غريبة - هذه وحدات الماكرو غير صحيحة تمامًا ، اتضح أن 0 هي قوة مكونة من اثنين وتسلسل صحيح (بالطبع ، تسلسل أيضًا) ، وهو غريب بعض الشيء. لكن 1 هي كل هذه الأشياء عن حق ، لذلك ، على ما يبدو الصفر ، فقط يجب النظر فيه بشكل منفصل. خاصية أخرى مثيرة للاهتمام من وحدات الماكرو هذه هي أننا لا نقدم أي افتراضات حول طول الوسيطة ، أي أنها تعمل بشكل صحيح مع أي نوع أساسي.
هناك كتاب رائع ، "Tricks for Programmers" ، حيث يمكنك العثور على وحدات الماكرو المذكورة والعديد من المهام الأخرى الممتعة والمتعة على قدم المساواة ، أوصي بشدة بقراءته ، خاصةً لأنه لا يوجد الكثير من الحروف فيه.
لكن العودة إلى فهرس المخزن المؤقت الحلقي. لقد قدمنا الحل الصحيح ، ولكن وعدنا بشكل صحيح ، مما يعني أن الحل الأخير له عيوب (من يشك في ذلك). أحدهما - يجب تحديد طول المخزن المؤقت بشكل ثابت في مرحلة التحويل البرمجي ، والثاني - في حالة طول غير ناجح ، فإن وقت التنفيذ طويل جدًا ولا يزال هناك عدد معين من الأخطاء في جزء صغير نسبيًا من البرنامج ، مما يجعلنا نتذكر مزحة حوالي 4 أخطاء في تهجئة كلمة "المزيد". سنقوم بإزالتها جميعًا (سيتم ترك بعضها في وقت لاحق) وفوراً ، وفي النهاية ، سنكتب الحل للمشكلة الأصلية كما هي:
static inline Counter_t NextCounter(const Counter_t Counter) { if ((Counter + 1) > Max) { return 0; } else { return Counter + 1; }; };
(كما فهمت بالفعل ، أنا من مؤيدي الأقواس المصرية وليس هناك ما يجب القيام به حيال ذلك).
دعنا ننتبه إلى حقيقة أننا ببساطة أعاد كتابة حالة المشكلة من لغة طبيعية في لغة البرمجة المختارة ، لذلك اتضح أنها واضحة ومفهومة للغاية. هل من الممكن تحسينه - لا شك ، ولكن فقط من وجهة نظر سرعة الكود ، لأنه ببساطة لا توجد أوجه قصور أخرى لهذا الحل (لا توجد أوجه قصور واضحة ، في الواقع أنها موجودة وسنقوم بإزالتها بنجاح).
لنقم بتقييم التعقيد الحسابي لهذا الحل - بالإضافة إلى الوحدة (1) والمقارنة (2) دائمًا ، ثم تعيين صفر (1) (نادرًا) أو إضافة (1) (دائمًا تقريبًا) - والذي يعطي 1 + 2 + 1 + Δ ~ 4 ابتدائي عمليات والذاكرة صفر. من المحتمل أن يقوم برنامج التحويل البرمجي الجيد في الوضع الصحيح بإجراء تحسينات معينة وتقليل وقت تنفيذ التعليمات البرمجية ، لكن من الأفضل أن نفعل ذلك بشكل صريح. هنا هو الخيار التالي:
static inline Counter_t NextCouner(const Counter_t Counter) { register sCounter_t Tmp; Tmp = (Counter + 1); if (Tmp > Max) { Tmp = 0; }; return Tmp; };
نقوم بتقييم التعقيد - الإضافة والمقارنة دائمًا ، مع تحديد الصفر (نادرًا) - حوالي 3 عمليات وعنصر ذاكرة واحد. في الواقع ، كان للإصدار السابق أيضًا عنصر ذاكرة واحد (ضمني) ، لذلك لدينا مكسب صافٍ في عملية أولية واحدة. وعلاوة على ذلك ، كان الإصدار السابق اثنين من العيوب - 1) انتهكت مبدأ DRY (تحسب الزيادة بمقدار واحد) و 2) كان أكثر من نقطة خروج واحدة ، وهو أمر غير جيد. نحن أيضًا لم نفهم الفهم ، أي أننا نجحنا في قتل مجموعة من الأرانب بقذيفة واحدة ، ولم ننفق أي خراطيش أيضًا - إنها مجرد قصة في أسلوب البارون مونشاوسين.
لاحظ أنني لم أستخدم بنية
if ( (Tmp = Counter + 1) > Max)
، على الرغم من أنه يحتوي على تعليمات صريحة إلى المحول البرمجي لمحاولة عدم إجراء عمليات نقل زائدة عن الحاجة. هذا هو النكهة في الشكل الأكثر وضوحا ، أنا فقط لا أحب القيمة التي أرجعها مشغل المهمة ومحاولة تجنب استخدامها. لا أستطيع أن أشرح سبب هذا الشعور القوي ، وفقًا لما قاله فرويد ، فمن المحتمل أن يكون هذا صدمة نفسية في الطفولة. المترجمون العصريون قادرون تمامًا على إجراء تحسين بسيط بمفردهم ، وإضافةً إلى ذلك ، أضفت أيضًا مؤهلًا للتسجيل ، بحيث يتطابق رمز الإصدار الخاص بي والصحيح (من وجهة نظر اللغة C). ومع ذلك ، لا أقيد مطلقًا حريتك في استخدام الطريقة التي تبدو مفضلة لك.
نستمر في التحسن ، لأنه لا يوجد حد للكمال ، ولم نصل إلى ذلك بعد. من أجل تحقيق ذلك ، نقوم بإعادة صياغة المشكلة الأصلية إلى حد ما ونترك فقط متطلبات المتغير في نطاق القيم ، دون الإشارة إلى اتجاه التغيير. يتيح لك هذا النهج إعادة كتابة البرنامج على النحو التالي
static inline Counter_t NextCouner(const Counter_t Counter) { register Counter_t Tmp; Tmp = (Counter - 1); if (Tmp < 0) { Tmp = ; }; return Tmp; };
للوهلة الأولى ، لم يتغير شيء كثيرًا ، لكن مع ذلك ، نحقق مكاسب في الوقت المناسب. بالطبع ، ليس بسبب حقيقة أن عملية التناقص من جانب واحد تعمل بشكل أسرع من عملية الزيادة به (على الرغم من أنني سمعت نسخة مماثلة) ، ولكن بسبب خصوصيات المقارنة. إذا عدت المقارنة في الإصدارات السابقة على أنها عمليتان أوليتان (نطرح أولاً ثم نتخذ قرارًا) ، في هذه الحالة ، يتم استخدام نتيجة العملية السابقة لاتخاذ قرار مباشرةً ، وتستغرق المقارنة عملية أولية واحدة ، مما يؤدي إلى عمليتين دائمًا وواجب واحد (نادراً) وحفظنا عملية واحدة (دون أن نفقد أي شيء) ، كما يقول المثل ، "تافه ، لكنه لطيف". هو الحل الناتج المثالي - للأسف ، لا. إنه أدنى من الحل باستخدام قناع (يتطلب عمليتين أوليتين بالضبط) من حيث السرعة وربما يكون هذا هو العيب الوحيد.
يوجد حل أسرع - ما عليك سوى زيادة (تقليل) قيمة العداد وعدم القيام بأي شيء آخر ، لكن لا يمكن تحقيق ذلك إلا في الحالة الوحيدة عندما تتزامن القيمة القصوى مع القيمة الأكثر تمثيلا في النوع المقبول. بالنسبة إلى عداد 8 بت (أي ، من النوع uint8_t) ، سيكون 255 ، ثم نكتب فقط Counter = Counter + 1 ونأخذ كلامي لأن كتابة Counter + = 1 أو ++ Counter اختيارية تمامًا ، على الرغم من أن العديد منها وسوف يكتبون وسوف يكون على حق تماما. إذا لم نفكر بجدية في إصدار الحاجة إلى حفظ الأحرف (حيث أن الخيار الأول هو الأطول) ، فإن هذا لا معنى له ، على الأقل إذا كنا نكتب برنامجًا لبنية ARM أو AVR (بالنسبة للآخرين الذين لم أحقق منهم ، أشك في أن النتيجة ستكون نفس الشيء) ضمن برنامج التحويل البرمجي لدول مجلس التعاون الخليجي (يدرك المؤلف أنهم يكتبون برنامجًا في محرر بيئة البرمجة المتكاملة ، هذه مجرد ثورة في الكلام من الماضي عندما كانت أجهزة الكمبيوتر كبيرة وصغيرة في الذاكرة) ، ومع تشغيل التحسين على أي مستوى ، لأن سوف رمز معين تكون متطابقة تماما.
المترجمون الحديثون متقدمون للغاية من حيث التحسين وينشئون كودًا جيدًا جدًا ، بالطبع ، إذا قمت بتمكين الوضع المقابل. على الرغم من أنني مستعد للموافقة على أن مثل هذه اللغة لا تؤذي ، ويمكن أن تكون مفيدة في ظل ظروف معينة ، فإن الشيء الوحيد الذي لاحظته هو أنه يجب تجنب تعبيرات Counter ++ (في هذه الحالة بالذات بالطبع) بشكل لا لبس فيه ، لأنه مخصص لمواقف مختلفة تمامًا ويمكن أن يؤدي إلى رمز أبطأ ، على الرغم من اختياري.
سؤال آخر هو أن المخزن المؤقت المكون من 256 عنصرًا غير مقبول دائمًا ، ولكن إذا كان لديك ذاكرة كافية ، فلماذا لا. من خلال هذا التطبيق ، إذا تمكنت من محاذاة المخزن المؤقت إلى حد الصفحة ، فيمكن جعل الوصول إلى العناصر سريعًا للغاية من خلال القضاء على عملية الانتقال من الفهرس إلى الفهرس (ستخبرك الكلمة الأساسية الموحدة بتنفيذ هذه الميزة ، فلن أحققها حتى لا أتعلم سيء) ، لكن هذا بالفعل قرار محدد للغاية مع ارتباط قوي بالعمارة ، وهو قريب بشكل خطير من الحيل في أسوأ معاني الكلمة ، وهذا ليس أسلوبنا.
بالطبع ، لا أحد يمنعنا من كتابة مجمّع يسمي هذا أو تلك الطريقة اعتمادًا على قيمة الحد الأقصى (والحد الأدنى ، نظرًا لأن العديد من الطرق لا تعمل ببساطة مع قيم الحد الأدنى غير الصفري) ، لقد اقترحت بالفعل المبادئ الأساسية لمثل هذا الحل ، لذلك سوف نقدم هذا كتمرين.
حسنًا ، في الختام ، لتلخيص - سنجمع بين تطبيقات مختلفة للعمل مع فهرس رنين وتقييم خصائصها.
يعرض السطر الثاني الموجود بين قوسين عدد قيم حجم المخزن المؤقت (لا يتجاوز 256) التي يتوفر بها هذا التطبيق ، لكننا نعني أن وجود مخزن مؤقت بحجم 0 لا يهمنا.
كما ترون من هذا الجدول ، DarZaNeBy (تعبيرتي المفضلة ، كما قد تلاحظ) ، والمزايا التي يتم شراؤها على حساب العيوب ، فإن الشيء الوحيد الذي يمكن ذكره بشكل لا لبس فيه هو أن الزيادة في عملية التحقق لديها منافس أكثر نجاحًا في شكل تناقص مع التحقق ولا تذهب إلى الجولة التالية تحت أي ظرف من الظروف.
ملاحظة ضرورية - هناك لغات البرمجة التي لن نضطر إلى التفكير في تنفيذ الفهرس على الإطلاق ، ولكن ببساطة يمكننا استخدام نوع الفاصل الزمني. لسوء الحظ ، لا يمكنني استدعاء تنفيذ هذه الإنشاءات في الكود الأمثل ، حيث إن هذه الإنشاءات (وهذه اللغات) ليست مخصصة للتحسين في وقت التشغيل ، ولكن من المؤسف.
لذلك ، جعلنا الوحدة النمطية الصحيحة (ما هو اسم قوي للدالة المضمنة) للعمل مع الفهرس ، والآن نحن مستعدون لبدء تنفيذ المخزن المؤقت الحلقي نفسه.
بالنسبة للمبتدئين ، يجب أن نقرر ما نريده بالضبط من كائن البرنامج هذا. من الضروري للغاية أن تكون قادرًا على وضع عنصر بيانات في مخزن مؤقت واستخراجه - طريقتان رئيسيتان ، نوع من الضرب والمضبط. من الممكن من الناحية النظرية تخيل وجود مخزن مؤقت بدون إحدى هذه الطرق ، أو حتى بدون كليهما (لا يمكن تخيل الكثير من الناحية النظرية البحتة) ، ولكن القيمة العملية لهذا التطبيق هي سؤال كبير. يمكن تنفيذ الوظيفة الضرورية التالية - التحقق من توفر المعلومات - إما كطريقة منفصلة أو كقيمة خاصة (أو سمة) يتم إرجاعها عن طريق القراءة. عادةً ما يفضلون الطريقة الأولى ، حيث يتضح أنها غير مفهومة وغير مكلفة للغاية.
لكن التحقق من اكتماله هو بالفعل سؤال كبير - هذه العملية ستتطلب وقتًا إضافيًا ، والذي سيُنفق دائمًا على التسجيل ، على الرغم من أن لا أحد يفرض علينا استخدامه - فليكن. لا نحتاج إلى أي شيء آخر من المخزن المؤقت ، فلنتذكر هذه العبارة للمستقبل.
العودة إلى التنفيذ. نحتاج إلى مكان لتخزين عناصر قائمة الانتظار وفهارس اثنين - واحد للكتابة إلى المخزن المؤقت والآخر للقراءة منه. كيف بالضبط سنحصل على هذا المكان (وهذه المؤشرات) هو موضوع لمناقشة منفصلة ، في الوقت الحالي ، دعونا نترك هذه اللحظة بين قوسين ونعتقد أن لدينا ببساطة. البعض (بما في ذلك مؤلفي كتاب "برمجة علماء الرياضيات" ، الذي أحترمه ، أوصي بقراءته) يستخدمون أيضًا عداد الأماكن المملوءة ، لكننا لن نفعل ذلك وسأحاول إظهار سبب هذا الشر.
أولاً ، حول المؤشرات - نلاحظ على الفور أن هذه مؤشرات ، وليست مؤشرات ، رغم أنني سمحت أحيانًا بنفسي أن أسميها.
لماذا تعد الفهارس (تخزين المعلومات حول عدد عنصر قائمة الانتظار) وليس المؤشرات (تخزين المعلومات حول الموقع في ذاكرة عنصر قائمة الانتظار) سؤالًا صعبًا للغاية ، فهناك حالات تكون فيها المؤشرات أكثر ربحية ، لكن من الواضح أن هذا ليس حالنا. ستكون قوائمنا قصيرة (حتى في 256 نتطلع بحذر) ، لذلك ستشغل الفهارس مساحة أقل ، وستكون العمليات الأولية أسرع بالنسبة إليهم ، ولن تكون هناك مشاكل في ذرية العمليات (في البنية العادية ، يجب ألا تكون هناك مؤشرات ، ولكن مع لن تحدث فهارس 8 بت ببساطة ، بالطبع ، إذا لم يكن لديك وحدة تحكم 4 بت) ، فإن التكاليف الإضافية المرتبطة بالتبديل من الفهرس إلى المؤشر لن تكون كبيرة جدًا (شريطة أن تكون عناصر قائمة الانتظار صغيرة).الملاحظات الهامشية, 51 ( ) 2 ( ) 3 ( ), , , . , , GCC x51, AVR .
علاوة على ذلك ، فإن العديد من الحيل التي تزيد من سرعة الحصول على القيمة التالية ستصبح غير متوفرة عند استخدام المؤشر. وإذا أخذنا في الاعتبار الرأي القائل بأن المؤشرات تكون أكثر صعوبة لفهمها (لا أعتقد أن هذا الرأي كان صحيحًا ، لكنه موجود) ، فإن الخيار يكون واضحًا - فهارس.لكن ما يجب أن تظهره المؤشرات بالضبط - هنا نطاق الخيال غير محدود ضمن حجم المخزن المؤقت لـ Max (وأكثر من ذلك) ، لكن مجموعة صغيرة جدًا من الخيارات لها معنى عملي. بالنسبة إلى فهرس التسجيل ، هناك احتمالان: 1) الإشارة إلى المكان الذي تم تسجيل العنصر الأخير فيه و 2) الإشارة إلى المكان الذي سيتم تسجيل العنصر التالي فيه. نظرًا لأنه بعد إنشاء قائمة الانتظار مباشرةً ، يبدو الخيار الأول غريبًا بعض الشيء ، ثم نختار الخيار الثاني ، خاصة وأن هذا يعدنا بمكاسب ملموسة في المستقبل. بالنسبة إلى فهرس القراءة ، نفترض على الفور أنه يشير إلى العنصر الذي سيتم قراءته في المرة التالية التي يتم قراءتها فيه. يوجد على الفور معيار بسيط (بمعنى التحقق) مفاده أن قائمة الانتظار ليست فارغة - المؤشرات غير متساوية. ولكن المشكلة الثانية تنشأ - إذا كنا نقف في طابور ماخ العناصر بالضبط ،ثم تتزامن المؤشرات ولن نتمكن من التمييز بين هذا الموقف وقائمة الانتظار الفارغة.تم استخدام الحل الأول لهذه المشكلة ("حل خاطئ واضح ومفهوم وبسيط") عدة مرات ، ويتألف من إعداد عداد لعدد العناصر الموضوعة في المخزن المؤقت ، أو في حالة التقدّم المتقدمة ، علم الاكتمال. لماذا لا أوافق على ذلك - هذا هو 1) مساحة ذاكرة إضافية ، 2) الوقت المستغرق في العمل معها (أنها صغيرة ، ولكن هناك) 3) حتى يتزامن الفهرس ، تكون قيمة العداد زائدة ، لأنها تتزامن مع اختلاف الفهرس ، 4) في حالة حجم المخزن المؤقت المكون من 256 عنصرًا ، يجب أن يكون العداد أطول من المؤشرات وقد لا يكون نوعًا أصليًا ، 5) يوجد عيب آخر (قاتل تقريبًا) ، ولكن المزيد عن ذلك لاحقًا. كما ذكر أعلاه ، من الممكن جزئياً تخفيف هذه العيوب من خلال تنظيم ليس عدادًا ، ولكن امتلاء العلم ، ولكن هناك حلًا أفضل بكثير.نحتاج فقط إلى تجنب الموقف الذي قد تتزامن فيه المؤشرات بعد السجل التالي (حقيقة أنها قد تتزامن بعد القراءة واضحة) ، ولهذا سنحتاج إلى قصر عدد العناصر المحتملة في المخزن المؤقت على 1 أقل من الممكن. هنا هو تنفيذها: #define NeedOverflowControl YES typedef uint8_t Data_t; static Data_t BufferData[Max]; static Counter_t BufferWriteCounter=0, BufferReadCounter=BufferWriteCounter; void BufferWrite(const data_t Data) { BufferData[BuffWriteCounter] = Data; register counter_t Tmp = NextCount(BufferWriteCounter); #if (NeedOverflowControl == YES) if (Tmp == BufferReadCounter) {BufferOverflow();} else #endif { BufferWriteCounter = Tmp; } };
هناك خطأ غير صحيح في الوظيفة السابقة ، أقترح أن أجدها وأصلحها بمفردي ، على الرغم من ... لا يزال هناك ، لكننا سنواصل: inline int BufferIsEmpty(void) { return ( BufferReadCounter == BufferWriteCounter ); }; inline int BufferIsFull(void) { return ( BufferReadCounter == NextCounter(BufferWriteCounter) ); }; #define DataSizeIsSmaller (sizeof(data_t) < sizeof(counter_t)) data_t BufferRead(void) { #if DataSizeIsSmaller register data_t Tmp = BufferData[BufferReadCounter]; #else register counter_t Tmp = BufferReadCounter; #endif BufferReadCounter = NextCount(BufferReadCounter); #if DataSizeIsSmaller return Tmp; #else return BufferData[Tmp]; #endif };
دعنا ننتبه إلى الموقف الذي أطلقنا عليه إجراء معالجة التدفق الزائد (إذا وضعنا علامة للحاجة إلى المعالجة) - عندما حاولنا كتابة آخر بايت غير مأهول من المخزن المؤقت ، لم نبلغ عن ذلك عن طريق نقل فهرس الكتابة ، لذلك لن نتمكن من قراءته - مثلي و حذر ، مع خيار التنفيذ المحدد ، يتم تقليل سعة المخزن المؤقت بمقدار واحد. لاحظ أيضًا أننا نضع العنصر التالي أولاً في قائمة الانتظار ، وعندها فقط نعلمه بتحريك المؤشر ، قد يؤدي الترتيب العكسي إلى عواقب غير سارة للغاية.قبل النظر في الكود مع العلم ، دعونا نتحدث قليلاً عن الفائض - يحدث ذلك عندما لا نستطيع وضع العنصر التالي في المخزن المؤقت ، ولدينا طرق مختلفة لحل المشكلة ، من بينها طرق جيدة وما إلى ذلك.أولاً وقبل كل شيء ، الطريقة (الصحيحة والجيدة) رقم1) لمنع حدوث موقف مماثل من حيث المبدأ عن طريق اختيار حجم المخزن المؤقت الصحيح (هناك خيار فرعي لهذه الطريقة - زيادة حجم المخزن المؤقت إذا لزم الأمر ، ولكن في عالم البرمجة المدمجة ، لم يتجذر ، ويبدو الأمر مشكوكًا فيه - الأوقات التي كان علينا فيها زيادة حجم المخزن المؤقت ، حيث يوجد ضمان بأنه لن يتعين علينا القيام بذلك مرارًا وتكرارًا).الطريقة التالية (الصحيحة ، ولكن الأسوأ ، رغم أنها لا تزال جيدة) رقم2) للإبلاغ عن حدوث تجاوز سعة بقيمة الإرجاع وإيقاف الكتابة مؤقتًا إلى المخزن المؤقت - ما يسمى بسجل الحظر ، لكن لا يتم تطبيقه دائمًا.وهنا طريقتان خاطئة وسيئة:3) و 4) تجاهل المشكلة التي نشأت وتظاهر أن كل شيء على ما يرام ("الابتسامة والموجة"). لماذا هي سيئة - لأننا ندعي فقط أن كل شيء على ما يرام ، في الواقع ، لا يمكن انتهاك مبدأ Dirichlet (مشكلة خلايا N وطيور N + 1) ونفقد عنصر البيانات ، ولماذا توجد طريقتان؟ يمكننا أننفقد 3) آخر عنصر من البيانات المسجلة ، ويمكننا4) أن نفقد أول العناصر التي لم يتم نقلها بعد.أي من هذه الطرق أسوأ - "كلاهما أسوأ" ، على الرغم من أن بعضها قد يكون أكثر قبولًا لمهمة محددة ، ولكن العيب الرئيسي غير قابل للإزالة - نحن مضطرون إلى فقدان المعلومات. لذلك ، يتم استخدام الطريقة الثالثة غالبًا ، نظرًا لأنه من الأسهل تنفيذها (لهذا يكفي ترك فهرس السجل بدون تغيير) ، وهو ما قمت به في المثال السابق إذا كانت معالجة تجاوز السعة فارغة.هناك طريقة أخرى - لا تتحكم في الموقف على الإطلاق (في مثالنا ، قم بتعليق السطر مع التعريف ، ولكن ليس مع الشيك الفعلي) ، بينما نحن5) سوف نفقد كامل المخزن المؤقت المملوء - يبدو للوهلة الأولى أنه الأسوأ ، حيث أن الخسائر هي الأكثر كبير ، في الواقع ، هذا ليس صحيحًا تمامًا ، لأن أي فقد للبيانات يعد شريرًا ، لكن له ميزة محددة - هذه الطريقة أسرع ، لأن التدفق الفائض لا يتحكم على الإطلاق.ملاحظة مهمة - لم يعثر البحث السريع على الإنترنت على خوارزمية لاستعادة البيانات في حالة فقد عنصر ، على عكس حالة تشويه العنصر ، حيث تعمل رموز الكتلة بشكل مثالي.من المثير للدهشة أن الخيار ذو العلامة الفائضة يفقد سرعته إلى حد ما إذا كان مكتوبًا بشكل صحيح ، ولكنه مع ذلك يفقد ، ومن الذاكرة ، نحن بالطبع نفوز بعنصر واحد ، لكننا بحاجة إلى تخصيص مساحة للعلم ، وبالتالي فإن المدخرات قيد السؤال . نحن ببساطة لن ندرس الخيار مع العداد ، لأنني قمت بالفعل بإدراج 4 من أوجه القصور فيه ، وقد حان الوقت لتذكر الخامس ، كما وعدت ، بالإضافة إلى القاتل. في التطبيق المقترح سابقًا ، تتمتع الفهارس بخصائص MRSW (كاتب متعدد القارئ الفردي) وفقًا لتصنيف "Art of Mulpiprocessor Programming" (أوصي بشدة بالقراءة ، العمل المدهش تمامًا) وفي حالة العمليات الذرية ، لا يتطلب تغيير الفهارس (للنوع الأصلي) لا توجد وسيلة التزامن.ملاحظة ضرورية وهامة للغاية - التزامن ليس مطلوبًا فقط من وجهة نظر تفاعل الكتابة والقراءة ، فكلتا الوظيفتين ليست قابلة لإعادة الاستخدام بأي حال من الأحوال وغير آمنة من وجهة النظر هذه ، وهو أمر مهم يجب تذكره.لكن العداد سيحتوي على خاصية MRMW ، وبدون التزامن ، لا يمكنك ببساطة التعامل معها ، من كلمة "بالكامل" (ما لم يكن هدفك بالطبع هو إنشاء برنامج "عربات التي تجرها الدواب فجأة"). إذا أخذنا في الاعتبار حقيقة أننا نكتب وحدة للعمل مع الأجهزة الطرفية ، وبالتالي يمكن استدعاء الكتابة أو القراءة من المقاطعة ، فإن مسألة التزامن ضرورية للغاية. ومن المثير للاهتمام ، أن العلم ، الذي يبدو أن له خصائص متشابهة ، يسمح مع ذلك بالعمل دون أدوات التزامن (مضحك ، أليس كذلك ، ولكن لديه تفسير علمي تمامًا - تصبح عملية التغيير ذرية ، ويسمح منطق العلامة ، وحتى يفرض ، بالتداخل السجلات) ، والذي يتضح من الجزء التالي من البرنامج.يرجى ملاحظة أن مثل هذا النهج (علامة بدون أدوات التزامن) لا يمكن تحقيقه إلا إذا تم استيفاء شروط معينة ، والتي يجب التحقق منها بعناية في قضيتك. يمكن العثور على التفاصيل في الأدبيات ، ولن أعطيها ، لأنني أعتقد أن هذه الطريقة لتنظيم المخزن المؤقت ليست جيدة جدًا ، وأحملها فقط لإظهار أن الحل الذي ليس هو الأكثر نجاحًا يمكن تنفيذه بدقة ، بالإضافة إلى إظهار مفهوم آخر أراه مفيدة جدا والتي أعتزم الالتزام بها. typedef uint8_t data_t; static data_t BufferData[Max]; static counter_t BufferWriteCounter=0, BufferReadCounter=WriteCounter; static int8_t BufferHaveData = 0; void BufferWrite(const data_t Data) { if ((BufferWriteCounter == BufferReadCounter) && (BufferHaveDataFlag == 1)) {BufferOverflow();} else { BufferData[BufferWriteCounter] = Data; BufferHaveDataFlag = 1; BufferWriteCounter = NextCounter(BufferWriteCounter); }; }; inline int BufferIsEmpty(void) { return ((BufferReadCounter==BufferWriteCounter) && (BufferHaveDataFlag == 0));}; data_t BufferRead(void) { register counter_t Tmp; Tmp = BufferReadCounter; BufferReadCounter = NextCount(BufferReadCounter); if (BufferReadCount == BufferWriteCounter) { BufferHaveDataFlag = 1; }; return BufferData[Tmp]; };
دعونا نلاحظ مرة أخرى أنه في إجراء الكتابة ، نضع العلامة أولاً ، ثم نقوم بتعديل الفهرس ، وفي إجراء القراءة ، نتحقق أولاً من الفهارس ، ثم نتحكم في العلم ، الذي يحفظنا مرة أخرى من المتاعب ويتداخل بطريقة أو بأخرى مع معالجة الموارد للقضاء على الحظر المتبادل .بشكل عام ، يجب إعادة كتابة هذه القطعة في النمط الصحيح (باستثناء الثوابت السحرية 0 و 1 ، إذا كنت تعتقد أن هذه ليست ثوابت سحرية ، فأنت مخطئ) ، وإذا كنت تستخدمها ، فقم بذلك ، أخفى المصححة رمز في المفسد ، ليس لأنني أشعر بالحرج ، ولكن حتى لا تحرض على الجولة القادمة من الحرب المقدسة (لا معنى لها ولا ترحم) ، كارهي عمليات النقل ، لا ينبغي عليك فتح هذا الزر ،الباقي - يمكنك ذلك typedef (NoBufferHaveData= 0, BufferHaveData =1) BufferHave DataFlag_t; BufferHaveData_t BufferYaveDataFlag; inline void BufferHaveDataFlagSet(void) {BufferHaveDataFlag = NoBufferHaveData;}; inline void BufferHaveDataFlagClr(void) {BufferHaveDataFlag = BufferHaveData;}; inline int BufferHaveDataFlagIsSet(void) {return (int)(BufferHaveDataFlag == BufferHaveData);};
ومن المثير للاهتمام ، أن رمز هذا النهج سيكون بالضبط هو نفسه بالنسبة للثوابت المباشرة 0 و 1 ، ولكن كل شيء شفاف للغاية وواضح ولا يترك مجالًا للتفسير. أوافق مقدمًا على أن المثال يبدو بعيد المنال وإذا كانت وظائف العمل هي فقط المخرجات مع العلم ، من الداخل يمكنك استخدام الثوابت 0 و 1. داخل نفسك. كل هذا صحيح ، الشيء الوحيد الذي أصر عليه هو سلوك العلم هذا ، يمكنك الاتصال بـ BufferFullFlag وتغيير منطق العمل به ، ولكن في أي حال من الأحوال يجب أن يسمى BufferIsNotEmptyFlag بما يلي انه يؤثر على العمليات المنطقية غامضة. وأؤكد مرة أخرى أن مبدأ KISS قد أثبت مرارًا وتكرارًا إخلاصه غير المشروط ، وإذا كنا مهتمين بما إذا كان المخزن المؤقت فارغًا ، فيجب أن نكتب ذلك مباشرةً في البرنامج وألا نطرح السؤال "هل هو غير مكتمل".على أي حال ، لا أعتقد أن التطبيق مع العلم جيد ، لذا فإنني أوصي بشدة بالتوافق مع المخزن المؤقت الذي لا يستخدم بشكل كامل وقبول التطبيق بمؤشرين وبدون حقول إضافية.في الواقع ، تم نشر منشور موسع بشكل غير متوقع لمثل هذا الموضوع البسيط ، فكرت في كتابة المزيد حول المزامنة والأقسام الهامة ، ولكن هذه هي المرة القادمة.ملحوظة: وفي الختام ، ما لم يعجبني بالضبط في المكتبة المذكورة ، ولكن في الوقت نفسه ، أخذ مؤلفو RTOS المحليون هذا في الكود الخاص بهم دون أدنى شك:- يتم تقديم تطبيقين للمخزن المؤقت - أحدهما بحجم قوة اثنين (آمل أن أكون هذا غير ضروري تمامًا) ، والثاني للحالات المتبقية ، لكن سيتعين عليك تحديد الإصدار مع الأقلام ، بالطبع ، لن يرتكبوا خطأ ، هناك عمليات تحقق في كل مكان.
- تم إجراء طرق غير ضرورية تمامًا ، مثل حذف العنصر الأخير أو الوصول المباشر من العنصر المؤقت.
- يتم محاذاة المخزن المؤقت للبيانات مع عدد صحيح.
- في تنفيذ الدرجة 2 ، خطأ في التحقق من معدل الإشغال.
- في تنفيذ حجم التعسفي ، عداد
- لا يتم تنظيم المقاطع الحرجة على الإطلاق ، والتي في التنفيذ الصحيح (مع اثنين من المؤشرات) ببساطة ليست هناك حاجة ، ولكن لا يمكن للمرء الاستغناء عنها ، واستخدام العمليات الذرية بدلا من ذلك هو واضح ليس كافيا.
- بعض الاهمال الاسلوب ، نوع من الخطوط
return ((_writeCount - Atomic::Fetch(&_readCount)) & (size_type)~(_mask)) != 0;
خاصةً النصف الثاني - هذا هو بالضبط ما يُلقى باللوم على C ، واللغة نفسها ليست مسؤولة ، فهي تسمح لك فقط بكتابة هذا بدلاً من فهمه
size_type(~(_mask))
ولكن ليس إجبارها على القيام بذلك.
PPS آمل أن يوافق مؤلف المكتبة على اعتبار هذا النقد بناءً وأن يقوم بالتصحيحات المناسبة.