كيف تقيس وتقارن سرعة لغات البرمجة المختلفة - دليل عمليّ لمقارنة C# / C++ / Java / Go تحت الشروط نفسها

· · Benchmark, Performance, C#, C++, Java, Go

«يقال إنّ C++ سريعة» «Go خفيفة في التشغيل الفعليّ» «Java تصبح سريعة جدّاً عند التشغيل لمدّة طويلة» «C# أيضاً قويّة بشكل غير متوقّع بفضل JIT في .NET»

تتكرّر هذه الأحاديث كثيراً. لكن أكثر ما يجب تجنّبه هنا هو رصّ أرقام قاسها أشخاص مختلفون في بيئات مختلفة، ثمّ اعتبارها مباشرةً حكماً على أفضليّة لغة على أخرى.

تتأثّر C# و Java بسهولة بـ JIT و warm-up، بينما تكون C++ و Go عادةً مُجمَّعتين مسبقاً. كما يختلف وجود GC وخصائصه. وفروق تنفيذ المكتبة القياسيّة والمكتبات الجانبيّة تُحدث فرقاً ملموساً أيضاً. علاوة على ذلك، حتّى على الجهاز نفسه، يتزعزع الناتج بسهولة بسبب إعدادات الطاقة والحرارة والمعالجة في الخلفيّة وانحياز بيانات الإدخال. عالم موحل إلى حدّ كبير بصراحة.

في هذه المقالة، نُنظّم طرق القياس لمقارنة C# / C++ / Java / Go بأكبر قدر ممكن من الإنصاف. خلاصة القول مقدّماً: عدم محاولة الحكم بـ “أيّ لغة هي الأسرع” برقم واحد هو الأهمّ.

موضوع هذه المقالة في الحقيقة هو تنظيم منهج المقارنة. رصّ أرقام تعتمد على البيئة بشكل ظاهريّ يقترب من العرافة، لذلك لن نكتب هنا ترتيباً قياسيّاً فعليّاً. بدلاً من ذلك، سنركّز على كيفيّة التصميم لكي تكتسب المقارنة قيمة.

أوّلاً: الخلاصة

إن قلنا الخلاصة فقط مقدّماً، فإنّ ما يهمّ فعلاً في مقارنة سرعة C# / C++ / Java / Go هو السبعة التالية.

  1. حدّد أوّلاً ما الذي تريد مقارنة سرعته تختلف طريقة القياس بحسب ما إذا كنت تقيس زمن البدء أم throughput الحالة المستقرّة أم زمن p95 أم كفاءة الذاكرة.

  2. لا تستنتج النتيجة من bench واحدة فقط في الحساب على CPU وتخصيص الذاكرة والمعالجة المتوازية وزمن البدء، يتبدّل ظهور اللغة أو الـ runtime القويّة.

  3. في C# و Java افصل cold عن warm إن خلطت مقارنةً تشمل التشغيل الأوّل بمقارنة الحالة المستقرّة بعد warm-up، يلتوي الكلام.

  4. قِس بالخوارزميّة نفسها والإدخال نفسه والتحقّق من الصحّة نفسه كثيراً ما تكون الـ bench «ليست تنفيذاً أسرع، بل حلّاً لمسألة مختلفة».

  5. افصل microbenchmark داخل اللغة عن bench end-to-end العابرة للّغات حُزَم القياس المخصّصة لكلّ لغة مفيدة، لكنّ المقارنة العابرة للّغات يُفضَّل تشغيلها عبر runner مشترك من الخارج.

  6. انظر إلى الوسيط والتوزيع لا إلى المتوسّط فحسب تكفي مرّة واحدة يَنغرس فيها GC أو معالجة في الخلفيّة لكي ينهار المتوسّط.

  7. احتفظ بالشروط لا بالأرقام فقط نتائج الـ bench هي سجلّ للسرعة وسجلّ لشروط التجربة معاً. النتيجة من دون شروط مكتوبة تصبح مؤلمة لاحقاً.

ما يجب تحديده أوّلاً

لو حسمت «السريع» بكلمة واحدة، فأنت غالباً ستقع في حادث. أوّلاً نُحدّد ما الذي نسمّيه سريعاً.

فمثلاً، حتّى في البرنامج نفسه، يختلف ما تريد مشاهدته اختلافاً كبيراً.

