مضاعفة أداء الكود أربع مرات باستخدام رمز if “عديم الفائدة”.
مضاعفة أداء التعليمات البرمجية أربع مرات مع “عديم الفائدة” في 12 يوليو 2026، لذلك كنت أقوم بتحسين ضاغط خاص بالمجال في اليوم الآخر، كما يفعل أحدهم. كانت إحدى المشكلات المهمة هي تقطيع سلسلة الإدخال واختيار التشفير الأكثر إحكاما لكل قطعة على النحو الأمثل (تضغط الترميزات المختلفة على أحرف مختلفة بشكل أفضل، لذلك لا يكون مكان التقسيم واضحًا على الفور). يصف المنشور السابق الخوارزمية إذا كنت مهتمًا، ولكنها تتلخص في العثور على أقصر مسار على الشبكة. لكل خلية، تحسب الخوارزمية أفضل خلية تليها. إن اتباع المراجع من الخلية الأولى إلى الخلية الأخيرة يعطي ترتيب الترميز الأمثل.uint8_t next_j(n_symbols)(8); __m128i best_path_length = _mm_setzero_epi16(); for (int i = n_symbols – 1; i >= 0; i–) { __m128i tmp = _mm_add_epi16(cost(i), best_path_length); __m128i minpos = _mm_minpos_epu16(tmp); __m128i cost_without_switching = _mm_sub_epi16(tmp, _mm_broadcastw_epi16(minpos)); __m128i cost_with_switching = _mm_set1_epi16(switch_cost); best_path_length = _mm_min_epu16(cost_without_switching, cost_with_switching); __m128i Choice = _mm_blendv_epi8( _mm_set1_epi16(_mm_extract_epi16(minpos, 1))), _mm_set_epi16(7, 6, 5, 4, 3, 2, 1, 0), _mm_cmpeq_epi16(best_path_length, cost_without_switching)); _mm_storeu_si64(&next_j(i), _mm_packs_epi16(الاختيار, الاختيار)); } uint8_t encoding(n_symbols); uint8_t j = 0; for (int i = 0; i < n_symbols; i++) { j = next_j(i)(j); الترميز(i) = j; } هذه الحلقة الطويلة ليست موضوع هذا المنشور، بل تم تحسينها جيدًا. نحن هنا للحديث عن الحلقة الثانية، والتي تبدو للوهلة الأولى أبسط بكثير. الكمون، باستثناء الكتابة، هو فقط j = next_j(i)(j)، الذي يتم تجميعه لتعليمة mov واحدة. كيف يمكن ألا يكون هذا هو الحل الأمثل؟ لو كنا نبرمج في عام 1984، لكان كذلك، لكن المعالجات الحديثة تتمتع بالتوازي على مستوى التعليمات - أي أنها تستطيع تنفيذ تعليمات متعددة بالتوازي. يعمل هذا حتى عبر تكرارات الحلقة، وهو أحد الأسباب التي تجعلنا عادةً لا ننتبه إلى تعليمات i < n_symbols وi++ عند تقييم أداء الحلقة - فهي لا تمنع عادةً وحدة المعالجة المركزية من القيام بالمزيد من العمل. لكن الأهم من ذلك هو أنه لا يمكنك تشغيل تعليمتين تابعتين في نفس الوقت. في حالتنا، لا يمكن أن يبدأ كل تكرار للحلقة قبل انتهاء التكرار السابق لأن j يتم تمريره عبر الحلقة، لذلك نحن مقيدون بزمن وصول الوصول إلى الذاكرة، وهو أمر ملحوظ جدًا حتى مع ذاكرة التخزين المؤقت. هل يمكن إصلاح هذا؟ في هذه الحالة بالذات، نعم! لا نتوقع عددًا كبيرًا جدًا من القطع، لذا من المرجح أن تكون next_j(i)(j) مساوية لـ j. إذا تمكنا من إخبار وحدة المعالجة المركزية بالتنبؤ ببقاء j سليمًا، فستصبح الحلقة مرتبطة بالإنتاجية بدلاً من زمن الوصول. على الرغم من أننا لا نملك تحكمًا مباشرًا في التنبؤ بالعنوان، يمكننا محاكاة ذلك باستخدام تنبؤ الفرع: for (int i = 0; i < n_symbols; i++) { if (j != next_j(i)(j)) { j = next_j(i)(j); } الترميز(i) = j; } إذا توقعت وحدة المعالجة المركزية أن نص if غير محتمل، فسوف تتجاهله وبالتالي لن ترى أي تبعية بين التكرارات المختلفة. عندما يتم تقييم الحالة في النهاية على أنها صحيحة، سيتم تفعيل حل التنبؤ الخاطئ للفرع، والتراجع عن عمليات الكتابة التخمينية الخاطئة، وإعادة التشغيل باستخدام j الصحيح. هذا بالضبط ما نريده! الأكاذيب على المترجمين. المشكلة الوحيدة هي أنه من وجهة نظر المترجم، فإن هذا عديم الفائدة تمامًا. إذا كان j في الذاكرة، فمن الممكن أن يتجنب الكتابة إلى ذاكرة القراءة فقط، ولكنه موجود في السجل. على عكس معظم الحالات الأخرى التي قد نصل فيها إلى تلميحات المترجم، نريد تحويل تعليمات برمجية بدون فروع إلى تعليمات برمجية متفرعة، وليس العكس - ولا يوجد مترجم يدعم ذلك، على الأقل بالنسبة للتعليمات البرمجية التي سيزيلها أي تمرير CSE دون تفكير ثانٍ! لا يدرك المترجم الغبي أن الأعداد الصحيحة لها مصدر أجهزة. الطريقة الوحيدة لتنفيذ ذلك التي أعرفها هي باستخدام قالب متقلب لجعله يبدو وكأن الشرط والمهمة مستقلان: for (int i = 0; i < n_symbols; i++) { if (j != next_j(i)(j)) { j = *(uint8_t volatile *)&next_j(i)(j); } الترميز(i) = j; } في اختبار قياس الأداء الاصطناعي، أدى هذا التغيير إلى تسريع الحلقة من 320 لنا إلى 80 لنا في بياناتي. (لا يبدو هذا كثيرًا، لكن الحلقة تعمل عدة مرات أثناء الضغط، لذا فهي تتراكم.) في تجربة أكثر واقعية، شهدت زيادة بمقدار 2x فقط، على الأرجح بسبب التشفير دون المستوى الأمثل بواسطة LLVM. ومع ذلك، لا يزال الأمر يستحق العناء! ومن المثير للاهتمام، في هذه الخوارزمية على وجه التحديد، أن كل next_j(i)(j) يمكن أن تكون واحدة فقط من قيمتين - إما j (في أغلب الأحيان)، أو بعض القيمة التي تعتمد فقط على i، ولكن ليس j. لذلك يمكنني استبدال كل مصفوفة مكونة من 8 عناصر next_j(i) بهذه القيمة المقترنة بقناع نقطي، مما يجعل القيمة ذات أهمية دلالية تلقائيًا وتزيل الحاجة إلى الخدع المتقلبة. ولكن من المحتمل أن يؤدي ذلك إلى إبطاء الكود، نظرًا لأن اختبار البت المتغير أبطأ من المقارنة (على الأقل على x86).
تم النشر: 2026-07-13 08:37:00
مصدر: purplesyringa.moe








