
"بغض النظر عن مدى صعوبة المحاولة ، لا يمكنك أن تجعل حصان السباق من خنزير. ومع ذلك ، يمكنك صنع خنزير أسرع" (التعليق في شفرة المصدر لـ Emax)
يعلم الجميع حقيقة أن الخنازير لا تطير. كما أن الرأي الشائع بنفس القدر هو الرأي القائل بأن مترجمي البايت كود كأسلوب لتنفيذ لغات عالية المستوى لا يمكن تسريعهم دون استخدام التجميع الديناميكي الذي يستغرق وقتًا طويلاً.
في الجزء الثاني من سلسلة من المقالات حول مفسري البايت كود ، باستخدام مثال آلة افتراضية صغيرة مكدسة من FDA (Pig Virtual Machine) ، سأحاول إظهار أنه لم يتم فقدان كل شيء للخنازير المجتهد ذات الطموحات وأنه من الممكن التسريع في إطار (في الغالب) القياسي C عمل هؤلاء المترجمين على الأقل مرة ونصف.
الجزء الأول ، تمهيدي
الجزء الثاني ، التحسين (الحالي)
الجزء الثالث التطبيقي
خنزير صغير
دعونا نتعرف.
Piglet VM هي آلة عادية مكدسة تعتمد على مثال من الجزء الأول من سلسلة من المقالات. خنزيرنا يعرف نوع بيانات واحد فقط - كلمة آلة 64 بت ، ويتم إجراء جميع الحسابات (عدد صحيح) على المكدس بعمق أقصى يصل إلى 256 كلمة آلة. بالإضافة إلى المكدس ، فإن هذا الخنزير الصغير لديه ذاكرة عاملة تبلغ 65.536 كلمة آلة. يمكن وضع نتيجة تنفيذ البرنامج - كلمة آلة واحدة - في سجل النتائج ، أو ببساطة الإخراج إلى الإخراج القياسي (stdout).
يتم تخزين الحالة الكاملة في آلة Piglet VM في بنية واحدة:
static struct { uint8_t *ip; uint64_t stack[STACK_MAX]; uint64_t *stack_top; uint64_t memory[MEMORY_SIZE]; uint64_t result; } vm;
يتيح لنا ما سبق أن نعزو هذا الجهاز إلى أجهزة افتراضية منخفضة المستوى ، وكلها تقريبًا تقع على عاتق صيانة دورة البرنامج الرئيسية:
interpret_result vm_interpret(uint8_t *bytecode) { vm_reset(bytecode); for (;;) { uint8_t instruction = NEXT_OP(); switch (instruction) { case OP_PUSHI: { uint16_t arg = NEXT_ARG(); PUSH(arg); break; } case OP_ADD: { uint64_t arg_right = POP(); *TOS_PTR() += arg_right; break; } case OP_DONE: { return SUCCESS; } default: return ERROR_UNKNOWN_OPCODE; } } return ERROR_END_OF_STREAM; }
يوضح الكود أنه بالنسبة لكل كود تشغيل ، يجب على الخنزير:
- استرجاع كود التشغيل من تدفق التعليمات.
- تأكد من أن كود التشغيل في النطاق الصحيح لقيم كود التشغيل (تتم إضافة هذا المنطق بواسطة المترجم C عند إنشاء رمز التبديل).
- اذهب إلى تعليمات الجسم.
- استخرج وسيطات التعليمات من المكدس أو فك شفرة وسيطة التعليمات الموجودة مباشرة في الرمز البايت.
- قم بإجراء عملية.
- إذا كان هناك نتيجة للحساب ، ضعها على المكدس.
- انقل المؤشر من التعليمات الحالية إلى التالية.
الحمولة هنا في الفقرة الخامسة فقط ، والباقي هو حمل: فك التعليمات أو استرجاع التعليمات من المكدس (الفقرة 4) ، والتحقق من قيمة كود التشغيل (الفقرة 2) ، والعودة بشكل متكرر إلى بداية الحلقة الرئيسية والانتقال الشرطي اللاحق الذي يصعب التنبؤ به (الفقرة 3).
باختصار ، تجاوز الخنزير بوضوح مؤشر كتلة الجسم الموصى به ، وإذا أردنا جعله في الشكل ، فسيتعين علينا التعامل مع كل هذه التجاوزات.
لغة تجميع الخنازير ومنخل إراتوستينس
أولاً ، دعنا نقرر قواعد اللعبة.
إن كتابة برامج لآلة افتراضية مباشرة في لغة C فكرة سيئة ، لكن إنشاء لغة برمجة وقت طويل ، لذلك قررنا أن نقتصر على لغة تجميع أصبع.
يبدو البرنامج الذي يحسب مجموع الأرقام من 1 إلى 65536 في هذا المجمع شيئًا مثل هذا:
# sum numbers from 1 to 65535 # init the current sum and the index PUSHI 1 PUSHI 1 # stack s=1, i=1 STOREI 0 # stack: s=1 # routine: increment the counter, add it to the current sum incrementandadd: # check if index is too big LOADI 0 # stack: s, i ADDI 1 # stack: s, i+1 DUP # stack: s, i+1, i+1 GREATER_OR_EQUALI 65535 # stack: s, i+1, 1 or 0 JUMP_IF_TRUE done # stack: s, i+1 DUP # stack: s, i+1, i+1 STOREI 0 # stack: s, i+1 ADD # stack: s+i+1 JUMP incrementandadd done: DISCARD PRINT DONE
ليس Python ، بالطبع ، ولكن هناك كل ما تحتاجه لسعادة الخنزير: التعليقات والعلامات والقفزات الشرطية وغير المشروطة لهم ، والتذكير بالتعليمات ، والقدرة على تحديد الحجج المباشرة للتعليمات.
كاملة مع آلة "Piglet VM" يتم تجميعها وتفكيكها ، وهي شجاعة في الروح ولديها الكثير من وقت الفراغ ، يمكن للقراء الاختبار بشكل مستقل في المعركة.
تتراكم الأرقام بسرعة كبيرة ، لذلك لاختبار الأداء ، كتبت برنامجًا آخر - تنفيذ ساذج لمصفاة إراتوستينس .
في الواقع ، يعمل الخنزير الصغير بسرعة كبيرة على أي حال (تعليماته قريبة من تعليمات الآلة) ، لذلك ، للحصول على نتائج واضحة ، سأفعل كل قياس لمائة بدء من البرنامج.
يعمل الإصدار الأول من خنزيرنا غير المحسن على النحو التالي:
> ./pigletvm runtimes test/sieve-unoptimized.bin 100 > /dev/null PROFILE: switch code finished took 545ms
نصف ثانية! المقارنة بالتأكيد غير نزيهة ، ولكن نفس خوارزمية Python تجعل مائة تشغيل أبطأ قليلاً:
> python test/sieve.py > /dev/null 4.66692185402
4.5 ثانية ، أو أبطأ تسع مرات. يجب أن نشيد بالخنزير الصغير - لديه القدرة! حسنًا ، دعنا الآن نرى ما إذا كان خنزيرنا يستطيع ضخ الصحافة.