1. هل تريد رؤية زمن البدء

في أدوات CLI أو دفعات قصيرة العمر أو أدوات مساعدة تبدأ مرّةً وتنتهي فوراً، يكون cold start أو process startup هو المؤثّر. على هذا المحور، تتغيّر النتيجة كثيراً بحسب ما إذا كانت تُحتَسب تكاليف التهيئة لـ JIT أو تحميل الفئات.

2. هل تريد رؤية throughput التشغيل الطويل

في الخوادم أو العمليّات المقيمة أو الـ workers أو معالجات التحويل التي تعمل طويلاً، فإنّ throughput steady-state هو المهمّ. في هذه الحالة، بطء التشغيل الأوّل بحدّ ذاته ليس جوهريّاً، وإنّما الموضوع هو إلى أيّ حدّ يمتدّ الأداء بثبات بعد warm-up.

3. هل تريد رؤية tail latency

في الـ API و UI والمعالجات القريبة من الزمن الحقيقيّ، قد يكون p95 / p99 أهمّ من المتوسّط. حتّى لو كان المتوسّط سريعاً، فإن توقّف أحياناً بشكل كبير، فإنّ تجربة المستخدم أو SLA يصبحان مؤلمَين.

4. هل تريد رؤية كفاءة الذاكرة أيضاً

ليس زمن CPU فقط، بل إن لم تنظر إلى الحدّ الأقصى لـ RSS، وكميّة التخصيص، وعدد مرّات GC، و GC pause، فستُسيء قراءة الثقل في التشغيل الفعليّ. «سريعة لكنّها تستهلك الذاكرة بشدّة» و«أبطأ قليلاً لكنّها خفيفة بثبات» يَنقَلِب تقييمهما بحسب الاستخدام.

باختصار، السؤال الذي ينبغي تحديده أوّلاً هو

ليس «أيّ لغة سريعة» في هذه المقارنة، بل أيّ workload وفي أيّ شروط وعلى أيّ مؤشّر تستطيع معالجته بسرعة

ذلك هو السؤال.

إن جمعت الأرقام مع ترك هذا الجزء غامضاً، فلن تستطيع تنظيمها في النهاية.

لماذا مقارنة اللغات صعبة

خلط JIT و AOT يصبح تجربة مختلفة

تتأثّر C# و Java عادةً بـ JIT. في المقابل، C++ و Go عادةً مُجمَّعتان مسبقاً.

أي إن قِست التشغيل الأوّل، فأنت تقيس مع سرعة جسم البرنامج، إقلاع الـ runtime وتحميل الفئات وتجهيز JIT أيضاً. وعلى العكس، إن نظرت فقط بعد warm-up كافٍ، فستصبح المقارنة مقارنةً لـ مدى فاعليّة تحسينات الحالة المستقرّة.

كلاهما له معنى. لكنّهما ليسا المعنى نفسه.

من الشائع أن تكون فروق التنفيذ أكبر من فروق اللغة

حتّى في «الفرز» نفسه،

  • جانب يستخدم المكتبة القياسيّة
  • جانب ينفّذها بنفسه
  • جانب يقوم بنسخ زائد
  • جانب يُولّد الإدخال من جديد في كلّ مرّة

هذا وحده يغيّر النتيجة كثيراً.

ومع المعالجات من نوع JSON أو الضغط أو التشفير أو التعابير المنتظمة، فإنّ فرق تنفيذ المكتبات يفوق تأثير اللغة بحدّ ذاتها. لذلك، إن لم تُصرّح بما تقيسه، فإنّ ما ظننتَه «مقارنة لغات» يصبح «مقارنة مكتبات».

في C++ فخّ أنّ التحسينات قد تُلغي المعالجة

خاصّةً في microbenchmark، إن قرّر المُجمِّع «نتيجة هذا الحساب لا يستخدمها أحد»، فقد يحذف المعالجة. عندها لا تكون سريعة، بل في الأصل لا تفعل شيئاً؛ قصّة رعب طريفة.

تَظهر هذه المشكلة بوضوح في C++، لذلك فإنّ استعمال النتيجة أو إخراج checksum، أو ميزة كبت التحسين في إطار benchmark، أمور بالغة الأهميّة.

