أخطاء الأداء الشائعة
أخطاء الأداء الشائعة
معظم مشكلات الأداء في بيئات الإنتاج ليست غريبة أو معقّدة — بل هي حفنة من الأنماط المضادة المعروفة تتكرر على نطاق واسع. تستعرض هذه الدرس ثلاثًا منها تظهر باستمرار في مراجعات الكود وجلسات التحليل: تكلفة الـ autoboxing، وتسلسل النصوص داخل الحلقات، وسوء تحديد حجم المجموعات. إن فهمك للـ لماذا وراء كل منها يُمكّنك من اتخاذ قرارات مدروسة مبنية على أدلة بدلًا من التخمين.
١. الـ Autoboxing والـ Unboxing
يفصل نظام أنواع Java بين الأنواع الأولية (int, long, double, …) ونظيراتها من الأغلفة (Integer, Long, Double, …). الـ autoboxing هو التحويل التلقائي الذي يجريه المُترجم بين النوعين. ورغم أنه يُتيح واجهات برمجية مريحة، فإنه يُخفي تكاليف حقيقية: تخصيص ذاكرة على الكومة (heap)، وضغط على GC، وإهدار ذاكرة التخزين المؤقت للمعالج.
يُحوّل المُترجم total += i تقريبًا إلى:
يُخصَّص عشرة ملايين كائن Long ويُهمَل كل منها فورًا، مما يُجهد GC ويُضيّع ذاكرة التخزين المؤقت للمعالج. الحل بسيط للغاية:
النسخة الأولية أسرع بنحو 5–10 مرات على JVM حديثة ولا تُخصّص شيئًا. القاعدة هي: استخدم الأنواع الأولية في المسارات الحرجة الثقيلة حسابيًا؛ واحتفظ بالأغلفة حين تفرضها الواجهة (الجينيريك، الحقول القابلة للـ null، المجموعات).
Map<String, Integer> تخزّن أعدادًا صحيحة مُغلَّفة. كل map.get(key) يُعيد Integer؛ واستخدامه في حسابات يُجري unboxing فورًا. إن كنت تُكرّر خريطة وتجمع قيمها، فكّر في مصفوفات int[] أو خرائط الأنواع الأولية من Eclipse Collections أو Koloboke في المسارات الساخنة.
فخ ثانٍ للـ autoboxing هو مقارنة المساواة:
تُخزّن JVM مثيلات Integer للقيم من −128 إلى 127 (قابلة للضبط عبر -XX:AutoBoxCacheMax)، لذا تعيد == true للقيم الصغيرة صدفةً — وهذه ثغرة تظهر فقط في الإنتاج مع الأعداد الكبيرة. استخدم دائمًا equals() لمقارنة الأغلفة.
٢. تسلسل النصوص داخل الحلقات
String غير قابل للتعديل. كل + أو += على String يُنشئ كائن String جديدًا وينسخ جميع الأحرف الموجودة إليه. داخل حلقة، ينتج عن ذلك نسخ أحرف بتعقيد O(n²) — وهو الخوارزمية الكلاسيكية لـ "Schlemiel the Painter".
مع 10,000 سطر، التكرار رقم 5,000 ينسخ ~5,000 حرف للمحتوى الموجود فقط، ثم ينسخ السطر الجديد. العمل الإجمالي يتناسب مع n². لا تُحوّل JVM والـ JIT هذا تلقائيًا إلى StringBuilder داخل جسم الحلقة.
"Hello " + name + "!" خارج الحلقة يُعيد المُترجم كتابته إلى تسلسل StringBuilder واحد (Java 9+ تستخدم invokedynamic + StringConcatFactory وهو أسرع من ذلك). المشكلة حصرًا عندما يقع التسلسل داخل حلقة، لأن المُترجم لا يعرف عدد التكرارات وقت الترجمة.
لبناء نصوص منظّمة على نطاق واسع، فضّل String.join() أو StringJoiner أو Collectors.joining() من Streams — وكلها تستخدم StringBuilder داخليًا:
٣. سوء تحديد حجم المجموعات
مجموعات Java القابلة للتوسع — ArrayList وHashMap وHashSet وStringBuilder — تبدأ صغيرة وتتضاعف حين تمتلئ. التضاعف يعني: تخصيص مصفوفة جديدة بضعف الحجم تقريبًا، ونسخ كل العناصر الموجودة إليها، ثم التخلص من المصفوفة القديمة. إن أضفت مليون عنصر إلى ArrayList بالحجم الافتراضي، فستُشغّل ~20 دورة توسّع ونسخ وتنتج ~20 مصفوفة مهملة على GC.
بالنسبة لـ HashMap وHashSet يختلف الحساب قليلًا لأن عامل التحميل الافتراضي هو 0.75 — أي تتوسّع الخريطة حين تبلغ 75% من امتلائها. لتجنب أي توسّع حين تعرف الحجم النهائي، مرّر capacity = expectedSize / 0.75 + 1:
تُغلّف Maps.newHashMapWithExpectedSize(n) من Guava تلك المعادلة فلا تحتاج إلى حسابها:
new ArrayList<>(10_000_000) حين تنتهي بتخزين 100 عنصر يُهدر 10 ملايين خانة على الكومة. حجّم مسبقًا حين تملك تقديرًا جيدًا؛ وإلا دع المجموعة تنمو طبيعيًا. التوازن المثالي هو تقدير حدّ أعلى معقول، لا الحد الأقصى المطلق.
القياس لا التخمين
كل خطأ من الأخطاء أعلاه غير مرئي بدون قياس. قبل أي تحسين:
- حلّل أولًا (JFR / async-profiler / VisualVM) للتأكد من المسار الساخن.
- قِس بـ JMH للحصول على أرقام قبل/بعد قابلة للإعادة.
- طبّق تغييرًا واحدًا في كل مرة ثم أعد القياس.
التحسين الأعمى كثيرًا ما ينقل العمل من مسار بارد إلى مسار ساخن. الأخطاء الثلاثة في هذا الدرس آمنة للإصلاح المسبق فقط في الحلقات الضيّقة أو المسارات الحرجة التي تفهم تكلفتها جيدًا.
الخلاصة
- Autoboxing: استخدم الأنواع الأولية في الحلقات الساخنة؛ كن حذرًا من أنواع الأغلفة في الحسابات؛ استخدم
equals()لمقارنة الأغلفة. - تسلسل النصوص: لا تستخدم
+=في حلقة أبدًا؛ استخدمStringBuilderأوStringJoinerأوCollectors.joining(). - تحجيم المجموعات: حجّم مسبقًا حين تعرف الحجم التقريبي النهائي؛ تذكّر عامل التحميل 0.75 للخرائط؛ تجنّب الإفراط في التحجيم.