سجل التعديلات (4 تحديثات، آخر تحديث 3 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22243173)
- أُصلِحت روابط الخلاصة وCTA الخدمة ورسالة رفض قاعدة البيانات الأحدث. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240857)
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621406)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
小村 豪 (2026). كيف تقارن سرعة C# وC++ وJava وGo بعدل. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621406 https://comcomponent.com/ar/blog/2026/03/17/000-language-benchmark-csharp-cpp-java-go/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621406
- DOI (هذه النسخة)
- 10.5281/zenodo.22279826
«يقال إن C++ سريعة» «Go خفيفة في التشغيل الفعلي» «Java تصير سريعة جداً عند التشغيل الطويل» «C# أيضاً قوية على نحو غير متوقّع بفضل JIT في .NET»
تتكرّر أحاديث كهذه كثيراً. غير أن أسوأ ما يُفعل هنا هو صف أرقام قاسها أشخاص مختلفون في بيئات مختلفة، ثم الحكم منها مباشرة على أفضليّة لغة.
تتأثّر C# وJava بسهولة بـ JIT والإحماء، بينما C++ وGo مجمَّعتان مسبقاً في العادة. وجود GC وخصائصه مختلف أيضاً. وفروق تنفيذ المكتبة القياسية والمكتبات المحيطة تؤثّر كثيراً. ثم حتى على الجهاز نفسه يتزعزع الناتج بسهولة بإعدادات الطاقة والحرارة والمعالجة في الخلفية وانحياز بيانات الإدخال. عالم موحل إلى حد بعيد.
flowchart TB
accTitle: عوامل تزعزع الناتج
accDescr: يبين المخطّط أن أثر JIT والإحماء، ووجود GC وخصائصه، وفروق تنفيذ المكتبة القياسية والمحيطة، وإعدادات الطاقة والحرارة والضوضاء وانحياز بيانات الإدخال تتراكب فيتزعزع ناتج القياس بسهولة.
n1["أثر JIT والإحماء"] --> n4["الناتج يتزعزع بسهولة"]
n2["فروق GC وتنفيذ المكتبات"] --> n4
n3["الطاقة والحرارة والضوضاء وانحياز الإدخال"] --> n4
n4 -.-> n5["لا تحكم بالأفضلية بصف أرقام من بيئات أخرى"]
الشكل 1: مصادر التزعزع كثيرة، لذا مقارنة بصف أرقام الآخرين لا تقوم.
ترتّب هذه المقالة طرق القياس لمقارنة C# / C++ / Java / Go بعدل قدر الإمكان. الخلاصة أولاً: الأهم ألا تحاول حسم «أي لغة الأسرع» برقم واحد.
موضوع المقالة ترتيب منهج المقارنة. صف أرقام تعتمد على البيئة ينقلب بسهولة إن اختلفت الشروط. لذا لا نكتب هنا ترتيباً قياسياً فعلياً. بدل ذلك نركّز على كيف تصمّم حتى تكتسب المقارنة قيمة.
الجمهور المستهدف
المقالة موجّهة إلى المطوّر أو القائد التقني الذي لديه مرشّحو لغات متعدّدون ويريد الحكم من جهة الأداء أيّها ينفّذ، وإلى من يريد تجميع قياس يمكنه القول في الشركة أو التقرير «قارنّا السرعة». الموضوع ليس تعمّقاً في لغة واحدة بل تصميم مقارنة عبر أربع لغات، لذا يُقرأ حتى إن كنت لا تلمس إلا لغة واحدة.
أمثلة الشيفرة: العدّاء المشترك PowerShell، والحزمة داخل اللغة BenchmarkDotNet لـ C# وJMH لـ Java. لـ C++ وGo نذكر فقط كيف توحَّد الشروط.
مصطلحات ينبغي تثبيتها مسبقاً
في المتن تظهر بعض المصطلحات بالإنجليزية كما هي. نجمعها مسبقاً حتى لا تتعثّر عند أوّل ظهور.
| المصطلح | المعنى |
|---|---|
| p95 / p99 | المئين. عند ترتيب كل التشغيلات من الأسرع، القيمة عند 95% / 99% من الأسفل. «5 من 100 أبطأ من هذا» يقابل p95 |
| RSS (Resident Set Size) | الحجم الذي تحمله العملية فعلاً في الذاكرة الفعلية. ليس حجم الذاكرة الافتراضية المحجوزة، بل «كم تشغل من الذاكرة الفعلية الآن» |
| LTO (Link Time Optimization) | آليّة تحسين عبر وحدات الترجمة عند الربط. يقابل -flto في GCC / Clang و/GL مع /LTCG في MSVC |
| PGO (Profile-Guided Optimization) | آليّة تستخدم ملف تعريف التفرّع والاستدعاء المجموع من تشغيل سابق في قرارات تحسين البناء التالي |
| Tiered Compilation | آليّة JIT في .NET: تشغّل أولاً شيفرة تُترجم بسرعة، ثم تعيد تحسين الدوال كثيرة الاستدعاء لاحقاً. أحد الأسباب الرئيسة لفرق cold وwarm |
| Server GC / Workstation GC | وضعا تشغيل GC في .NET. Server GC يملك كومة وخيط GC لكل معالج منطقي ويميل إلى الإنتاجية، وWorkstation GC يميل إلى الاستجابة |
| GOMAXPROCS | حدّ أعلى لعدد خيوط نظام التشغيل التي ينفّذ عليها وقت تشغيل Go شيفرة Go في الوقت نفسه. في القياس المتوازي بلا تثبيت هذا لا تُقارن النتائج |
| cgo | آليّة استدعاء شيفرة C من Go. تفعيلها يغيّر تكلفة الاستدعاء وشروط البناء وإمكان الربط الساكن |
الخلاصة أولاً
ما يؤثّر حقاً في مقارنة سرعة C# / C++ / Java / Go هذه السبعة.
-
قرّر أولاً أي سرعة تقارن طريقة القياس تتغيّر بحسب إن كان زمن التشغيل، أم إنتاجية الحالة المستقرّة، أم تأخير p95، أم كفاءة الذاكرة.
-
لا تستنتج من قياس واحد فقط في حساب CPU، وتخصيص الذاكرة، والمعالجة المتوازية، وزمن التشغيل، يتبدّل ظهور اللغة أو وقت التشغيل القوي.
-
افصل cold عن warm في C# وJava خلط مقارنة تشمل التشغيل الأوّل بمقارنة الحالة المستقرّة بعد الإحماء يعوّج الحديث.
-
قِس بالخوارزمية نفسها، والإدخال نفسه، والتحقق من الصحة نفسه «لم يكن تنفيذاً سريعاً بل حلّ مسألة أخرى» حادث شائع في القياس.
-
افصل القياس المصغّر داخل اللغة عن القياس من طرف إلى طرف العابر للغات حزم كل لغة مريحة، لكن المقارنة عبر اللغات أمتن بعدّاء مشترك خارجي.
-
انظر إلى الوسيط والتوزيع لا إلى المتوسط وحده إصابة GC أو معالجة خلفية مرّة واحدة تكسر المتوسط.
-
أبقِ الشروط لا الأرقام وحدها ناتج القياس سجل سرعة وسجل شروط تجربة في آن. الناتج بلا شروط مؤلم جداً لاحقاً.
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 24، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
ما ينبغي تقريره أولاً
إن أنهيت «سريع» بكلمة واحدة وقعت الحوادث في الغالب. قرّر أولاً ماذا تسمّي سريعاً.
حتى مع البرنامج نفسه، ما تريد رؤيته يختلف كثيراً.
1. هل تريد رؤية زمن التشغيل
لأدوات CLI، والدفعات قصيرة العمر، والأدوات المساعدة التي تُشغَّل مرّة وتنتهي فوراً، يؤثّر cold start وprocess startup. على هذا المحور يتغيّر الناتج كثيراً بحسب تضمين تكلفة تهيئة JIT وتحميل الأصناف أم لا.
2. هل تريد رؤية إنتاجية التشغيل الطويل
للخوادم، والعمليات المقيمة، والعاملين، ومعالجة التحويل طويلة الأمد، مهمّة إنتاجية الحالة المستقرّة. هنا البطء في المرّة الأولى ليس الجوهر؛ الموضوع إلى أين تمتد الاستقرار بعد الإحماء.
3. هل تريد رؤية تأخير الذيل
في API وواجهة المستخدم والمعالجة القريبة من الزمن الحقيقي قد يهم p95 / p99 أكثر من المتوسط. حتى إن كان المتوسط سريعاً، التوقّف الكبير أحياناً مؤلم لتجربة المستخدم أو لـ SLA.
4. هل تريد تضمين كفاءة الذاكرة أيضاً
بلا النظر إلى أقصى RSS، وكمية التخصيص، وعدد GC، وتوقّف GC إضافة إلى زمن CPU تخطئ قراءة ثقل التشغيل الفعلي. «سريع لكن يأكل ذاكرة كثيرة» و«أبطأ قليلاً لكن خفيف مستقر» ينقلبان في التقييم بحسب الغرض.
أي أن السؤال الذي ينبغي تقريره أولاً هو
ما تريد معرفته في هذه المقارنة ليس أي لغة أسرع، بل أي حمل عمل، وبأي شروط، وبأي مؤشر يُعالَج بسرعة
جمع الأرقام وهذا غامض لا يتماسك في النهاية.
flowchart TB
accTitle: قرّر أولاً ماذا تسمّي سريعاً
accDescr: يبين المخطّط أن طريقة القياس تتغيّر بحسب إن كنت تريد زمن التشغيل أو إنتاجية الحالة المستقرّة أو تأخير الذيل أو كفاءة الذاكرة، لذا تقرّر قبل المقارنة أي حمل عمل وبأي شروط وبأي مؤشر تنظر.
q1["ماذا تريد معرفته في هذه المقارنة"] --> a1["زمن التشغيل"]
q1 --> a2["إنتاجية الحالة المستقرّة"]
q1 --> a3["تأخير p95 / p99"]
a3 -.-> a4["كفاءة الذاكرة محور منفصل أيضاً"]
a1 -.-> a5["طريقة القياس تتغيّر بحسب المحور"]
الشكل 2: لا تبدأ جمع الأرقام حتى تتحدّد تعريف «سريع».
لماذا مقارنة اللغات صعبة
خلط JIT وAOT يصير تجربة أخرى
تتأثّر C# وJava عادة بـ JIT. أما C++ وGo فمجمَّعتان مسبقاً في العادة.
أي أن قياس التشغيل الأوّل يقيس مع سرعة البرنامج نفسه أيضاً تشغيل وقت التشغيل وتحميل الأصناف وتجهيز JIT. عكس ذلك، إن نظرت بعد إحماء كافٍ فقط صارت المقارنة إلى أين يصل تحسين الحالة المستقرّة.
لكليهما معنى. غير أنهما ليسا المعنى نفسه.
flowchart TB
accTitle: فرق مجموعة JIT ومجموعة AOT
accDescr: يبين المخطّط أن C# وJava تتأثّران عادة بـ JIT فيختلط في التشغيل الأوّل تشغيل وقت التشغيل وتحميل الأصناف وتجهيز JIT، وأن C++ وGo مجمَّعتان مسبقاً في العادة، لذا مقارنة التشغيل الأوّل ومقارنة ما بعد الإحماء ليستا المعنى نفسه.
j1["C# وJava (عادة JIT)"] --> j2["يختلط في الأوّل التشغيل وتجهيز JIT"]
g1["C++ وGo (عادة AOT)"] --> g2["تعمل مجمَّعة مسبقاً"]
j2 --> mix["مقارنة الأوّل ومقارنة الحالة المستقرّة تجربتان مختلفتان"]
g2 --> mix
الشكل 3: بين لغات بنماذج تنفيذ مختلفة، من أين تقيس يحدّد محتوى التجربة.
فرق التنفيذ أكبر من فرق اللغة في العادة
حتى مع «الفرز» نفسه:
- أحد الجانبين يستخدم المكتبة القياسية
- والآخر تنفيذ خاص
- أحد الجانبين ينسخ زائداً
- والآخر يعيد توليد الإدخال في كل مرّة
هذا وحده يغيّر الناتج كثيراً.
ثم في معالجة مثل JSON والضغط والتشفير والتعبير النمطي يؤثّر تنفيذ المكتبة أكثر من اللغة نفسها. لذا بلا التصريح بما تقيس يصير «مقارنة لغات» في النية «مقارنة مكتبات».
flowchart TB
accTitle: مقارنة اللغات تنزلق إلى مقارنة مكتبات
accDescr: يبين المخطّط أن الناتج يتغيّر حتى مع المعالجة نفسها بحسب المكتبة القياسية أو التنفيذ الخاص ووجود نسخ زائد أو إعادة توليد الإدخال، وأن فرق تنفيذ المكتبة يؤثّر في JSON والضغط والتشفير والتعبير النمطي أكثر من اللغة نفسها، فبلا التصريح بما تقيس تصير مقارنة اللغات مقارنة مكتبات.
d1["تنفيذات يُفترض أنها المعالجة نفسها"] --> d2["يختلط فرق التنفيذ والمكتبة"]
d2 --> d3{"هل صرّحت بما تقيس؟"}
d3 -->|"لا"| d4["نية مقارنة لغات تصير مقارنة مكتبات"]
d3 -->|"نعم"| d5["يمكن تفسيرها مقارنة"]
الشكل 4: تسمية ما تقيس تسمية صحيحة تزيل معظم سوء الفهم.
في C++ فخ اختفاء العمل بسبب التحسين
خصوصاً في القياس المصغّر، إن حكم المترجم أن «لا أحد يستخدم ناتج هذا الحساب» فقد يحذف المعالجة. عندئذ تقيس ليس سرعة بل عدم فعل شيء أصلاً. المظهر النموذجي اختفاء الحلقة كلّها وزمن تنفيذ يقترب من الصفر.
في C++ تظهر هذه المشكلة بوضوح خاص، لذا مهم جداً استخدام الناتج أو إخراج checksum أو وظيفة كبت التحسين في إطار القياس.
flowchart TB
accTitle: فخ اختفاء العمل بسبب التحسين
accDescr: يبين المخطّط أنه في القياس المصغّر إن حكم المترجم أن لا أحد يستخدم ناتج الحساب اختفت المعالجة نفسها، فتقيس عدم فعل شيء لا سرعة، لذا مهم استخدام الناتج أو إخراج checksum أو وظيفة كبت التحسين.
o1["لا أحد يستخدم ناتج الحساب"] --> o2["المترجم يحذف المعالجة"]
o2 --> o3["زمن التنفيذ يبدو قريباً من الصفر"]
o3 -.-> o4["ليس سرعة بل عدم فعل شيء"]
o3 --> o5["امنع بإخراج checksum أو وظيفة الكبت"]
الشكل 5: الناتج السريع على نحو غريب اشك أولاً إن كان العمل لم يُحذف.
وجود GC ليس «عيباً» ولا «ميزة» بل خاصية
لدى C# وJava وGo جامع قمامة. اعتبار ذلك ببساطة «يوجد GC إذن أبطأ» فجّ جداً.
عملياً يؤثّر أكثر:
- كيف تُعالَج الكائنات قصيرة العمر الكثيرة
- إعداد حجم الكومة
- تكرار GC وتوقّفه
- تخطيط الكائنات
- عادة التخصيص في المكتبة
عكس ذلك تستطيع C++ التحكّم الدقيق بالإدارة اليدوية أو RAII، لكن بذلك يسهل ظهور فرق التصميم والتنفيذ. أي أن فرق أسلوب الإدارة ليس حسناً أو سوءاً أو أفضليّة كما هو.
flowchart TB
accTitle: GC خاصية لا عيب
accDescr: يبين المخطّط أنه في اللغات ذات GC تؤثّر طريقة معالجة الكائنات قصيرة العمر وإعداد الكومة وتكرار GC وتوقّفه وعادة التخصيص أكثر، وأن C++ تستطيع التحكّم الدقيق فيسهل ظهور فرق التصميم والتنفيذ، ففرق أسلوب الإدارة ليس أفضليّة كما هو.
gc1["لغة ذات GC"] --> gc2["يؤثّر الإعداد وعادة التخصيص"]
gp1["الإدارة اليدوية وRAII في C++"] --> gp2["يمكن التحكّم لكن يسهل فرق التنفيذ"]
gc2 --> gv1["فرق أسلوب الإدارة ليس أفضليّة"]
gp2 --> gv1
الشكل 6: ترتيب حتى لا تُنهى الجملة بـ «يوجد GC إذن أبطأ».
ما لا يجوز فعله في المقارنة
1. خلط Debug وRelease
هذا خارج النقاش. وحّد المقارنة حتماً على بناء محسَّن بمستوى الإنتاج.
2. عدم حل المسألة نفسها
صيغة الإدخال مختلفة، الخرج مختلف، معالجة الخطأ ناقصة في أحد الجانبين، سياسة إعادة استخدام الذاكرة مختلفة. ترك هذا يقيس فرق المتطلّب لا السرعة.
3. الاستنتاج بعد تشغيل واحد فقط
التشغيل الواحد ضوضاء في الغالب.
- JIT
- ذاكرة التخزين المؤقّت للصفحات
- تعزيز CPU
- الحرارة
- مهام الخلفية
- GC
- قراءة الملف في المرّة الأولى
هذه تختلط كلّها في مرّة واحدة.
flowchart TB
accTitle: ما يختلط في تشغيل واحد
accDescr: يبين المخطّط أن تشغيل واحد يخلط JIT وذاكرة التخزين المؤقّت للصفحات وتعزيز CPU والحرارة ومهام الخلفية وGC وقراءة الملف في المرّة الأولى، لذا ناتج المرّة الواحدة ضوضاء في الغالب.
x1["JIT والقراءة الأولى"] --> x4["يختلط كل شيء في تشغيل واحد"]
x2["التخزين المؤقّت وتعزيز CPU"] --> x4
x3["الحرارة ومعالجة الخلفية"] --> x4
x4 --> x5["ناتج المرّة الواحدة ضوضاء في الغالب"]
الشكل 7: الرقم من مرّة واحدة يحمل أكثر ما لا تريد قياسه.
4. خلط الإحماء
عند قياس C# وJava، غموض تضمين المرّة الأولى أو النظر بعد الإحماء فقط يهدم النقاش. عامل cold وwarm كشيئين منفصلين.
5. عدم التحقق من الصحة
القياس يحتاج «إرجاع الناتج نفسه» قبل «سريع». أكّد حتماً أن كل التنفيذات المقارنة تحصل من الإدخال نفسه على checksum نفسه أو الخرج نفسه.
6. حسم النظرة إلى العالم بقياس مصغّر واحد
الفوز في حلقة ضيقة لا يعني الفوز في الخدمة كلّها. عكس ذلك، الخسارة في زمن التشغيل قد تبقى قوّة كافية في التشغيل الطويل.
السياسة الأساسية عند مقارنة C# / C++ / Java / Go
هنا مهم جداً. الموصى به بنية من طبقتين.
flowchart TB
subgraph outer["الطبقة الخارجية: عدّاء مشترك عابر للغات"]
direction TB
R["العدّاء المشترك<br/>تعشية ترتيب التنفيذ / فصل cold وwarm<br/>التحقق من checksum / حفظ البيانات الخام"]
R --> E1["ملف تشغيل القياس<br/>C++"]
R --> E2["ملف تشغيل القياس<br/>C#"]
R --> E3["ملف تشغيل القياس<br/>Java"]
R --> E4["ملف تشغيل القياس<br/>Go"]
end
subgraph inner["الطبقة الداخلية: التعمّق داخل اللغة"]
direction TB
H1["Google Benchmark"]
H2["BenchmarkDotNet"]
H3["JMH"]
H4["go test -bench وbenchstat"]
end
E1 -.->|"قياس المعالجة نفسها بتفصيل داخل اللغة"| H1
E2 -.->|"قياس المعالجة نفسها بتفصيل داخل اللغة"| H2
E3 -.->|"قياس المعالجة نفسها بتفصيل داخل اللغة"| H3
E4 -.->|"قياس المعالجة نفسها بتفصيل داخل اللغة"| H4
الشكل 8: المقارنة عبر اللغات بعدّاء مشترك خارجي، والتعمّق داخل اللغة بحزمة مخصّصة.
الأرقام من الطبقة الخارجية أرقام يجوز مقارنتها عبر اللغات، والأرقام من الطبقة الداخلية أرقام لتتبع التحسين داخل تلك اللغة. الجوهر ألا تخلط الاثنين في جدول واحد.
1. القياس داخل اللغة استخدم الحزمة المناسبة لتلك اللغة
لكل لغة أدوات قياس تمتص ظروف تلك اللغة.
- C#: BenchmarkDotNet
- Java: JMH
- Go:
go test -benchوbenchstat - C++: Google Benchmark
هذه ترعى إلى حد ما ظروف وقت التشغيل والإحصاء ومطبات القياس في كل منها. فعّالة جداً في المقارنة داخل اللغة والتعمّق في التنفيذ.
مثلاً قياس «فرز 10 ملايين int32» بـ BenchmarkDotNet يكون أصغر تكوين كالتالي.
// C# / .NET 8 + BenchmarkDotNet 0.13 系
// dotnet add package BenchmarkDotNet
// dotnet run -c Release
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<SortBench>();
[MemoryDiagnoser] // 割り当て量と GC 回数も一緒に出す
public class SortBench
{
private int[] _source = Array.Empty<int>();
private int[] _work = Array.Empty<int>();
[GlobalSetup]
public void Setup()
{
var rng = new Random(12345); // seed を固定して毎回同じ入力にする
_source = new int[10_000_000];
for (int i = 0; i < _source.Length; i++)
{
_source[i] = rng.Next();
}
_work = new int[_source.Length];
}
[IterationSetup] // 毎回、未ソートの状態に戻す
public void ResetInput() => Array.Copy(_source, _work, _source.Length);
[Benchmark]
public long SortInt32()
{
Array.Sort(_work);
long checksum = 0;
foreach (int v in _work)
{
checksum = checksum * 31 + v;
}
return checksum; // 結果を返して最適化で消えないようにする
}
}
لـ [IterationSetup] قيد. وثائق BenchmarkDotNet الرسمية لا توصي به في القياس المصغّر لأنه يلوّث الناتج، وتعدّه مفيداً في قياس كلّي يستغرق 100ms فأكثر. فرز 10 ملايين عنصر يلبّي هذا الشرط، أما عند قياس معالجة قصيرة فغيّر الشكل إلى الحمل في جانب [GlobalSetup].
في JMH لـ Java التفكير نفسه.
// Java / JMH。pom.xml に jmh-core と jmh-generator-annprocess を入れます
// mvn clean verify
// java -jar target/benchmarks.jar SortBench
package bench;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3) // JVM を分けて JIT の当たり外れをならす
public class SortBench {
private int[] source;
private int[] work;
@Setup(Level.Trial)
public void setUp() {
Random rng = new Random(12345); // seed を固定して毎回同じ入力にする
source = new int[10_000_000];
for (int i = 0; i < source.length; i++) {
source[i] = rng.nextInt();
}
work = new int[source.length];
}
@Setup(Level.Invocation) // 毎回、未ソートの状態に戻す
public void resetInput() {
System.arraycopy(source, 0, work, 0, source.length);
}
@Benchmark
public long sortInt32() {
Arrays.sort(work);
long checksum = 0;
for (int v : work) {
checksum = checksum * 31 + v;
}
return checksum; // 返り値は JMH が消費するので最適化で消えない
}
}
لـ Level.Invocation قيد من النوع نفسه. تنصّ javadoc في JMH على أن هذا المستوى لا يُستخدم إلا في قياس يتجاوز فيه استدعاء واحد لميثود @Benchmark ملي ثانية واحدة. لأنه يأخذ طابع زمن عند كل استدعاء، فتصير عملية القياس نفسها عنق زجاجة في المعالجة القصيرة.
قيم @Warmup / @Measurement / @Fork في BenchmarkDotNet وJMH شروط تجربة كما هي. أبقها حتماً مع الناتج.
2. للمقارنة العابرة للغات ضع عدّاء مشتركاً في الخارج
من جهة أخرى، صف ناتج BenchmarkDotNet لـ C# وناتج JMH لـ Java كما هما فيه شيء من الخطر. لأن ممارسة الحزمة نفسها مختلفة.
لذا في العبور بين اللغات الموصى به جعل كل تنفيذ ملف تشغيل يُستدعى بعقد 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
جانب الخرج أيضاً عقد. اجعل كل تنفيذ بأي لغة يخرج هذين السطرين فقط إلى الخرج القياسي، فيكتفي تحليل العدّاء بتعبير نمطي واحد.
checksum=[16進の文字列]
inner_ms=[小数のミリ秒]
checksum للتحقق من الصحة، وinner_ms زمن المعالجة نفسها الذي قاسه التنفيذ. يقيس العدّاء على حدة wall-clock للعملية كلّها، فيبقى الزمن شاملاً التشغيل وزمن المتن وحده.
flowchart TB
accTitle: عقد CLI وعقد الخرج
accDescr: يبين المخطّط أن جعل تنفيذ كل لغة ملف تشغيل يُستدعى بعقد CLI واحد، وإخراج سطرين فقط checksum وinner_ms إلى الخرج القياسي، يجعل العدّاء يقيس زمن العملية كلّها على حدة فيبقى الزمن شاملاً التشغيل وزمن المتن وحده.
ct1["ملف تشغيل بعقد CLI واحد"] --> ct2["يخرج سطرين checksum وinner_ms"]
ct2 --> ct3["العدّاء يقيس wall-clock للكل"]
ct3 --> ct4["يبقى الشامل والمتن وحدهما"]
الشكل 9: جعل الاستدعاء والخرج عقداً يضع اللغات الأربع على الحلبة نفسها.
ثم في جانب العدّاء المشترك اجعل التدفّق:
- تعشية ترتيب التنفيذ
- فصل cold / warm
- تمرير مجموعة البيانات نفسها
- التحقق من checksum
- أخذ wall-clock والذاكرة
- إبقاء البيانات الخام في CSV / JSON
الهيكل وحده يُكتب كالتالي.
# run-bench.ps1 : عدّاء مشترك عبر اللغات (الهيكل)
# يعمل على PowerShell 7.4.
# pwsh ./run-bench.ps1 -Scenario sort_int32 -Dataset ./data/sort_10m.bin -Runs 15
param(
[Parameter(Mandatory = $true)][string]$Scenario,
[Parameter(Mandatory = $true)][string]$Dataset,
[int]$Runs = 15,
[int]$WarmupRuns = 3,
[string]$OutCsv = "./results/raw.csv"
)
# الملفّات التنفيذية لكلّ لغة. عقد CLI موحَّد تماماً عبر التنفيذات الأربعة.
# لـ Java ضع غلافاً رقيقاً يلفّ java -jar لتوحيد طريقة الاستدعاء.
$Implementations = @(
@{ Language = "cpp"; Exe = "./build/cpp/bench.exe" },
@{ Language = "csharp"; Exe = "./build/csharp/bench.exe" },
@{ Language = "java"; Exe = "./build/java/bench.cmd" },
@{ Language = "go"; Exe = "./build/go/bench.exe" }
)
function Invoke-OneRun {
param(
[Parameter(Mandatory = $true)][hashtable]$Impl,
[Parameter(Mandatory = $true)][string]$Mode,
[Parameter(Mandatory = $true)][int]$Index
)
# $Scenario و $Dataset من كتلة param في البداية
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$stdout = & $Impl.Exe --scenario $Scenario --dataset $Dataset --mode $Mode
$sw.Stop()
$exitCode = $LASTEXITCODE
# حلّل عقد الإخراج مع التنفيذ في موضع واحد هنا
$checksum = ""
$innerMs = [double]::NaN
foreach ($line in $stdout) {
if ($line -match '^checksum=(\S+)$') { $checksum = $Matches[1] }
if ($line -match '^inner_ms=(\S+)$') { $innerMs = [double]$Matches[1] }
}
if ($exitCode -ne 0 -or [string]::IsNullOrEmpty($checksum)) {
throw "$($Impl.Language) / $Mode / run $Index فشل. exit=$exitCode"
}
# التنفيذ الذي نسي inner_ms يرجع checksum فقط برمز خروج 0.
# إن لم ترفض هنا دخل NaN إلى CSV وانكسر مقارنة الزمن الداخليّ صامتاً
if ([double]::IsNaN($innerMs) -or [double]::IsInfinity($innerMs) -or $innerMs -lt 0) {
throw "$($Impl.Language) / $Mode / run $Index لم يرجع inner_ms (القيمة: $innerMs)." +
"عقد الإخراج سطران: checksum= و inner_ms="
}
return [pscustomobject]@{
timestamp = (Get-Date).ToString("o")
language = $Impl.Language
scenario = $Scenario
cold_or_warm = $Mode
run_index = $Index
process_ms = [math]::Round($sw.Elapsed.TotalMilliseconds, 3)
inner_ms = $innerMs
checksum = $checksum
}
}
# 1. تحقّق من الصحّة أوّلاً. إن لم ترجع كلّ التنفيذات نفس checksum فلا معنى لقياس السرعة
$expected = $null
foreach ($impl in $Implementations) {
for ($i = 1; $i -le $WarmupRuns; $i++) {
$r = Invoke-OneRun -Impl $impl -Mode "warm" -Index $i
if ($null -eq $expected) {
$expected = $r.checksum
}
elseif ($r.checksum -ne $expected) {
throw "الـ checksum غير متطابق. $($impl.Language) أعطى $($r.checksum)، والمعيار $expected"
}
}
}
# 2. الإنتاج. عشِّ ترتيب التنفيذ في كلّ run لتسوية انحياز الحرارة والوقت
# تحقق 1 يخصّ warm run المُهمَل، لذا طابق checksum أيضاً لكلّ run يُسجَّل.
# إن أُسقط هذا اختلط زمن تنفيذ صار يعمل عملاً مختلفاً في الوسط إلى CSV،
# فبطلت المقارنة نفسها
$rows = [System.Collections.Generic.List[object]]::new()
foreach ($mode in @("cold", "warm")) {
for ($i = 1; $i -le $Runs; $i++) {
$shuffled = $Implementations | Get-Random -Count $Implementations.Count
foreach ($impl in $shuffled) {
$r = Invoke-OneRun -Impl $impl -Mode $mode -Index $i
if ($r.checksum -ne $expected) {
throw "الـ checksum غير متطابق. $($impl.Language) في $mode run #$i أعطى $($r.checksum)، والمعيار $expected"
}
$rows.Add($r)
}
}
}
# 3. أبقِ البيانات الخام حتماً. التجميع لاحقاً من هذا CSV
# إن مُرِّر اسم بلا دليل أب مثل -OutCsv raw.csv
# رجع Split-Path -Parent سلسلة فارغة ورفضها New-Item.
# السقوط هنا بعد انتهاء كلّ run فيُفقد قياس النتيجة بالكامل
$outDir = Split-Path -Parent $OutCsv
if ($outDir) { New-Item -ItemType Directory -Force -Path $outDir | Out-Null }
$rows | Export-Csv -Path $OutCsv -NoTypeInformation -Encoding utf8
Write-Host "raw data: $OutCsv / $($rows.Count) سطر"
هنا ثلاثة أمور مقصودة.
- وضع التحقق من الصحة قبل قياس السرعة. مقارنة السرعة وchecksum مختلف لا معنى لها
- التعشية في كل تشغيل. الغرض ألا يكون الترتيب أ كلّه ثم ب
- كتابة البيانات الخام دون تجميع. المتوسط والوسيط يُخرجان لاحقاً من CSV
بهذا يسهل فصل أفضل الممارسات داخل كل لغة عن العدل العابر للغات.
flowchart TB
accTitle: المقاصد الثلاثة للعدّاء المشترك
accDescr: يبين المخطّط أن العدّاء المشترك يقصد ثلاثة أمور: وضع التحقق من الصحة بـ checksum قبل قياس السرعة، وتعشية ترتيب التنفيذ في كل تشغيل، وكتابة البيانات الخام دون تجميع.
rn1["التحقق من الصحة قبل السرعة"] --> rn4["قياس يمكن مقارنته بعدل"]
rn2["تعشية في كل تشغيل"] --> rn4
rn3["كتابة البيانات الخام دون تجميع"] --> rn4
rn2 -.-> rn5["تسوية انحياز الحرارة والوقت"]
الشكل 10: عمل العدّاء ليس التشغيل السريع، بل إزالة الشك.
مثال ملموس: أي بنود قياس ينبغي تجهيزها
عندما يُطلب «مقارنة C# / C++ / Java / Go»، إن كان واحداً فقط فجهّز سلسلة CPU بسيطة يصعب سوء فهمها، وإن كانت عدّة فجهّز 3 إلى 4 أحمال عمل مختلفة الطابع.
التكوين الموصى به
1. sort_int32_10m
الغرض: رؤية CPU + عرض نطاق الذاكرة + استخدام المنطقة المؤقّتة
- الإدخال: 10 ملايين
int32مولَّدة ببذرة ثابتة - المعالجة: فرز المصفوفة وإرجاع checksum
- نقطة الانتباه: إعادة الإدخال غير المفرز نفسه في كل مرّة
هذا سهل الفهم نسبياً. غير أنه يتضمّن أيضاً فرق تنفيذ الفرز القياسي، لذا يصير مقارنة تشمل المكتبة القياسية أكثر من اللغة نفسها.
2. hash_group_count
الغرض: رؤية اتجاه جدول التجزئة ومعالجة السلاسل والتخصيص وGC
- الإدخال: بيانات نص ثابتة
- المعالجة: عدّ ظهور كل كلمة
- الخرج: أعلى N وchecksum
هذا أقرب إلى الواقع، وفي المقابل يؤثّر أيضاً فرق مكتبة السلاسل وتنفيذ map كثيراً. بذلك تصير المقارنة أقرب إلى الواقع.
3. parallel_sha256
الغرض: رؤية عادة المعالجة المتوازية والمجدول ومجمّع العاملين والمزامنة
- الإدخال: سلسلة أجزاء ثنائية بحجم ثابت
- المعالجة: تجزئة بالترتيب على N خيط وإرجاع checksum النهائي
- الشرط: تدرج عدد الخيوط مثل 1 / 2 / 4 / 8
أسهل رؤية امتداد التنفيذ المتوازي من الحلقة الضيقة البسيطة.
4. startup_noop أو startup_parse_small
الغرض: رؤية زمن التشغيل
noop: التشغيل ثم الإنهاء فوراًparse_small: معالجة إدخال صغير مرّة واحدة ثم الإنهاء
هنا تظهر تكلفة JIT والتهيئة في C# / Java بسهولة، ويختلف المظهر كثيراً عن C++ / Go. عكس ذلك، حتى إن ظهر فرق هنا فهو غير نتيجة المعالجة الطويلة.
flowchart TB
accTitle: أربعة بنود قياس مختلفة الطابع
accDescr: يبين المخطّط تكويناً يجهّز أربعة بنود يرى كل منها شيئاً مختلفاً: الفرز CPU وعرض النطاق، وعدّ الكلمات اتجاه التجزئة والسلاسل والتخصيص، والتجزئة المتوازية امتداد التنفيذ المتوازي، وقياس التشغيل زمن التشغيل والتهيئة.
w1["sort_int32_10m"] -.-> v1["CPU والعرض والمنطقة المؤقّتة"]
w2["hash_group_count"] -.-> v2["اتجاه السلاسل وmap وGC"]
w3["parallel_sha256"] -.-> v3["امتداد التنفيذ المتوازي"]
w4["عائلة التشغيل"] -.-> v4["تكلفة التشغيل والتهيئة"]
w1 --> w2
w2 --> w3
w3 --> w4
الشكل 11: بند واحد لا يرى الكل، لذا افصل البنود بحسب ما تريد رؤيته.
ماذا عن قياس JSON أو HTTP
JSON وHTTP قريبان من الواقع، فلهما معنى طبعاً. غير أنهما عندئذ يصيران مقارنة تشمل المكتبة والإطار والنظام البيئي أكثر من مقارنة لغات.
هذا نفسه ليس سيئاً. بل في الواقع كثيراً ما يكون أهم. لكن في المقالة أو التقرير أقل سوء فهم إن صرّحت:
هذه ليست مقارنة لغات، بل مقارنة تشمل التنفيذ القياسي والمكتبات الرئيسة
الشروط التي ينبغي توحيدها لكل لغة
C++
- وحّد على بناء محسَّن
- ثبّت المترجم
- ثبّت تنفيذ المكتبة القياسية
- صرّح بشروط مثل
-O3//O2وLTO وPGO - انتبه ألا يختفي الناتج بسبب التحسين
- اشك إن كان يبدو سريعاً بسلوك غير معرّف
C++ حرّة كثيراً، لذا يظهر فرق الشروط كبيراً كما هو. لذلك مهم جداً بأي مترجم، وبأي أعلام، وبأي 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
- ثبّت الإحماء / القياس / التفريع
- سجّل حجم الكومة وخيارات JVM
- افصل cold start عن الحالة المستقرّة
Java يسهل تلقّي فائدة JIT، وفي المقابل يتغيّر مظهر المرّة الأولى كثيراً. لذا فصل مقارنة العملية قصيرة العمر عن مقارنة التشغيل الطويل إلزامي.
Go
- ثبّت إصدار Go
- ثبّت
GOMAXPROCS - صرّح بـ
CGO_ENABLED - إن عبثت بـ
GOGCفسجّل حتماً - إن أمكن أبقِ خرجاً بصيغة القياس
Go أسهل نسبياً، لكن أثر GOMAXPROCS كبير في القياس المتوازي.
كما يتغيّر العالم بحسب استخدام cgo أم لا، لذا أبقِ ذلك حتماً في الشروط.
كيف توحَّد بيئة التنفيذ
في أي لغة، مقارنة بلا توحيد البيئة تقارن البيئة في الغالب.
flowchart TB
accTitle: حقيقة المقارنة بلا توحيد البيئة
accDescr: يبين المخطّط أنه في مقارنة غير موحّدة البيئة من CPU ونظام التشغيل وشروط الطاقة وبيانات الإدخال والأولوية وعدد الأنوية يصير فرق الناتج فرق بيئة لا فرق لغة.
en1["قياسان غير موحّدي البيئة"] --> en2["يظهر فرق في الناتج"]
en2 --> en3{"فرق ماذا هو؟"}
en3 -->|"إن وُحِّدت البيئة"| en4["يُقرأ فرق تنفيذ أو لغة"]
en3 -->|"إن لم تُوحَّد"| en5["مقارنة بيئة فقط"]
الشكل 12: نسبة الفرق إلى اللغة جائزة فقط بعد محو فرق البيئة.
ما ينبغي توحيده
- CPU / ذاكرة / تخزين نفسها
- إصدار نظام التشغيل نفسه
- شروط الطاقة نفسها
- شروط قريبة من درجة حرارة الغرفة نفسها
- بيانات الإدخال نفسها
- أولوية العملية نفسها
- شروط عدد الأنوية نفسها
- شروط الحاوية أو المعدن العاري نفسها
ما يؤثّر خصوصاً
إعدادات الطاقة وتردّد CPU
على الحاسوب المحمول، الاتصال بالمحوّل أو البطارية وحدهما عالم آخر. إن لم يُوحَّد منظم CPU أو وضع الطاقة تزعزع ناتج المقارنة كثيراً.
شروط الطاقة على Windows، والإشعارات، وضوضاء الخلفية، والحرارة، وتوحيد ترتيب التنفيذ مرتّبة بتفصيل في مقالة منفصلة: كيف تقارن إصدارات البرامج على Windows دون قياس الشيء الخطأ إن قست على Windows فهذا يؤثّر كثيراً.
الحرارة
إن كانت المرّات الأولى فقط سريعة ثم سقطت في النصف الثاني، اشك في الحرارة أو الخنق. بدل تشغيل أ كلّه ثم ب كلّه، أ / ب / أ / ب بالتناوب يقلّل الانحياز.
flowchart TB
accTitle: انحياز الحرارة وترتيب التنفيذ
accDescr: يبين المخطّط أنه إن سقط الناتج في النصف الثاني فقط اشك في الحرارة أو الخنق، وألا تشغّل أ كلّه ثم ب بل تناوب أ وب لتقليل الانحياز.
th1["الناتج يسقط في النصف الثاني فقط"] --> th2["اشك في الحرارة أو الخنق"]
th2 --> th3["لا تشغّل أ كلّه ثم ب"]
th3 --> th4["ناوب أ وب"]
الشكل 13: الحرارة لا تُمحى، لكن ترتيب التنفيذ يوزّعها بعدل على الاثنين.
معالجة الخلفية
التحديث، والفهرسة، والمزامنة، ومسح الفيروسات، والمتصفّح، وأدوات الدردشة. هذا الجانب هادئ لكنه يصيب عادة.
ماذا ينبغي قياسه
في مقارنة اللغات يُوصى بفصل هذه الأربعة على الأقل.
1. wall-clock time
الزمن الفعلي الذي ينتظره المستخدم. أول مؤشر ينبغي النظر إليه.
2. CPU time
«كم استُخدم CPU فعلاً». إن كان wall-clock سريعاً وCPU time لم يتغيّر فقد يكون أثر انتظار أو إدخال/إخراج.
3. memory / allocations
- أقصى RSS
- إجمالي التخصيص
- عدد التخصيص
- عدد GC
- توقّف GC
النظر إلى هذا يكشف التكلفة خلف السرعة.
4. التوزيع
- الوسيط
- p95 / p99
- الأدنى / الأقصى
- الانحراف المعياري أو التبعثر
الحديث بالمتوسط وحده يخفي حقيقة المعالجة التي تقفز أحياناً.
flowchart TB
accTitle: أربعة مؤشرات تُرى مفصولة
accDescr: يبين المخطّط أنه في مقارنة اللغات تُرى مفصولة أربعة: wall-clock وهو الزمن الفعلي الذي ينتظره المستخدم، وزمن استخدام CPU فعلاً، والذاكرة مثل أقصى RSS والتخصيص، والتوزيع مثل الوسيط والمئين.
ms1["wall-clock time"] --> ms5["انظر إلى الأربعة مفصولة"]
ms2["CPU time"] --> ms5
ms3["الذاكرة والتخصيص"] --> ms5
ms5 -.-> ms4["التوزيع (الوسيط وp95) أيضاً إطار منفصل"]
الشكل 14: رقم السرعة ليس نوعاً واحداً؛ يُقرأ بعد صف التكلفة خلفه.
إجراءات التنفيذ الموصى بها
التدفّق السهل للتشغيل الفعلي بهذا الترتيب تقريباً.
flowchart TB
a1["1. قرّر حمل العمل"] --> a2["2. ثبّت مجموعة بيانات مشتركة"]
a2 --> a3["3. أمرّ التحقق من الصحة أولاً"]
a3 --> a4{"هل تطابق checksum<br/>كل التنفيذات؟"}
a4 -- لا --> a3
a4 -- نعم --> a5["4. ثبّت شروط البناء"]
a5 --> a6["5. افصل cold عن warm"]
a6 --> a7["6. شغّل بتعشية الترتيب"]
a7 --> a8{"هل بلغت<br/>العدد اللازم؟"}
a8 -- لا --> a7
a8 -- نعم --> a9["8. احفظ البيانات الخام"]
a9 --> a10{"هل ظهر فرق<br/>ذو معنى؟"}
a10 -- لا --> a11["سجّل الشروط والعدد وانتهِ"]
a10 -- نعم --> a12["9. خذ ملفاً تعريفياً واحفر السبب"]
الشكل 15: إجراءات التنفيذ من تقرير حمل العمل إلى التحقق من الصحة، والتشغيل المعشّى، وحفظ البيانات الخام.
1. قرّر حمل العمل
وضّح أولاً ماذا تريد مقارنة.
- زمن التشغيل
- إنتاجية الحالة المستقرّة
- تأخير الذيل
- كفاءة الذاكرة
- التوسّع المتوازي
2. ثبّت مجموعة بيانات مشتركة
وحّد بيانات الإدخال ببذرة ثابتة أو ملف ثابت. إن شملت توليد البيانات أيضاً لزم توحيد شروطه في كل لغة.
3. أمرّ التحقق من الصحة أولاً
أكّد أن كل التنفيذات ترجع الناتج نفسه على بيانات صغيرة وكبيرة. إخراج checksum أو تجزئة يسهّل التعامل.
4. ثبّت شروط البناء
اصنع في كل لغة شكلاً تنفيذياً Release / محسَّناً، وسجّل الإصدار والأعلام.
5. افصل cold عن warm
مهم خصوصاً في C# وJava.
- cold: يشمل ما بعد تشغيل العملية مباشرة
- warm: الحالة المستقرّة بعد عدّة تشغيلات
رسم ما يُقاس إلى أين يوضّح أن الاثنين شيئان منفصلان.
flowchart LR
subgraph coldrange["النطاق المقاس كـ cold"]
direction LR
s1["تشغيل العملية"] --> s2["تهيئة وقت التشغيل<br/>تحميل الأصناف"]
s2 --> s3["الترجمة الأولى لـ JIT"] --> s4["المعالجة نفسها المرّة الأولى"]
end
s4 --> s5["المعالجة نفسها من المرّة الثانية<br/>يتقدّم Tiered Compilation"]
subgraph warmrange["النطاق المقاس كـ warm"]
direction LR
s6["المعالجة نفسها في الحالة المستقرّة"]
end
s5 --> s6
الشكل 16: يشمل cold من تشغيل العملية حتى الترجمة الأولى لـ JIT، ويقيس warm الحالة المستقرّة فقط.
ليس لـ C++ وGo، لكونهما مجمَّعتين مسبقاً، مرحلة تقابل «الترجمة الأولى لـ JIT»، وتهيئة وقت التشغيل أخف نسبياً. كثير من فرق cold يولد هنا. لذلك أنظف ألا تخلط الاثنين في الجدول نفسه.
6. ناوب ترتيب التنفيذ أو عشّه
مثال:
cpp -> csharp -> java -> go
go -> java -> cpp -> csharp
csharp -> go -> java -> cpp
...
بهذا يقلّ انحياز الحرارة والضوضاء.
7. أمّن العدد
في القياس المصغّر الخفيف أكثر بكثير، وفي القياس من طرف إلى طرف عشر مرّات على الأقل مرغوبة. الفرق الصغير مع عدد قليل يجعل التفسير هشّاً جداً.
8. احفظ البيانات الخام
أبقِ البيانات الخام لكل تشغيل لا ناتج التجميع وحده. لاحقاً تُقرأ القيم الشاذة وعادة الإحماء.
9. إن ظهر فرق فخذ ملفاً تعريفياً
عند ظهور الفرق احفر السبب عندئذ فقط.
- ملف تعريف CPU
- ملف تعريف التخصيص
- سجل GC
- مخطّط اللهب
- تتبع جانب نظام التشغيل
إلى هنا يصير الحديث لماذا صار كذلك لا «سريع / بطيء».
كيف تُقرأ النتائج
حتى بعد ظهور الأرقام، خطأ القراءة يبقى خطراً.
C# / Java بطيئتان في المرّة الأولى فقط
اشك في أثر JIT وتحميل الأصناف والتهيئة. عندئذ:
- فرق ذو معنى إن كان زمن التشغيل مهمّاً
- فرق ينبغي فصله في جدول آخر إن كان الموضوع التشغيل الطويل
C++ قوية في الحلقة الضيقة
قد تؤثّر التحسينات منخفضة المستوى وتخطيط الكائنات والحدّ الأدنى من عبء وقت التشغيل. غير أن القول من ذلك وحده «إذن الأسرع أيضاً في الخدمة الفعلية» قفزة.
Go تبدو مواتية في زمن التشغيل وسهولة التوزيع
قد تؤثّر الثنائية الواحدة، والنهوض الخفيف نسبياً، ونموذج التوازي السهل التعامل. غير أنها ليست مواتية في كل حمل عمل كثيف CPU.
C# / Java تلحقان كثيراً في الحالة المستقرّة، أو تقلبان النتيجة
قد تؤثّر تحسينات JIT. وهذا أيضاً ليس حديثاً نادراً. لذا المهم ألا تخلط مقارنة تشمل التشغيل ومقارنة الحالة المستقرّة.
الفرق كبير في المعالجة الكثيفة التخصيص
عندئذ يؤثّر أكثر من اسم اللغة:
- تخطيط الذاكرة
- التعامل مع السلاسل وmap
- سلوك GC
- النسخ الزائد
flowchart TB
accTitle: كيف تقرأ عندما يظهر فرق
accDescr: يبين المخطّط أنه إن كانت C# أو Java بطيئة في المرّة الأولى فقط اشك في أثر JIT والتهيئة، وعامل الفرق ذا معنى إن كان زمن التشغيل هو الموضوع، وفرقاً يُفصل في جدول آخر إن كان الموضوع التشغيل الطويل، واحفر السبب بملف تعريفي عند ظهور الفرق.
rd1["ظهر فرق"] --> rd2{"فرق بأي شروط؟"}
rd2 -->|"فرق يشمل التشغيل"| rd3["ذو معنى إن كان زمن التشغيل هو الموضوع"]
rd2 -->|"فرق الحالة المستقرّة"| rd4["عامل كمقارنة تشغيل طويل"]
rd3 --> rd5["احفر السبب بملف تعريفي"]
rd4 --> rd5
الشكل 17: قبل كبر الرقم وصغره، أكّد على أي حلبة يقع ذلك الفرق.
قالب التسجيل
في ناتج القياس يساعد لاحقاً إبقاء بنود بهذا القدر على الأقل.
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 بهذا الإحساس.
micromacrostartupparallel
cold_or_warm تريد التصريح حتماً بأيّهما.
coldwarm
ما يُدخل في كل عمود لا يتزعزع إن قُرّر مسبقاً.
| العمود | ما يُدخل | مثال الصيغة |
|---|---|---|
timestamp |
لحظة بدء التشغيل. تُستخدم لاحقاً لرؤية التغيّر بحسب الوقت | صيغة ISO 8601. 2026-03-17T10:00:00+09:00 |
language |
معرّف التنفيذ. مفردات ثابتة لمنع تزعزع الكتابة | cpp / csharp / java / go |
scenario |
اسم بند القياس | sort_int32_10m |
run_kind |
نوع القياس | micro / macro / startup / parallel |
cold_or_warm |
هل يشمل التشغيل | cold / warm |
elapsed_ms |
wall-clock. الإبقاء حتى المنزلة الثالثة لا يُحرج لاحقاً | ملي ثانية عشرية |
cpu_ms |
زمن CPU للعملية. مجموع user وsystem | ملي ثانية عشرية |
max_rss_mb |
أقصى RSS | MB صحيح أو عشري |
alloc_bytes |
إجمالي بايتات التخصيص. في لغة لا تُؤخذ اتركه فارغاً وأبقِ الفراغ نفسه | صحيح، أو فارغ |
gc_count |
عدد GC. في C++ فارغ دائماً | صحيح، أو فارغ |
checksum |
للتحقق من الصحة. يُفحص على حدة تطابقه في كل التنفيذات | سلسلة ست عشرية |
compiler_or_runtime |
نوع المعالج | msvc / dotnet / temurin / go |
compiler_version |
إصدار المعالج. حتى الثانوي | سلسلة الإصدار التي يخرجها المعالج |
flags |
شروط التحسين. /O2، -O3 -flto، Server GC، GOMAXPROCS=8 وغيرها |
سلسلة مفصولة بفراغ |
os / cpu / threads |
بيئة التنفيذ | اسم نظام التشغيل ورقم البناء، وطراز CPU، وعدد الخيوط المستخدمة |
input_id |
معرّف مجموعة البيانات. تجزئة الملف أضمن | اسم الملف والتجزئة |
notes |
مذكرة تشغيل فيه شذوذ | وصف حر |
الجوهر السماح بفراغ أعمدة القياس وإبقاء الفراغ نفسه. «لا يوجد gc_count لأنه C++» معلومة، لكن حذف العمود كلّه لا يتبيّن لاحقاً.
ماذا يُغفل بالنظر إلى المتوسط وحده
عند التجميع، التلخيص في متوسط واحد يطير بالمعلومات. التالي أرقام خيالية لشرح الحساب وليست قياساً فعلياً لأي لغة. افترض elapsed_ms لعشرة تشغيلات لتنفيذ واحد مرتّبة من الأسرع.
98, 99, 100, 101, 101, 102, 103, 104, 106, 720
القيم الممثّلة من هذه العشرة تصير كالتالي.
| المؤشر | القيمة | القراءة |
|---|---|---|
| المتوسط | 163.4 | انجرّ بالمرّة الأخيرة فابتعد نحو الأعلى بنحو 60% عن النطاق المعتاد |
| الوسيط | 101.5 | قيمة قريبة من إحساس 9 من 10 مرّات |
| الأدنى / الأقصى | 98 / 720 | الفرق أكثر من 7 أضعاف، فيصير إشارة للبحث أولاً عن سبب القيمة الشاذة |
| p95 / p99 | لا يمكن إخراجهما | 10 عيّنات لا تحمل معنى للمئين 95 ولا 99 |
أي أن جدولاً يحمل المتوسط وحده يجعل «تنفيذاً يتوقّف كبيراً أحياناً» و«تنفيذاً أبطأ قليلاً ومستقرّاً» الوجه نفسه. في صنع الجدول يؤثّر الفرق التالي.
| الجانب | جدول ناتج ضعيف | جدول ناتج صالح للاستخدام |
|---|---|---|
| القيمة الممثّلة | المتوسط وحده | الوسيط بطلاً، مع الأدنى / الأقصى والتبعثر |
| عدد المحاولات | غير مكتوب | عدد التشغيلات وسياسة استبعاد القيم الشاذة أم لا |
| cold / warm | مختلطان، أو بلا تمييز | جدول منفصل، أو صف منفصل |
| الصحة | غير مذكورة | التصريح بتطابق checksum في كل التنفيذات |
| الشروط | «قسنا على الجهاز نفسه» | حتى نظام التشغيل وCPU وإصدار المعالج وأعلام التحسين وعدد الخيوط |
| البيانات الخام | قيم التجميع فقط | موضع حفظ CSV الخام أيضاً |
إن أردت تحميل p95 أو p99 لزم أصلاً زيادة عدد التشغيلات. الحديث حديث أن الكلام عن التوزيع يحتاج عيّنات بعدد يرى التوزيع.
القياس أحياناً إمكان التفسير لاحقاً أهم من القياس نفسه.
flowchart TB
accTitle: ما يخفيه متوسط واحد
accDescr: يبين المخطّط أن قيمة شاذة كبيرة واحدة تزيح المتوسط كثيراً عن النطاق المعتاد، وأن الوسيط أقرب إلى الإحساس، وأن فرق الأدنى والأقصى الكبير إشارة للبحث عن سبب القيمة الشاذة، لذا الجدول بالمتوسط وحده خطر.
av1["تظهر قيمة شاذة كبيرة مرّة واحدة"] --> av2["المتوسط ينزاح للأعلى عن النطاق المعتاد"]
av2 --> av3["جدول المتوسط وحده يخفي الواقع"]
av3 --> av4["أرفق الوسيط والأدنى والأقصى"]
av4 -.-> av5["القيمة الشاذة إشارة للبحث عن السبب"]
الشكل 18: المتوسط لا يكذب، لكنه يسكت عن وجود القيمة الشاذة.
الخلاصة
ما يهم حقاً في مقارنة سرعة C# / C++ / Java / Go هو إسقاط سؤال فجّ أي لغة الأسرع إلى شكل تجربة: أي حمل عمل، وبأي شروط، وبأي مؤشر تقارن.
ما يصعب إخطاؤه خصوصاً هذا الجانب.
- افصل زمن التشغيل عن الحالة المستقرّة
- قِس بالخوارزمية نفسها، والإدخال نفسه، والتحقق من الصحة نفسه
- لا تستنتج من قياس واحد فقط
- افصل قياس داخل اللغة عن القياس العابر للغات
- انظر إلى الوسيط والتوزيع أكثر من المتوسط
- أبقِ الشروط والبيانات الخام
وأخيراً الأهم ألا تحاول حسم الغلبة باسم اللغة أكثر مما ينبغي. الأداء الفعلي حصيلة لغة ووقت تشغيل ومكتبة وشروط بناء وبيانات ونظام تشغيل وعتاد.
أحاديث «C++ سريعة» و«Java قوية» و«Go خفيفة» و«C# أيضاً سريعة بما يكفي» صحيحة بمعنى ما كلّها. غير أن سقوط بأي شروط يُقال ذلك يُنهي النقاش بلا التقاء.
وحّد الشروط، وعلى عدّة أحمال عمل، وافصل cold / warm، وانظر حتى التوزيع. هادئ، لكن هذا في النهاية الأقوى.
flowchart TB
accTitle: أسقط السؤال الفجّ إلى شكل تجربة
accDescr: يبين المخطّط أن إسقاط سؤال فجّ أي لغة الأسرع إلى شكل تجربة أي حمل عمل وبأي شروط وبأي مؤشر تقارن، وتوحيد الشروط وتشغيل عدّة أحمال عمل بفصل cold وwarm والنظر حتى التوزيع، هو الأقوى.
sq1["أي لغة الأسرع"] --> sq2["أعد الإسقاط إلى شكل تجربة"]
sq2 --> sq3["قرّر حمل العمل والشروط والمؤشر"]
sq3 --> sq4["شغّل بفصل cold وwarm"]
sq4 --> sq5["انظر حتى التوزيع وأبقِ مع الشروط"]
الشكل 19: إعادة صياغة السؤال هي أقوى خلاصة في هذه المقالة.
روابط مرجعية
-
BenchmarkDotNet Getting Started https://benchmarkdotnet.org/articles/guides/getting-started.html
-
BenchmarkDotNet Setup and Cleanup (شروط جواز استخدام
[IterationSetup]) https://benchmarkdotnet.org/articles/features/setup-and-cleanup.html -
OpenJDK JMH Project https://openjdk.org/projects/code-tools/jmh/
-
JMH
Leveljavadoc (قيودLevel.Invocationوالتحذير) https://javadoc.io/doc/org.openjdk.jmh/jmh-core/latest/org/openjdk/jmh/annotations/Level.html -
JMH GitHub Repository / README https://github.com/openjdk/jmh
-
Go
testingpackage https://pkg.go.dev/testing -
Go
benchstathttps://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/
موضوعات ذات صلة
صفحات يسهل الفهم معها إلى جانب هذه المقالة.
جهة الاستشارة لهذا الموضوع
تصميم مقارنة الأداء، وتوحيد شروط القياس، وتفسير الناتج، وحفر السبب موضوعات تتوافق جيداً مع الخدمات التالية.
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
لماذا تتعطّل الوسائط ── قواعد وسائط سطر أوامر Windows
في Windows لا توجد مصفوفة وسائط؛ يصل إلى CreateProcess سلسلة واحدة ويتولّى الطرف المستقبل تقسيمها. نشرح قواعد CommandLineToArgvW وCRT و.N...
ما الذي يبقى بعد موت الأب ── تربية العمليات الابنة في كائن Job
لماذا يبقى مساعد SDK ماسكاً الكاميرا أو منفذ COM بعد إنهاء الواجهة قسراً. كيف تجعل كائن Job شجرة العمليات وحدة واحدة وتصمم عمر العملية ال...
الأنابيب المسمّاة عمليّاً ── IPC القياسيّ في Windows من التصميم إلى الأمان
دليل عمليّ للأنابيب المسمّاة، وسيلة IPC القياسيّة في Windows. يغطّي وضع البايت مقابل وضع الرسالة، وخوادم متعدّدة العملاء، وأمان ACL وانتح...
الإيقاظات الزائفة ── لماذا يستيقظ متغيّر الشرط «دون إشعار» وكيف تنتظر بشكل صحيح على Windows
يمكن لانتظار متغيّر شرط أن يستيقظ بلا إشعار (إيقاظ زائف). لماذا يسمح Windows بذلك، والانتظار الصحيح بـ while ومحمول في Win32 وC++ وC#.
التوافق الخلفي لواجهات DLL وCOM ── جدول قرار لتحديد أيّ تغيير يكسر جهة الاستدعاء
أيّ تغيير في مكوّنات DLL أو COM يكسر جهة الاستدعاء؟ نرتّب الطبقات الثلاث للتوافق: الثنائيّ، والمصدريّ، والسلوكيّ، ونقدّم جدول قرار حسب نو...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
الاستشارات التقنية ومراجعة التصميم
موضوع يتناسب جيّداً مع الاستشارة التقنيّة ومراجعة التصميم، بما يشمل تصميم مقارنة الأداء وتوحيد شروط القياس وwarm-up وطريقة قراءة الإحصاءات.
التحقيق في الأخطاء وتحليل السبب الجذري
تحديد أسباب فروق الأداء بين اللغات والإصدارات، وتعيين نقاط الاختناق، والتحقّق من سلامة إجراءات القياس، أمور يسهل التقدّم فيها ضمن التحقيق في الأخطاء وتحليل الأسباب.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- أيّها الأسرع: C# أم C++ أم Java أم Go؟
- لا يُحسم برقم واحد. الأداء الفعلي حصيلة لغة ووقت تشغيل ومكتبة وشروط بناء وبيانات ونظام تشغيل وعتاد. المهم إسقاط سؤال «أي لغة الأسرع» إلى شكل تجربة: أي حمل عمل، وبأي شروط، وبأي مؤشر. في زمن التشغيل تظهر تكلفة JIT والتهيئة في C#/Java بسهولة، أما في الحالة المستقرّة فليس نادراً أن تلحق تحسينات JIT أو حتى تقلب النتيجة.
- لماذا يُفصل الإحماء في قياس C# وJava؟
- لأن C# وJava تتأثّران عادة بـ JIT، فقياس التشغيل الأوّل يقيس مع سرعة البرنامج نفسه تشغيل وقت التشغيل وتحميل الأصناف وتجهيز JIT. أما C++ وGo فمجمَّعتان مسبقاً في العادة. لـ cold وwarm معنى لكنهما ليسا المعنى نفسه، لذا عامل cold الذي يشمل ما بعد تشغيل العملية مباشرة وwarm وهو الحالة المستقرّة بعد عدّة تشغيلات كشيئين منفصلين، ولا تخلطهما في الجدول نفسه.
- كيف ينبغي تصميم قياس يعبر اللغات؟
- بنية من طبقتين موصى بها. القياس داخل اللغة يستخدم حزمة تناسب تلك اللغة: BenchmarkDotNet (C#)، وJMH (Java)، وgo test -bench مع benchstat (Go)، وGoogle Benchmark (C++). في المقارنة العابرة للغات خطر صف نتائج الحزم كما هي، لذا الأمتن جعل كل تنفيذ ملف تشغيل بعقد CLI واحد، وجعل عدّاء مشترك خارجي يعشّئ ترتيب التنفيذ، ويفصل cold/warm، ويستخدم مجموعة البيانات نفسها، ويتحقّق من checksum، ويحفظ البيانات الخام.
- ما الذي ينبغي الانتباه إليه في قياس مصغّر لـ C++؟
- انتبه إلى فخ اختفاء العمل بسبب التحسين. إن حكم المترجم أن «لا أحد يستخدم ناتج هذا الحساب» فقد يحذف المعالجة نفسها، فتصير النتيجة ليست سرعة بل عدم فعل شيء. لذا مهم استخدام الناتج أو إخراج checksum أو وظيفة كبت التحسين في إطار القياس. وفروق الشروط تظهر في C++ كبيرة كما هي، لذا مهم جداً ذكر أي مترجم وبأي أعلام (-O3/O2، LTO، PGO وغيرها) وبأي STL قست.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.