وجود GC ليس «عيباً» ولا «ميزة» بل خاصيّة

في C# و Java و Go يوجد GC. وصف ذلك ببساطة بـ«GC موجود فيكون بطيئاً» تبسيط مفرط.

في الواقع، الأهمّ هو

  • كيفيّة التعامل مع كميّات كبيرة من الكائنات قصيرة العمر
  • إعداد حجم الـ heap
  • تردّد GC و pause
  • تخطيط الكائنات
  • عادات التخصيص في المكتبات

عكس ذلك، تستطيع C++ التحكّم بدقّة عبر الإدارة اليدويّة أو RAII، لكن بقدر ذلك تظهر فروق التصميم والتنفيذ بسهولة. أي إنّ اختلاف أسلوب الإدارة لا يساوي مباشرةً خيراً أو شرّاً أو أفضليّة.

ما يجب ألّا يُفعَل في المقارنة

1. خلط Debug و Release

هذا خارج النقاش. ينبغي توحيد أهداف المقارنة على بِنية مُحسَّنة بمستوى الإنتاج.

2. عدم حلّ المسألة نفسها

تنسيق الإدخال مختلف، الإخراج مختلف، معالجة الأخطاء غير موجودة في طرف، سياسة إعادة استخدام الذاكرة مختلفة. إن تركتَ هذا الأمر، فأنت تقيس فروق المتطلّبات لا السرعة.

3. الاستنتاج بعد تشغيل واحد فقط

التشغيل لمرّة واحدة فقط يكون عادةً ضوضاء.

  • JIT
  • ذاكرة الصفحات
  • زيادة تردّد CPU
  • الحرارة
  • المهامّ الخلفيّة
  • GC
  • قراءة الملفّ للمرّة الأولى

كلّ هذا يختلط في مرّة واحدة.

4. خلط warm-up

عند قياس C# و Java، إن أبهمتَ ما إذا كنتَ ستُضمِّن التشغيل الأوّل أم تنظر فقط بعد warm-up، فإنّ النقاش ينهار. يُتعامَل مع cold و warm كَشَيئَين منفصلَين.

5. عدم التحقّق من الصحّة

في الـ bench، يجب قبل «السرعة» التأكّد من «إعادة النتيجة نفسها». يجب التحقّق دائماً من أنّ جميع التنفيذات قيد المقارنة تُعطي نفس checksum أو نفس الإخراج من نفس الإدخال.

6. تحديد الرؤية الكاملة بـ microbenchmark واحدة

الفوز في tight loop وحده لا يعني الفوز في الخدمة الكاملة الفعليّة. وعلى العكس، حتّى لو خسرتَ في زمن البدء، قد تكون قويّاً بما يكفي في التشغيل الطويل.

السياسة الأساسيّة عند مقارنة C# / C++ / Java / Go

هذا الجزء مهمّ جدّاً. الأسلوب الموصى به هو بنية مكوّنة من طبقتين.

1. القياس داخل اللغة استخدِم الحُزمة المناسبة لتلك اللغة

لكلّ لغة أداة benchmark تستوعب ظروف تلك اللغة.

  • C#: BenchmarkDotNet
  • Java: JMH
  • Go: go test -bench و benchstat
  • C++: Google Benchmark

تعتني هذه الأدوات إلى حدٍّ ما بظروف الـ runtime لكلّ لغة، والمعالجة الإحصائيّة، وفخاخ القياس. وهي فعّالة جدّاً لـ المقارنة داخل اللغة ولـ التعمّق في التنفيذ.

2. للمقارنة العابرة للّغات ضع runner مشتركاً من الخارج

من ناحية أخرى، رصّ نتيجة BenchmarkDotNet لـ C# ونتيجة JMH لـ Java بشكل متجاور قد يكون خطيراً قليلاً. لأنّ أعراف الـ harness نفسها مختلفة.

لذلك، في المقارنة العابرة للّغات، يُوصى بجعل كلّ تنفيذ ملفّاً تنفيذيّاً يُستدعى بعقد CLI نفسه، وتشغيله من الخارج بالشروط نفسها.

مثلاً، تُجهَّز في كلّ لغة ملفّات تنفيذيّة بهذا الشكل.

