لماذا تتم معالجة المصفوفة المرتبة بشكل أسرع؟ شرح مع Java

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: المفارقة التي فجرت Stack Overflow

\n\n

تخيل: أنت تكتب حلقة بسيطة بلغة Java تقوم بجمع عناصر مصفوفة بناءً على شرط (مثلاً، إذا كان الرقم أكبر من 128). تقوم بتشغيل الكود على مصفوفة مرتبة - ويتم تنفيذه أسرع بمقدار 2-3 مرات من المصفوفة غير المرتبة. هل يبدو هذا كالسحر؟ في الحقيقة، هذا ليس سحراً، بل branch prediction (توقع التفرعات) - إحدى الميزات الأساسية للمعالجات الحديثة.

\n\n

السؤال «لماذا تكون المعالجة الشرطية لمصفوفة مرتبة أسرع من مصفوفة غير مرتبة؟» حصل على أكثر من 30 ألف صوت على Stack Overflow وأصبح كلاسيكياً لدراسة التحسين منخفض المستوى. في هذه المقالة، لن نحلل النظرية فقط، بل سنكتب كوداً حقيقياً بلغة Java لنرى التأثير بأعيننا. والأهم من ذلك، ستتعلم كيفية استخدام هذه المعرفة عملياً.

\n\n

إذا كنت تريد فهم أعمق لكيفية عمل تحسين الكود بلغة Java ولماذا يمكن لـ branch prediction أن يسرع أو يبطئ برنامجك - فأنت في المكان الصحيح.

\n\n

1. التجربة: الكود الذي يوضح كل شيء

\n\n

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

\n\n
import java.util.Arrays;\nimport java.util.Random;\n\npublic class BranchPredictionDemo {\n    private static final int SIZE = 100_000;\n    private static final int THRESHOLD = 128;\n\n    public static void main(String[] args) {\n        // إنشاء مصفوفة غير مرتبة\n        int[] unsorted = new int[SIZE];\n        Random rand = new Random();\n        for (int i = 0; i < SIZE; i++) {\n            unsorted[i] = rand.nextInt(256); // 0..255\n        }\n\n        // إنشاء مصفوفة مرتبة (نسخ وترتيب)\n        int[] sorted = unsorted.clone();\n        Arrays.sort(sorted);\n\n        // قياس الوقت للمصفوفة غير المرتبة\n        long start = System.nanoTime();\n        long sumUnsorted = processArray(unsorted);\n        long end = System.nanoTime();\n        System.out.println("غير مرتبة: " + (end - start) / 1_000_000 + " ms, المجموع = " + sumUnsorted);\n\n        // قياس الوقت للمصفوفة المرتبة\n        start = System.nanoTime();\n        long sumSorted = processArray(sorted);\n        end = System.nanoTime();\n        System.out.println("مرتبة: " + (end - start) / 1_000_000 + " ms, المجموع = " + sumSorted);\n    }\n\n    private static long processArray(int[] arr) {\n        long sum = 0;\n        for (int i = 0; i < arr.length; i++) {\n            if (arr[i] >= THRESHOLD) {\n                sum += arr[i];\n            }\n        }\n        return sum;\n    }\n}\n
\n\n

النتائج على جهاز الكمبيوتر المحمول الخاص بي (Intel i7, Java 17):

\n
    \n
  • المصفوفة غير المرتبة: ~22 ms
  • \n
  • المصفوفة المرتبة: ~8 ms
  • \n
\n\n

الفرق يقارب 3 مرات! لماذا؟ على الرغم من أن عدد العمليات متساوٍ. الإجابة تكمن في كيفية معالجة المعالج للشرط if (arr[i] >= THRESHOLD).

\n\n

2. Branch Prediction: عقل المعالج

\n\n

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

\n\n

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

\n\n

في المصفوفة غير المرتبة، تقفز القيم بشكل عشوائي أعلى وأسفل الحد. يخطئ المعالج في حوالي 50% من الحالات، و

المدونات

توصيات الكتب