التمرين الأول: تعليمات فائقة ثابتة
القاعدة الأولى للرمز السريع هي عدم القيام بالكثير من العمل. القاعدة الثانية للرمز السريع هي عدم القيام بالكثير من العمل. ما نوع العمل الإضافي الذي تقوم به Piglet VM؟
الملاحظة الأولى: يظهر التنميط برنامجنا أن هناك سلسلة من التعليمات أكثر شيوعًا من غيرها. لن نعذب خنزيرنا كثيرًا ونقتصر على بضع تعليمات فقط:
- LOADI 0، ADD - ضع رقمًا على المكدس من الذاكرة على العنوان 0 وأضفه إلى الرقم الموجود في الجزء العلوي من المكدس.
- PUSHI 65536 ، GREATER_OR_EQUAL - ضع رقمًا على المكدس وقارنه مع الرقم الذي كان موجودًا من قبل في الجزء العلوي من المكدس ، مع إعادة نتيجة المقارنة (0 أو 1) إلى المكدس.
- PUSHI 1 ، ADD - ضع رقمًا على المكدس ، وأضفه إلى الرقم الذي كان موجودًا من قبل في الجزء العلوي من المكدس ، وأعد نتيجة الإضافة إلى المكدس.
هناك أكثر من 20 تعليمات بقليل في جهاز Piglet VM ، ويتم استخدام بايت كامل للترميز - 256 قيمة. تقديم تعليمات جديدة ليست مشكلة. ماذا سنفعل:
for (;;) { uint8_t instruction = NEXT_OP(); switch (instruction) { case OP_LOADADDI: { uint16_t addr = NEXT_ARG(); uint64_t val = vm.memory[addr]; *TOS_PTR() += val; break; } case OP_GREATER_OR_EQUALI:{ uint64_t arg_right = NEXT_ARG(); *TOS_PTR() = PEEK() >= arg_right; break; } case OP_ADDI: { uint16_t arg_right = NEXT_ARG(); *TOS_PTR() += arg_right; break; } }
لا شيء معقد. دعونا نرى ما جاء منها:
> ./pigletvm runtimes test/sieve.bin 100 > /dev/null PROFILE: switch code finished took 410ms
واو! الرمز مخصص لثلاث تعليمات جديدة فقط ، وقد فزنا بمائة ونصف ميلي ثانية!
يتم تحقيق المكاسب هنا نظرًا لحقيقة أن أصبعنا لا يقوم بحركات غير ضرورية عند تنفيذ مثل هذه التعليمات: لا يقع خيط التنفيذ في الحلقة الرئيسية ، ولا يتم فك ترميز أي شيء ، ولا تمر حجج التعليمات من خلال المكدس مرة أخرى.
وهذا ما يسمى تعليمات فائقة ثابتة ، حيث يتم تعريف تعليمات إضافية بشكل ثابت ، أي بواسطة مبرمج الآلة الافتراضية في مرحلة التطوير. هذه تقنية بسيطة وفعالة تستخدمها جميع الأجهزة الافتراضية للغات البرمجة بشكل أو بآخر.
تتمثل المشكلة الرئيسية في التعليمات الفائقة الثابتة في أنه بدون برنامج معين ، من المستحيل تحديد التعليمات التي يجب دمجها. تستخدم البرامج المختلفة تسلسلات مختلفة من التعليمات ، ويمكنك معرفة هذه التسلسلات فقط في مرحلة تشغيل رمز معين.
يمكن أن تكون الخطوة التالية هي التجميع الديناميكي للتعليمات الفائقة في سياق برنامج معين ، أي التركيبات الفائقة الديناميكية (في التسعينيات وأوائل الألفية الثانية ، لعبت هذه التقنية دور تجميع JIT البدائي).
من المستحيل إنشاء تعليمات على الطاير في إطار C العادي ، ولا يعتبر خنزيرنا هذا حقًا منافسة صادقة. لحسن الحظ ، لدي تمارين أفضل له.
التمرين الثاني: التحقق من نطاق قيم كود التشغيل
باتباع قواعد الشفرة السريعة ، نسأل أنفسنا مرة أخرى السؤال الأبدي: ما الذي لا يمكنك فعله؟
عندما تعرفنا على جهاز جهاز Piglet VM ، أدرجت جميع الإجراءات التي يقوم بها الجهاز الظاهري لكل رمز تشغيل. والنقطة 2 (التحقق من قيمة كود التشغيل لتناسب النطاق الصحيح لقيم التبديل) هي الأكثر إثارة للريبة.
دعونا نلقي نظرة على كيفية قيام مجلس التعاون الخليجي بتجميع بنية التبديل:
- يتم إنشاء جدول انتقالي ، أي جدول يعرض قيمة كود التشغيل إلى عنوان الكود الذي ينفذ نص التعليمات.
- يتم إدراج رمز يتحقق مما إذا كان رمز التشغيل المستلم يقع ضمن نطاق جميع قيم التبديل الممكنة ويرسله إلى التسمية الافتراضية إذا لم يكن هناك معالج لرمز التشغيل.
- يتم إدراج التعليمات البرمجية التي تنتقل إلى المعالج.
ولكن لماذا التحقق من فاصل القيم لكل تعليمة؟ نحن نعتقد أن كود التشغيل إما صحيح - ينهي التنفيذ بواسطة تعليمات OP_DONE ، أو غير صحيح - يتجاوز رمز البايت. يتم تمييز ذيل دفق opcodes بصفر ، والصفر هو كود op الخاص لتعليمات OP_ABORT ، والذي يكمل تنفيذ الرمز الفرعي مع وجود خطأ.
اتضح أن هذا الاختيار غير مطلوب على الإطلاق! ويجب أن يتمكن الخنزير الصغير من نقل هذه الفكرة إلى المترجم. دعنا نحاول إصلاح المفتاح الرئيسي قليلاً:
uint8_t instruction = NEXT_OP(); switch (instruction & 0x1f) { case 26 ... 0x1f: { return ERROR_UNKNOWN_OPCODE; } }
مع العلم بأن لدينا 26 تعليمات فقط ، فإننا نفرض قناع بت (القيمة الثمانية 0x1f هي 0b11111 ثنائية تغطي نطاق القيم من 0 إلى 31) على كود التشغيل وإضافة معالجات إلى القيم غير المستخدمة في النطاق من 26 إلى 31.
تعليمات بت هي بعض من أرخص في بنية x86 ، وهي بالتأكيد أرخص من الفروع الشرطية الإشكالية مثل الفرع الذي يستخدم التحقق الفاصل. من الناحية النظرية ، يجب أن نفوز بعدة دورات على كل تعليمات قابلة للتنفيذ إذا كان المترجم يفهم تلميحنا.
بالمناسبة ، طريقة تحديد نطاق القيم في الحالة ليست قياسية C ، ولكن امتداد GCC. ولكن لأغراضنا ، يعد هذا الرمز مناسبًا ، خاصة أنه ليس من الصعب إعادة تشكيله في عدة معالجات لكل من القيم غير الضرورية.
نحن نحاول:
> ./pigletvm runtimes test/sieve.bin 100 > /dev/null PROFILE: switch code finished took 437ms PROFILE: switch code (no range check) finished took 383ms
50 مللي ثانية أخرى! خنزير صغير ، وكأنك سمعت نفسك في كتفيك! ..
التمرين الثالث: الممرات
ما هي التمارين الأخرى التي يمكن أن تساعد خنزيرنا الصغير؟ أكبر توفير للوقت بفضل التعليمات الفائقة. وهي تقلل من عدد المخارج للدورة الرئيسية وتسمح لك بالتخلص من التكاليف العامة المقابلة.
المفتاح المركزي هو نقطة المشكلة الرئيسية لأي معالج مع تنفيذ استثنائي للتعليمات. لقد تعلم المنبئون بالفرع الحديث التنبؤ حتى بهذه التحولات غير المباشرة المعقدة بشكل جيد ، ولكن نقاط الفرع "التلطيخ" على طول الكود يمكن أن تساعد المعالج على التحول بسرعة من التعليمات إلى التعليمات.
مشكلة أخرى هي قراءة بايت بايت بايت opcodes التعليمات والحجج المباشرة من bytecode. تعمل الآلات المادية بكلمة آلة 64 بت ولا تحبها حقًا عندما تعمل الشفرة بقيم أقل.
غالبًا ما تعمل برامج التحويل البرمجي مع الكتل الأساسية ، أي تسلسل التعليمات بدون فروع وملصقات بداخلها. تبدأ الكتلة الأساسية إما من بداية البرنامج أو من الملصق ، وتنتهي بنهاية البرنامج أو التفرع الشرطي أو الانتقال المباشر إلى الملصق الذي يبدأ الكتلة الأساسية التالية.
هناك العديد من المزايا للعمل مع الوحدات الأساسية ، ولكن خنزيرنا مهتم بميزته الرئيسية: يتم تنفيذ التعليمات داخل الوحدة الأساسية بالتتابع. سيكون من الرائع عزل هذه الكتل الأساسية بطريقة أو بأخرى واتباع التعليمات الموجودة بها دون إضاعة الوقت في الدخول إلى الحلقة الرئيسية.
في حالتنا ، يمكنك حتى توسيع تعريف الوحدة الأساسية إلى المسار. سيتضمن المسار من حيث آلة Piglet VM جميع الكتل الأساسية المتصلة بشكل تسلسلي (أي باستخدام القفزات غير المشروطة).
بالإضافة إلى التنفيذ المتسلسل للتعليمات ، سيكون من الجيد فك شفرة الحجج المباشرة للتعليمات مقدمًا.
يبدو كل شيء مخيفًا جدًا ويشبه تجميعًا ديناميكيًا ، قررنا عدم استخدامه. حتى أن الخنزير شك في قوته قليلاً ، ولكن من الناحية العملية لم يكن الأمر سيئًا للغاية.
دعونا نفكر أولاً في كيف يمكنك تخيل التعليمات المضمنة في المسار:
struct scode { uint64_t arg; trace_op_handler *handler; };
هنا arg هي الوسيطة التي تم فك تشفيرها مسبقًا للإرشادات ، والمعالج هو مؤشر للدالة التي تنفذ منطق التعليمات.
الآن تبدو طريقة عرض كل أثر كما يلي:
typedef scode trace[MAX_TRACE_LEN];
أي أن التتبع عبارة عن سلسلة من رموز s ذات الطول المحدود. ذاكرة التخزين المؤقت للتتبع نفسها داخل الجهاز الظاهري تبدو كما يلي:
trace trace_cache[MAX_CODE_LEN];
هذه مجرد مجموعة من الآثار ذات طول لا يتجاوز الطول المحتمل لرمز بايت. الحل كسول ، من أجل توفير الذاكرة ، من المنطقي استخدام جدول تجزئة.
في بداية المترجم ، سيقوم المعالج الأول لكل أثر بتجميع نفسه:
for (size_t trace_i = 0; trace_i < MAX_CODE_LEN; trace_i++ ) vm_trace.trace_cache[trace_i][0].handler = trace_compile_handler;
تبدو حلقة المترجم الرئيسية الآن كما يلي:
while(vm_trace.is_running) { scode *code = &vm_trace.trace_cache[vm_trace.pc][0]; code->handler(code); }
يعد مترجم التتبع أكثر تعقيدًا قليلاً ، بالإضافة إلى بناء تتبع بدءًا من التعليمات الحالية ، فإنه يقوم بما يلي:
static void trace_compile_handler(scode *trace_head) { scode *trace_tail = trace_head; trace_head->handler(trace_head); }
معالج التعليمات العادي:
static void op_add_handler(scode *code) { uint64_t arg_right = POP(); *TOS_PTR() += arg_right; code++; code->handler(code); }
يقوم المعالج الذي لا يقوم بإجراء أي مكالمات في نهاية الوظيفة بإنهاء كل أثر:
static void op_done_handler(scode *code) { (void) code; vm_trace.is_running = false; vm_trace.error = SUCCESS; }
كل هذا ، بالطبع ، أكثر تعقيدًا من إضافة تعليمات فائقة ، لكن دعنا نرى ما إذا كان ذلك أعطانا أي شيء:
> ./pigletvm runtimes test/sieve.bin 100 > /dev/null PROFILE: switch code finished took 427ms PROFILE: switch code (no range check) finished took 395ms PROFILE: trace code finished took 367ms
مرحى ، 30 مللي ثانية أخرى!
كيف ذلك؟ بدلاً من التنقل ببساطة عبر التسميات ، نقوم بعمل سلاسل مكالمات لمعالجات التعليمات ، ونقضي بعض الوقت في المكالمات وتمرير الحجج ، لكن أصبعنا لا يزال يعمل على المسارات بشكل أسرع من المفتاح البسيط مع تسمياته.
يتم تحقيق هذا المكاسب في أداء المسار بسبب ثلاثة عوامل:
- من السهل توقع الفروع المنتشرة في أماكن مختلفة في المدونة.
- يتم دائمًا ترميز حجج المعالجات مسبقًا في كلمة آلية كاملة ، ويتم ذلك مرة واحدة فقط - أثناء تجميع التتبع.
- يقوم المترجم بتحويل سلاسل الوظائف إلى مكالمة واحدة إلى وظيفة المعالج الأول ، وهو أمر ممكن بسبب تحسين المكالمة الخلفية .
قبل تلخيص نتائج تدريبنا ، قررت أنا والخنزير الصغير تجربة تقنية قديمة أخرى لتفسير البرامج - رمز مخيط.
التمرين الرابع: كود "مخيط"
سمع أي خنزير مهتم بتاريخ المترجمين الشفرين رمزًا مترابطًا. هناك العديد من الخيارات لهذه التقنية ، ولكن جميعها تختصر بدلاً من المرور عبر مجموعة من رموز opcodes ، على سبيل المثال ، إشارات إلى الوظائف أو التسميات ، متابعتها مباشرة دون رمز op opmediate متوسط.
وظائف الاتصال هي عمل مكلف ولا معنى له هذه الأيام ؛ معظم الإصدارات الأخرى من كود الخياطة غير قابلة للتحقيق في إطار المعيار C. حتى التقنية ، التي ستتم مناقشتها أدناه ، تستخدم الامتدادات واسعة النطاق ، غير القياسية - C إلى التسميات.
في إصدار رمز مخيط (الرمز المميز للغة الإنجليزية) الذي اخترته لتحقيق أهداف الخنزير الخاصة بنا ، نقوم بحفظ الرمز الفرعي ، ولكن قبل بدء التفسير ، نقوم بإنشاء جدول يعرض شفرات التعليمات إلى عنوان تسميات معالج التعليمات:
const void *labels[] = { [OP_PUSHI] = &&op_pushi, [OP_LOADI] = &&op_loadi, [OP_LOADADDI] = &&op_loadaddi, [OP_STORE] = &&op_store, [OP_STOREI] = &&op_storei, [OP_LOAD] = &&op_load, [OP_DUP] = &&op_dup, [OP_DISCARD] = &&op_discard, [OP_ADD] = &&op_add, [OP_ADDI] = &&op_addi, [OP_SUB] = &&op_sub, [OP_DIV] = &&op_div, [OP_MUL] = &&op_mul, [OP_JUMP] = &&op_jump, [OP_JUMP_IF_TRUE] = &&op_jump_if_true, [OP_JUMP_IF_FALSE] = &&op_jump_if_false, [OP_EQUAL] = &&op_equal, [OP_LESS] = &&op_less, [OP_LESS_OR_EQUAL] = &&op_less_or_equal, [OP_GREATER] = &&op_greater, [OP_GREATER_OR_EQUAL] = &&op_greater_or_equal, [OP_GREATER_OR_EQUALI] = &&op_greater_or_equali, [OP_POP_RES] = &&op_pop_res, [OP_DONE] = &&op_done, [OP_PRINT] = &&op_print, [OP_ABORT] = &&op_abort, };
انتبه للرموز & - فهذه مؤشرات على التسميات مع نصوص التعليمات ، الامتداد غير القياسي لدول مجلس التعاون الخليجي.
لبدء تنفيذ الرمز ، ما عليك سوى النقر على المؤشر المقابل لرمز التشغيل الأول للبرنامج:
goto *labels[NEXT_OP()];
لا توجد دورة هنا ولن تكون هناك ، فكل من التعليمات نفسها تقفز إلى المعالج التالي:
op_pushi: { uint16_t arg = NEXT_ARG(); PUSH(arg); goto *labels[NEXT_OP()]; }
إن عدم وجود مفتاح "ينتشر" نقاط الفرع على طول هيئات التعليمات ، والتي من الناحية النظرية يجب أن تساعد الفرع على التنبؤ في حالة التنفيذ غير العادي للتعليمات. يبدو الأمر كما لو قمنا ببناء المفتاح مباشرةً في التعليمات وقمنا بتشكيل جدول انتقال يدويًا.
هذه هي التقنية بأكملها. لقد أحببت الخنزير الصغير لبساطته. دعونا نرى ما يحدث في الممارسة:
> ./pigletvm runtimes test/sieve.bin 100 > /dev/null PROFILE: switch code finished took 443ms PROFILE: switch code (no range check) finished took 389ms PROFILE: threaded code finished took 477ms PROFILE: trace code finished took 364ms
عفوًا! هذا هو أبطأ تقنياتنا! ماذا حدث؟ دعونا نجري نفس الاختبارات ، بإيقاف جميع التحسينات في دول مجلس التعاون الخليجي:
> ./pigletvm runtimes test/sieve.bin 100 > /dev/null PROFILE: switch code finished took 969ms PROFILE: switch code (no range check) finished took 940ms PROFILE: threaded code finished took 824ms PROFILE: trace code finished took 1169ms
هنا ، يعمل رمز مخيط بشكل أفضل.
تلعب ثلاثة عوامل دورًا هنا:
- المحول البرمجي الأمثل سوف يبني جدول تحويل ليس أسوأ من لوحة الملصقات اليدوية.
- يتخلص المترجمون الحديثون بشكل ملحوظ من مكالمات الوظائف الإضافية.
- بدءًا من جيل Haswell من معالجات Intel ، تعلم متنبئو الفروع التنبؤ بدقة بالتحولات عبر نقطة فرع واحدة.
وفقًا للذاكرة القديمة ، لا تزال هذه التقنية مستخدمة في رمز ، على سبيل المثال ، مترجم Python VM ، ولكن ، بصراحة ، هذه الأيام أصبحت بالفعل قديمة.
دعونا أخيرا نلخص ونقيم النجاحات التي حققها خنزيرنا.
استخلاص المعلومات

لست متأكدًا مما يمكن تسميته رحلة ، ولكن دعنا نواجهها ، فقد قطعنا أصبعًا بعيدًا من 550 مللي ثانية لمئات الجري على "المنخل" إلى 370 ميلي ثانية أخيرة. استخدمنا تقنيات مختلفة: تعليمات فائقة ، والتخلص من فحص فترات القيم ، والميكانيكا المعقدة للآثار ، وأخيرًا ، حتى كود مخيط. في الوقت نفسه ، بشكل عام ، تصرفنا في إطار الأشياء المنفذة في جميع المترجمين المشهورين لـ C. يعتبر التسارع مرة ونصف ، كما يبدو لي ، نتيجة جيدة ، ويستحق الخنزير الصغير جزءًا إضافيًا من النخالة في الحوض الصغير.
أحد الشروط الضمنية التي وضعناها لأنفسنا مع الخنزير هو الحفاظ على بنية المكدس لآلة Piglet VM. يقلل الانتقال إلى تسجيل البنية ، كقاعدة عامة ، من عدد التعليمات اللازمة لمنطق البرامج ، وبالتالي ، يمكن أن يساعد في التخلص من المخارج غير الضرورية لمدير التعليمات. أعتقد أنه يمكن قطع 10-20٪ أخرى من الوقت على هذا.
شرطنا الرئيسي - عدم وجود تجميع ديناميكي - ليس أيضًا قانونًا للطبيعة. إن ضخ الخنزير بالستيرويدات على شكل تجميع JIT أمر سهل للغاية هذه الأيام: في مكتبات مثل GNU Lightning أو LibJIT ، تم بالفعل تنفيذ جميع الأعمال القذرة. لكن وقت التطوير والمقدار الإجمالي من التعليمات البرمجية حتى باستخدام المكتبات ينمو بشكل هائل.
هناك بالطبع حيل أخرى لم يصل إليها خنزيرنا الصغير إلى الحافر. , — - — - . , .
PS , , , , ( https://www.instagram.com/vovazomb/ ), .
PPS , . true-grue - — PigletC . !
PPPS iliazeus : . ; . .