bench --scenario sort_int32 --dataset data/sort_10m.bin --mode warm
bench --scenario group_words --dataset data/words_100mb.txt --mode cold
bench --scenario parallel_hash --dataset data/blob_1gb.bin --threads 8

ثمّ على جانب الـ runner المشترك،

  • عشوئة ترتيب التنفيذ
  • فصل cold / warm
  • تمرير مجموعة البيانات نفسها
  • التحقّق من checksum
  • جمع wall-clock والذاكرة
  • الاحتفاظ بالـ raw data في CSV / JSON

بهذا التدفّق.

عند فعل ذلك، يصبح من الأسهل التعامل مع أفضل الممارسات داخل كلّ لغة والإنصاف العابر للّغات بشكل منفصل.

مثال محدّد: ما هي بنود الـ bench التي ينبغي تجهيزها

عندما يُقال «أريد مقارنة C# / C++ / Java / Go»، إن كانت واحدة فقط، فيُوصى بـ CPU بسيطة يصعب إساءة فهمها، وإن كانت متعدّدة، فيُوصى بتجهيز 3 إلى 4 workloads ذات طبائع مختلفة.

البنية الموصى بها

1. sort_int32_10m

الغاية: رؤية CPU + عرض نطاق الذاكرة + كيفيّة استعمال المنطقة المؤقّتة

  • الإدخال: 10 ملايين قيمة int32 مولّدة بـ seed ثابت
  • المعالجة: فرز المصفوفة وإعادة checksum
  • ملاحظة: العودة في كلّ مرّة إلى نفس الإدخال غير المفروز

هذا واضح نسبيّاً. لكنّه يشمل فرق تنفيذ الفرز القياسيّ، فيكون مقارنةً مع تضمين المكتبة القياسيّة لا مقارنةً لـ اللغة بحدّ ذاتها.

2. hash_group_count

الغاية: رؤية ميل جداول التجزئة ومعالجة السلاسل والتخصيص و GC

  • الإدخال: بيانات نصّيّة ثابتة
  • المعالجة: عَدّ مرّات ظهور كلّ كلمة
  • الإخراج: أعلى N عنصراً و checksum

هذا قريب من الواقع، لكنّ فروق مكتبات السلاسل وتنفيذ map تُحدث فرقاً كبيراً أيضاً. بقدر ذلك تكون المقارنة قريبةً من الواقع.

3. parallel_sha256

الغاية: رؤية المعالجة المتوازية والمُجدوِل وworker pool وعادات المزامنة

  • الإدخال: سلسلة من قِطَع ثنائيّة بحجم ثابت
  • المعالجة: تجزئتها بالتسلسل عبر N من الـ threads وإعادة checksum النهائيّ
  • الشرط: تدريج عدد الـ threads إلى 1 / 2 / 4 / 8

أوضح من tight loop البسيط لرؤية كيفيّة الامتداد عند التنفيذ المتوازي.

4. startup_noop أو startup_parse_small

الغاية: رؤية زمن البدء

  • noop: يبدأ ثمّ ينتهي فوراً
  • parse_small: يعالج إدخالاً صغيراً مرّةً ثمّ ينتهي

هنا تظهر بسهولة تكاليف JIT والتهيئة في C# / Java، ويختلف الظهور كثيراً عمّا في C++ / Go. وعلى العكس، حتّى لو ظهر فرق هنا، فهو منفصل عن نتيجة المعالجة الطويلة.

ماذا عن bench JSON أو HTTP

JSON أو HTTP قريبة من الواقع، لذلك بالطبع لها معنى. لكن في تلك الحالة، تصبح مقارنةً مع تضمين المكتبات و الأُطر و النظام البيئيّ، لا مقارنة لغات.

ذلك بحدّ ذاته ليس سيّئاً. بل في العمل الفعليّ، قد تكون تلك الجهة أهمّ في حالات كثيرة. لكن في المقالات أو التقارير، يُفضَّل التصريح

هذه ليست مقارنة لغات، بل مقارنة تشمل التنفيذ القياسيّ والمكتبات الرئيسيّة

لتقليل سوء الفهم.

الشروط التي ينبغي توحيدها لكلّ لغة

