لماذا طباعة "B" أبطأ من "#" في Java؟ تحليل مع أمثلة

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

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

ابدأ الدورة

لماذا يتباطأ "B" بينما "#' يطير؟ حل اللغز الشهير في Java

\n\n

إذا سبق لك كتابة حلقات بسيطة في Java لإخراج أحرف إلى وحدة التحكم، فربما لاحظت أمرًا غريبًا: إخراج الحرف الصغير 'B' قد يكون أبطأ بشكل ملحوظ من إخراج علامة الشباك '#'. هذا التأثير أحدث ضجة ذات مرة على Stack Overflow — حيث حصد السؤال آلاف المشاهدات والأصوات. اليوم سنفهم ما وراء هذه الظاهرة، وكيف ترتبط بالآليات الداخلية لـ Java، الإدخال/الإخراج لوحدة التحكم، والتخزين المؤقت.

\n\n

إعداد التجربة

\n

تخيل أننا نشغل حلقتين تطبعان 100,000 حرف في وحدة التحكم: حلقة تطبع '#'، والأخرى تطبع 'B'. للوهلة الأولى، العملية متطابقة. ومع ذلك، في العديد من الأنظمة (خاصة Windows)، تنتهي الحلقة الأولى في حوالي 100 مللي ثانية، بينما تستغرق الثانية عدة ثوانٍ. لماذا؟

\n

دعنا نعيد إنتاج هذا في Java.

\n\n

كود الاختبار

\n
public class SlowPrintTest {\n    public static void main(String[] args) {\n        long start = System.nanoTime();\n        for (int i = 0; i < 100_000; i++) {\n            System.out.print("#");\n        }\n        long end = System.nanoTime();\n        System.out.println("\\nالوقت لـ '#': " + (end - start) / 1_000_000 + " ms");\n\n        start = System.nanoTime();\n        for (int i = 0; i < 100_000; i++) {\n            System.out.print("B");\n        }\n        end = System.nanoTime();\n        System.out.println("\\nالوقت لـ 'B': " + (end - start) / 1_000_000 + " ms");\n    }\n}
\n\n

شغّل هذا الكود في الطرفية، وليس في بيئة التطوير المتكاملة (IDE) (مثلاً عبر java SlowPrintTest). ستدهشك النتيجة. في Windows مع CMD، قد يكون الفارق من 10 إلى 50 ضعفًا.

\n\n

لماذا يحدث هذا؟ جذر المشكلة في عرض الخطوط

\n

الأمر لا يتعلق بـ System.out.print() نفسه ولا بـ JVM. السبب الرئيسي هو كيفية معالجة نظام التشغيل والطرفية لإخراج الأحرف. System.out في Java هو غلاف لتيار الإخراج القياسي (stdout). يتم نقل البيانات إلى وحدة التحكم، لكن سرعة الإخراج على الشاشة تعتمد على:

\n
    \n
  • عرض الحرف — العديد من وحدات التحكم القديمة (مثل CMD في Windows) تستخدم خطوطًا أحادية العرض، لكن الحرف 'B' قد يتطلب عرضًا أكثر تعقيدًا من '#'.
  • \n
  • تحديث الشاشة — قد تقوم وحدة التحكم بإعادة رسم السطر بالكامل عند إخراج كل حرف، بدلاً من مجرد إضافته.
  • \n
  • التخزين المؤقت للطرفية — بعض الأطراف تخزن الإخراج في كتل، لكن إذا كان الحرف يتطلب حساب عرض الصورة الرمزية (glyph)، فإن ذلك يبطئ العملية.
  • \n
\n\n

يظهر التأثير بشكل أوضح في موجه أوامر Windows (CMD) مع الخطوط النقطية. عندما تطبع 'B'، تحاول وحدة التحكم عرض الحرف في الوضع الرسومي، بينما بالنسبة لـ '#' تستخدم أسلوبًا مبسطًا. في الأطراف الحديثة (Windows Terminal, iTerm2, Alacritty)، قد يكون الفارق أقل وضوحًا أو غير موجود.

\n\n

دور التخزين المؤقت في Java

\n

جانب مهم من تحسين الإخراج هو استخدام التدفقات المخزنة مؤقتًا. System.out غير مخزن مؤقتًا افتراضيًا (أو بالأحرى، مخزن مؤقتًا على مستوى JVM ولكن بحجم صغير). كل استدعاء لـ print() قد يؤدي إلى استدعاء نظام write()، وهو مكلف جدًا. إذا أضفنا تخزينًا مؤقتًا عبر BufferedWriter، سيزداد الأداء، لكن الفارق بين الأحرف سيبقى — فهو من جانب الطرفية.

\n\n
// مثال مع التخزين المؤقت\nBufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));\nfor (int i = 0; i < 100_000; i++) {\n    bw.write("#");\n}\nbw.flush();
\n\n

حتى مع التخزين المؤقت، إذا كانت الطرفية تعرض 'B' ببطء، سيزداد الوقت الإجمالي. أح

المدونات

توصيات الكتب