C++

  • توحيد بِنية مُحسَّنة
  • تثبيت المُجمِّع
  • تثبيت تنفيذ المكتبة القياسيّة
  • التصريح بشروط مثل -O3 / /O2 و LTO و PGO
  • الانتباه إلى ألّا تختفي النتيجة بسبب التحسين
  • الاشتباه فيما إذا كانت تبدو سريعة بسبب سلوك غير معرَّف

C++ بقدر ما لها حريّة، تظهر فيها فروق الشروط بشكل كبير. لذلك بأيّ مُجمِّع وبأيّ flags وبأيّ STL قِسناه من الأمور المهمّة جدّاً.

C#

  • توحيد بِنية Release
  • تثبيت إصدار .NET
  • تسجيل شروط مثل Server GC / Workstation GC
  • التصريح بوجود Tiered Compilation و ReadyToRun و Native AOT
  • فصل cold عن warm

في C# تُغيّر فروق إعدادات .NET ظهور النتائج. خاصّةً C# مع JIT و C# مع Native AOT، رغم كونهما «C#» نفسها، يقعان على محورَين مختلفَين. خلطهما يجعل هدف المقارنة لا اللغة بل شكل التوزيع.

Java

  • تثبيت بائع وإصدار JDK
  • التصريح بـ GC
  • تثبيت warm-up / measurement / fork
  • تسجيل حجم الـ heap وخيارات JVM
  • فصل cold start عن steady-state

Java تتلقّى منافع JIT بسهولة، لكنّ ظهور التشغيل الأوّل يتغيّر كثيراً. لذلك فصل مقارنة العمليّات قصيرة العمر عن مقارنة التشغيل الطويل أمر ضروريّ.

Go

  • تثبيت إصدار Go
  • تثبيت GOMAXPROCS
  • التصريح بـ CGO_ENABLED
  • إن لمستَ GOGC فسجّله دائماً
  • إن أمكن، احتفظ بالإخراج بصيغة benchmark

Go أسهل نسبيّاً في التعامل، لكن في bench المتوازية تأثير GOMAXPROCS كبير. كذلك، يتبدّل العالم بحسب استخدام cgo، لذلك لا بدّ من تسجيل ذلك ضمن الشروط.

كيفيّة توحيد بيئة التنفيذ

في أيّ لغة، المقارنة من دون توحيد البيئة هي عادةً مقارنة بيئات.

ما ينبغي توحيده

  • نفس CPU / الذاكرة / التخزين
  • نفس إصدار OS
  • نفس شروط الطاقة
  • شروط قريبة من نفس درجة حرارة الغرفة
  • نفس بيانات الإدخال
  • نفس أولويّة العمليّة
  • نفس شروط عدد النوى
  • نفس شروط الحاوية أو bare metal

الأمور المؤثّرة بشكل خاصّ

إعدادات الطاقة وتردّد CPU

في حاسوب محمول، حتّى مجرّد كون الجهاز موصولاً بـ AC أو يعمل على البطاريّة يصنع عالماً مختلفاً. إن لم يكن CPU governor أو power mode موحّداً، فإنّ نتائج المقارنة تتزعزع كثيراً.

بشأن شروط الطاقة على Windows والإشعارات وضوضاء الخلفيّة والحرارة وكيفيّة توحيد ترتيب التنفيذ، فقد نظّمناها بالتفصيل في مقالة منفصلة: كيف تقارن إصدارات البرامج على Windows دون قياس الشيء الخطأ إن كنت تقيس على Windows، فهذا الجزء مؤثّر جدّاً.

الحرارة

إن كانت الأولى تكون سريعة فقط ثمّ تنخفض في النصف الثاني، فاشتبِه في الحرارة أو throttling. بدلاً من تشغيل A بالكامل ثمّ تشغيل B بالكامل، فإنّ تشغيلها بالتناوب على نمط A / B / A / B يقلّل الانحياز.

معالجة الخلفيّة

التحديثات والفهرسة والمزامنة وفحص الفيروسات والمتصفّح وأدوات الدردشة. هذه الأمور بسيطة لكنّها تتسلّل بشكل اعتياديّ.

ما الذي ينبغي قياسه

في مقارنة اللغات، يُوصى على الأقلّ بفصل الأربعة التالية.

1. wall-clock time

الزمن الحقيقيّ الذي ينتظره المستخدم. هذا أوّل مؤشّر يجب النظر إليه.

2. CPU time

«كم استخدم CPU فعليّاً». إن كان wall-clock فقط سريعاً ولم يتغيّر CPU time، فقد يكون التأثير من زمن الانتظار أو I/O.

3. memory / allocations

  • الحدّ الأقصى لـ RSS
  • إجماليّ التخصيص
  • عدد مرّات alloc
  • عدد مرّات GC
  • GC pause

النظر إلى هذه الأمور يُظهر التكلفة وراء السرعة.

4. التوزيع

  • الوسيط
  • p95 / p99
  • min / max
  • الانحراف المعياريّ والتشتّت

إن تكلّمتَ بالمتوسّط فقط، فلن ترى هويّة المعالجات التي تقفز أحياناً.

إجراءات التنفيذ الموصى بها

تنظيم تدفّق سهل التشغيل في الإنتاج، وإن كان التقريب، يصبح بهذا الترتيب.

1. تحديد الـ workload

أوّلاً، وضّح ما الذي تريد مقارنته.

  • زمن البدء
  • throughput الحالة المستقرّة
  • tail latency
  • كفاءة الذاكرة
  • التوسّع المتوازي

2. تثبيت مجموعة بيانات مشتركة

تُوحَّد بيانات الإدخال بـ seed ثابت أو ملفّ ثابت. إن ضمّنتَ توليد البيانات أيضاً، فعليك أن تجعل ذلك أيضاً بنفس الشروط في كلّ لغة.

3. تمرير التحقّق من الصحّة أوّلاً

تحقّق من أنّ جميع التنفيذات تُعيد النتيجة نفسها على بيانات صغيرة وكبيرة. إخراج checksum أو hash يجعل التعامل أسهل.

4. تثبيت شروط البناء

في كلّ لغة، أنشئ صيغة تنفيذيّة Release / مُحسَّنة، وسجّل الإصدار والـ flags.

5. فصل cold عن warm

خاصّةً في C# و Java هذا الجزء مهمّ.

  • cold: يشمل ما بعد بدء العمليّة مباشرةً
  • warm: الحالة المستقرّة بعد عدّة تشغيلات

من الأنظف عدم خلط هاتَين في الجدول نفسه.

6. تبادل ترتيب التنفيذ أو عشوئته

مثال:

cpp -> csharp -> java -> go
go -> java -> cpp -> csharp
csharp -> go -> java -> cpp
...

هذا يقلّل من انحياز الحرارة والضوضاء.

7. تأمين عدد المرّات

في microbenchmark خفيفة، يُفضَّل عدد كبير، وفي end-to-end على الأقلّ 10 مرّات. إن كان الفرق صغيراً والعدد قليلاً، فإنّ التفسير يصبح هشّاً جدّاً.

8. حفظ raw data

ليس النتائج المُجمَّعة فقط، بل احتفظ بـ بيانات كلّ run الخام. بالنظر لاحقاً، ستُقرَأ القيم الشاذّة وعادات warm-up.

9. عند ظهور فرق خذ profile

عندما يظهر فرق، عندئذٍ فقط نَتعمّق في السبب.

  • CPU profile
  • allocation profile
  • سجلّ GC
  • flame graph
  • آثار من جانب OS

عند الوصول إلى هنا، يصبح في الإمكان الحديث عن لماذا يحدث ذلك لا «سريع / بطيء».

كيفيّة قراءة النتائج

حتّى بعد ظهور الأرقام، إن أخطأتَ القراءة فالأمر يبقى خطيراً.

C# / Java بطيئة في التشغيل الأوّل فقط

اشتبه في تأثير JIT وتحميل الفئات والتهيئة. في هذه الحالة،

  • إن كان زمن البدء مهمّاً فهو فرق ذو معنى
  • إن كان الموضوع التشغيل الطويل فينبغي فصله في جدول مختلف

C++ قويّة في tight loop

قد يكون تحسين منخفض المستوى، وتخطيط الكائنات، والحدّ الأدنى من overhead الـ runtime، فعّالاً. لكنّ النظر إلى هذا فقط واستنتاج «إذاً هي الأسرع في الخدمة الفعليّة أيضاً» قفزة.

Go تبدو متفوّقة في زمن البدء وسهولة التوزيع

الـ binary المفرد، والإقلاع الخفيف نسبيّاً، ونموذج التوازي السهل، قد تكون فعّالة. لكنّها ليست متفوّقة بالضرورة في جميع workloads على CPU.

C# / Java تلحقان في steady-state بشكل ملحوظ، أو حتّى تقلبان النتيجة

قد تكون تحسينات JIT فعّالة. هذا أيضاً ليس أمراً نادراً. لذلك من المهمّ عدم خلط المقارنة شاملةً البدء ومقارنة الحالة المستقرّة.

الفروق كبيرة في المعالجات الكثيفة التخصيص

في هذه الحالة، الأكثر تأثيراً عادةً ليس اسم اللغة بل

  • تخطيط الذاكرة
  • معالجة السلاسل و map
  • سلوك GC
  • النسخ الزائد

قالب التسجيل

إن احتفظتَ على الأقلّ بالبنود التالية في نتائج الـ bench، فستوفّر على نفسك لاحقاً.

timestamp,language,scenario,run_kind,cold_or_warm,elapsed_ms,cpu_ms,max_rss_mb,alloc_bytes,gc_count,checksum
compiler_or_runtime,compiler_version,flags,os,cpu,threads,input_id,notes

مثلاً، يمكن تقسيم run_kind بهذا الشكل.

  • micro
  • macro
  • startup
  • parallel

أمّا cold_or_warm فعلى الأقلّ يُفضَّل التصريح بأحد الآتيَين.

  • cold
  • warm

في الـ bench، القدرة على التفسير لاحقاً أهمّ أحياناً من القياس نفسه.

الخلاصة

ما يهمّ فعلاً في مقارنة سرعة C# / C++ / Java / Go هو إنزال السؤال الفظّ «أيّ لغة هي الأسرع» إلى صيغة تجربة من نوع «أيّ workload وفي أيّ شروط وعلى أيّ مؤشّر نقارن».

النقاط التي يصعب الإخفاق فيها بشكل خاصّ، إن لخّصناها مرّةً أخرى، هي كما يلي.

  • فصل زمن البدء عن الحالة المستقرّة
  • القياس بنفس الخوارزميّة ونفس الإدخال ونفس التحقّق من الصحّة
  • عدم استنتاج النتيجة من bench واحدة فقط
  • فصل benchmark داخل اللغة عن benchmark العابرة للّغات
  • النظر إلى الوسيط والتوزيع لا إلى المتوسّط
  • الاحتفاظ بالشروط و raw data

والأهمّ في النهاية هو عدم المبالغة في تحديد الفائز والخاسر باسم اللغة. الأداء الواقعيّ يتحدّد بتركيب لغة وruntime ومكتبة وشروط بناء وبيانات و OS وعتاد.

«C++ سريعة»، «Java قويّة»، «Go خفيفة»، «حتّى C# سريعة بما يكفي» - كلّها صحيحة بمعنى ما. لكن إن سقط في أيّ شروط نقول ذلك، فإنّ الأمر يصبح غالباً معركة في الضباب.

وَحِّد الشروط، وعلى workloads متعدّدة، وافصل cold / warm، وانظر حتّى التوزيع. بسيط لكنّه في النهاية الأقوى.

مراجع

  • BenchmarkDotNet Getting Started https://benchmarkdotnet.org/articles/guides/getting-started.html

  • OpenJDK JMH Project https://openjdk.org/projects/code-tools/jmh/

  • JMH GitHub Repository / README https://github.com/openjdk/jmh

  • Go testing package https://pkg.go.dev/testing

  • Go benchstat https://pkg.go.dev/golang.org/x/perf/cmd/benchstat

  • Google Benchmark User Guide https://google.github.io/benchmark/user_guide.html

  • كيف تقارن إصدارات البرامج على Windows دون قياس الشيء الخطأ https://comcomponent.com/ar/blog/2026/03/16/002-windows-benchmark-comparing-program-versions/

مواضيع ذات صلة

صفحات يسهل فهمها بالاطّلاع عليها مع هذه المقالة.

جهة الاستشارة لهذا الموضوع

تصميم مقارنة الأداء، وكيفيّة توحيد ظروف القياس، وتفسير النتائج، والتعمّق في الأسباب، مواضيع تلائم الخدمات التالية.

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

روابط عامة

العودة إلى المدونة