في إحدى المقالات السابقة ، تحدثت عن كيفية بناء برنامج تنفيذي لجهاز مكدس افتراضي باستخدام أساليب البرمجة الوظيفية والموجهة نحو اللغة. اقترحت البنية الرياضية للغة الهيكل الأساسي لتنفيذ مترجمها ، بناءً على مفهوم المجموعات شبه الأحادية. سمح لي هذا النهج ببناء تنفيذ جميل وقابل للتوسيع وكسر التصفيق ، لكن السؤال الأول من الجمهور جعلني أترك المنصة وأتسلق إلى إيماكس مرة أخرى.
لقد أجريت اختبارًا بسيطًا وتأكدت من أن الجهاز الظاهري يعمل بذكاء في المهام البسيطة التي تستخدم المكدس فقط ، وعند استخدام "الذاكرة" - وهي مجموعة ذات وصول عشوائي - تبدأ مشاكل كبيرة. حول كيف تمكنا من حلها دون تغيير المبادئ الأساسية لبنية البرنامج وتحقيق تسارع ألف مرة للبرنامج ، وسوف نناقش في هذه المقالة.
هاسكل هي لغة غريبة ذات مكانة خاصة. كان الهدف الرئيسي من إنشائه وتطويره هو الحاجة إلى lingua franca للتعبير عن أفكار البرمجة الوظيفية واختبارها. وهذا يبرر أكثر سماته اللافتة للنظر: الكسل والنقاء الشديد والتركيز على الأنواع والتلاعب بها. لم يتم تصميمه للتطوير اليومي ، وليس للبرمجة الصناعية ، وليس للاستخدام على نطاق واسع. حقيقة أنه يستخدم حقًا لإنشاء مشاريع واسعة النطاق في صناعة الشبكات ومعالجة البيانات هي النية الحسنة للمطورين ، دليل على المفهوم ، إذا أردت. ولكن حتى الآن ، المنتج الأكثر أهمية والأكثر استخدامًا وقوة بشكل مثير للدهشة المكتوب في Haskell هو ... مترجم ghc. وهذا مبرر تمامًا من وجهة نظره - أن يكون أداة للبحث في مجال علوم الكمبيوتر. إن المبدأ الذي أعلنه سيمون بايتون جونسون: "تجنب النجاح مهما كان الثمن" ضروري لكي تظل اللغة مثل هذه الأداة. هاسكل مثل غرفة معقمة في مختبر مركز بحثي لتطوير تقنيات أشباه الموصلات أو المواد النانوية. من غير المريح للغاية العمل فيه ، وبالنسبة للممارسة اليومية ، فإنه يقيد أيضًا حرية العمل ، ولكن بدون هذه المضايقات ، دون الالتزام الصارم بالقيود ، لن يكون من الممكن مراقبة ودراسة الآثار الدقيقة التي ستصبح لاحقًا أساسًا للتطورات الصناعية. في الوقت نفسه ، لن تكون هناك حاجة إلى تعقيم الصناعة إلا في الحجم الأكثر ضرورة ، وستظهر نتائج هذه التجارب في جيوبنا في شكل أدوات. نحن ندرس النجوم والمجرات ليس لأننا نتوقع الحصول على فوائد مباشرة منها ، ولكن لأنه على نطاق هذه الأشياء غير العملية ، تصبح التأثيرات الكمية والنسبية ملحوظة ودراسة ، لدرجة أنه يمكننا لاحقًا استخدام هذه المعرفة لتطوير شيء مفيد جدًا. لذا ، فإن Haskell بخطوطها "الخاطئة" ، والكسل غير العملي للحسابات ، وصلابة بعض خوارزميات الاستدلال النوعي ، مع منحنى إدخال حاد للغاية ، لا تسمح لك أخيرًا بإنشاء تطبيق ذكي على الركبة أو نظام تشغيل. ومع ذلك ، فإن العدسات والموناد والتحليل التوافقي والاستخدام الواسع النطاق للأحاديات وطرق إثبات النظرية التلقائية ومديري الحزم الوظيفية التعريفية والأنواع الخطية والتابعة تعتمد على العالم العملي. يجد هذا التطبيق في ظروف أقل تعقيدًا باللغات Python و Scala و Kotlin و F # و Rust وغيرها الكثير. لكنني لن أستخدم أيًا من هذه اللغات الرائعة لتعليم مبادئ البرمجة الوظيفية: سوف آخذ الطالب إلى المختبر ، وأظهر كيف يعمل في أمثلة مشرقة ونظيفة ، وبعد ذلك يمكنك رؤية هذه المبادئ بشكل عملي في المصنع آلة كبيرة وغير مفهومة ، ولكن سريعة للغاية. تجنب النجاح مهما كان الثمن هو الحماية من محاولات وضع آلة صنع القهوة في المجهر الإلكتروني من أجل الترويج لها. وفي المسابقات التي تكون فيها اللغة أكثر برودة ، سيكون هاسكل دائمًا خارج الترشيحات المعتادة.
ومع ذلك ، فإن الشخص ضعيف ، كما يعيش شيطان في داخلي ، مما يجعلني أرغب في مقارنة "لغتي المفضلة" وتقييمها والدفاع عنها أمام الآخرين. لذا ، بعد أن كتبت تنفيذًا أنيقًا لآلة مكدسة تستند إلى تركيبة أحادية ، لغرض وحيد هو معرفة ما إذا كانت هذه الفكرة تناسبني ، شعرت بالضيق على الفور لأنني أدركت أن التنفيذ كان رائعًا ، ولكنه غير فعال بشكل رهيب! يبدو الأمر كما لو أنني سأستخدمه حقًا في المهام الجادة ، أو لبيع جهازي المكدس في نفس السوق حيث يتم تقديم أجهزة Python أو Java الافتراضية. لكن لعنة ذلك ، مقالة الخنزير الصغير التي بدأت بها هذه المحادثة بأكملها تعطي مثل هذه الأرقام اللذيذة: مئات المللي ثانية للخنزير الصغير ، والثواني لـ Python ... ولا يستطيع أحادي الجميل التعامل مع نفس المهمة في غضون ساعة! علي أن أنجح! المجهر الخاص بي سوف يخمر الإسبريسو ليس أسوأ من آلة قهوة في مدخل المعهد! يمكن تفريق قصر كريستال وإطلاقه في الفضاء!
لكن ما الذي أنت مستعد للتخلي عنه ، يسألني عالم الرياضيات؟ نقاء وشفافية عمارة القصر؟ المرونة والقابلية للتوسع التي توفرها الأشكال المتجانسة من البرامج إلى الحلول الأخرى؟ الشيطان والملاك كلاهما عنيد ، واقترح الطاوي الحكيم ، الذي أسمح لنفسي أن أعيشه ، أن أسلك الطريق الذي يناسبهما ، وأن نتبعه لأطول فترة ممكنة. ومع ذلك ، ليس بهدف تحديد الفائز ، ولكن من أجل معرفة المسار نفسه ، ومعرفة مدى تقدمه ، واكتساب خبرة جديدة. وهكذا عرفت الحزن الباطل وفرحة التحسين.
قبل أن نبدأ ، نضيف أن مقارنات اللغات من حيث الفعالية لا معنى لها. تحتاج إلى مقارنة المترجمين (مترجمين شفويين أو مترجمين) ، أو أداء المبرمج الذي يستخدم اللغة. في النهاية ، من السهل دحض التأكيد على أن برامج C هي الأسرع من خلال كتابة مترجم C كامل في BASIC ، على سبيل المثال. لذلك ، نحن لا نقارن هاسكل وجافا سكريبت ، ولكن أداء البرامج التي ينفذها مترجم يجمعها ghc
والبرامج المنفذة ، على سبيل المثال ، في متصفح معين. تأتي جميع مصطلحات الخنازير من مقالة ملهمة عن الآلات المكدسة. يمكن دراسة جميع كود هاسكل المصاحب للمقال في المستودع .
نترك منطقة الراحة
سيكون موقع البدء هو تنفيذ آلة مكدس أحادية في شكل EDSL - لغة بسيطة صغيرة تسمح بدمج عشرين بدائيين لتقديم برامج لجهاز مكدس افتراضي. بمجرد دخوله في المقالة الثانية ، نعطيه الاسم monopig
. إنه يعتمد على حقيقة أن لغات الآلات المكدسة تشكل أحاديًا مع عملية تسلسل وعملية فارغة كوحدة. وفقًا لذلك ، فقد تم بناؤه بنفسه في شكل تحول أحادي لحالة الماكينة. تتضمن الحالة كومة وذاكرة في شكل ناقل (هيكل يوفر وصولًا عشوائيًا للعناصر) ، وعلامة توقف طارئة ، وبطارية أحادية لتراكم معلومات التصحيح. ينتقل هذا الهيكل بأكمله على طول سلسلة من الأشكال الداخلية من العملية إلى العملية ، وتنفيذ عملية حسابية. تم بناء بنية متشابهة لرموز البرنامج من البنية التي تشكلها البرامج ، ومنه التماثلات في هياكل مفيدة أخرى تمثل متطلبات البرنامج من حيث عدد الحجج والذاكرة. كانت المرحلة النهائية من البناء هي إنشاء أحاديات التحويل في فئة Claysley ، والتي تتيح لك غمر الحسابات في monad تعسفي. لذلك حصلت الآلة على إمكانيات إخراج المدخلات والحسابات الغامضة. سنبدأ بهذا التنفيذ. يمكن العثور على رمزها هنا .
سنختبر فعالية البرنامج على التنفيذ الساذج لمنخل إراتوستينس ، الذي يملأ الذاكرة (المصفوفة) بالأصفار والأخرى ، مع الإشارة إلى الأعداد الأولية بصفر. نقدم الكود الإجرائي للخوارزمية في javascript
:
var memSize = 65536; var arr = []; for (var i = 0; i < memSize; i++) arr.push(0); function sieve() { var n = 2; while (n*n < memSize) { if (!arr[n]) { var k = n; while (k < memSize) { k+=n; arr[k] = 1; } } n++; } }
تم تحسين الخوارزمية على الفور قليلاً. يزيل المشي السيئ من خلال خلايا الذاكرة الممتلئة بالفعل. لم يوافق ملاكي الرياضي على نسخة ساذجة حقًا من مثال في مشروع PorosenokVM
، نظرًا لأن هذا التحسين يكلف خمسة إرشادات فقط من لغة المكدس. إليك كيفية ترجمة الخوارزمية إلى monopig
:
sieve = push 2 <> while (dup <> dup <> mul <> push memSize <> lt) (dup <> get <> branch mempty fill <> inc) <> pop fill = dup <> dup <> add <> while (dup <> push memSize <> lt) (dup <> push 1 <> swap <> put <> exch <> add) <> pop
وإليك كيفية كتابة تطبيق مكافئ لهذه الخوارزمية على لغة Haskell الاصطلاحية ، باستخدام نفس الأنواع الموجودة في monopig
:
sieve' :: Int -> Vector Int -> Vector Int sieve' km | k*k < memSize = sieve' (k+1) $ if m ! k == 0 then fill' k (2*k) m else m | otherwise = m fill' :: Int -> Int -> Vector Int -> Vector Int fill' knm | n < memSize = fill' k (n+k) $ m // [(n,1)] | otherwise = m
يستخدم نوع Data.Vector
وأدوات للعمل معه ، وهي ليست شائعة جدًا للعمل اليومي في Haskell. التعبير m ! k
m ! k
بإرجاع العنصر m ! k
للمتجه m
، و m // [(n,1)]
- يعين العنصر برقم n
إلى 1. أكتب هذا هنا لأنني اضطررت للذهاب إليهم للمساعدة ، على الرغم من أنني أعمل في Haskell كل يوم تقريبًا. والحقيقة هي أن الهياكل ذات الوصول العشوائي في تنفيذ وظيفي غير فعالة ولهذا السبب غير محبوب.
وفقًا لشروط المنافسة المحددة في المقالة حول الخنزير الصغير ، يتم تشغيل الخوارزمية 100 مرة. وللتخلص من جهاز كمبيوتر معين ، دعنا نقارن سرعات التنفيذ لهذه البرامج الثلاثة ، ونحيلها إلى أداء برنامج javascript
الذي تم تشغيله في Chrome.

رعب رعب !!! لا يتباطأ monopig
فقط monopig
إله ، لذا فإن النسخة الأصلية ليست أفضل بكثير! هاسكل ، بالطبع ، رائع ، ولكن ليس أقل شأنا من برنامج يعمل في المتصفح؟! كما يعلمنا المدربون ، لا يمكنك العيش هكذا ، لقد حان الوقت لمغادرة منطقة الراحة التي يوفرها لنا هاسكل!
التغلب على الكسل
هيا بنا للقيام بذلك ، قم بتجميع برنامج على monopig
مع علامة -rtsopts
إحصائيات وقت التشغيل ومعرفة ما نحتاجه لتشغيل غربال إراتوستينس مرة واحدة:
$ ghc -O -rtsopts ./Monopig4.hs [1 of 1] Compiling Main ( Monopig4.hs, Monopig4.o ) Linking Monopig4 ... $ ./Monopig4 -RTS -sstderr "Ok" 68,243,040,608 bytes allocated in the heap 6,471,530,040 bytes copied during GC 2,950,952 bytes maximum residency (30667 sample(s)) 42,264 bytes maximum slop 15 MB total memory in use (7 MB lost due to fragmentation) Tot time (elapsed) Avg pause Max pause Gen 0 99408 colls, 0 par 2.758s 2.718s 0.0000s 0.0015s Gen 1 30667 colls, 0 par 57.654s 57.777s 0.0019s 0.0133s INIT time 0.000s ( 0.000s elapsed) MUT time 29.008s ( 29.111s elapsed) GC time 60.411s ( 60.495s elapsed) <-- ! EXIT time 0.000s ( 0.000s elapsed) Total time 89.423s ( 89.607s elapsed) %GC time 67.6% (67.5% elapsed) Alloc rate 2,352,591,525 bytes per MUT second Productivity 32.4% of total user, 32.4% of total elapsed
يخبرنا السطر الأخير أن البرنامج شارك في الحوسبة الإنتاجية فقط ثلث الوقت. لبقية الوقت ، كان جامع القمامة يعمل من الذاكرة وينظف للحسابات الكسولة. كم مرة قيل لنا في الطفولة أن الكسل ليس جيدًا! هنا ، تسببت لنا الميزة الرئيسية لـ Haskell في إحداث ضرر ، في محاولة لإنشاء عدة مليارات من التحويلات المؤجلة والتكدس.
يرفع ملاك عالم الرياضيات في هذه المرحلة إصبعه ويتحدث بسعادة عن حقيقة أنه منذ عهد كنيسة ألونزو ، هناك نظرية تنص على أن استراتيجية الحساب لا تؤثر على نتيجتها ، مما يعني أننا أحرار في اختيارها لأسباب تتعلق بالكفاءة. ليس من الصعب على الإطلاق تغيير الحسابات - ضع إشارة !
في الإعلان عن نوع المكدس والذاكرة ، وبالتالي جعل هذه الحقول صارمة.
data VM a = VM { stack :: !Stack , status :: Maybe String , memory :: !Memory , journal :: !a }
لن نغير أي شيء آخر ونتحقق من النتيجة على الفور:
$ ./Monopig41 +RTS -sstderr "Ok" 68,244,819,008 bytes allocated in the heap 7,386,896 bytes copied during GC 528,088 bytes maximum residency (2 sample(s)) 25,248 bytes maximum slop 16 MB total memory in use (14 MB lost due to fragmentation) Tot time (elapsed) Avg pause Max pause Gen 0 129923 colls, 0 par 0.666s 0.654s 0.0000s 0.0001s Gen 1 2 colls, 0 par 0.001s 0.001s 0.0006s 0.0007s INIT time 0.000s ( 0.000s elapsed) MUT time 13.029s ( 13.048s elapsed) GC time 0.667s ( 0.655s elapsed) EXIT time 0.001s ( 0.001s elapsed) Total time 13.700s ( 13.704s elapsed) %GC time 4.9% (4.8% elapsed) Alloc rate 5,238,049,412 bytes per MUT second Productivity 95.1% of total user, 95.1% of total elapsed
نمت الإنتاجية بشكل ملحوظ. لا يزال إجمالي تكاليف الذاكرة مثيرًا للإعجاب نظرًا لعدم ثبات البيانات ، ولكن الأهم من ذلك ، الآن بعد أن حدنا من كسل البيانات ، فإن جامع القمامة لديه الفرصة ليكون كسولًا ، ولا يزال هناك 5 ٪ فقط من العمل عليه. أدخل إدخالًا جديدًا في التصنيف.

حسنًا ، لقد قربتنا الحسابات الصارمة من سرعة كود Haskell الأصلي ، والذي يبطئ بشكل مخجل دون أي آلة افتراضية. هذا يعني أن النفقات العامة لاستخدام ناقلات غير قابلة للتغيير تتجاوز بشكل كبير تكلفة صيانة آلة مكدسة. وهذا يعني أن الوقت قد حان لتوديع ثبات الذاكرة.
ترك التغييرات في الحياة
نوع Data.Vector
جيد ، ولكن باستخدامه ، فإننا نقضي الكثير من الوقت في النسخ ، باسم الحفاظ على نقاء عملية الحوسبة. Data.Vector.Unpacked
بنوع Data.Vector.Unpacked
على الأقل Data.Vector.Unpacked
على تغليف الهيكل ، ولكن هذا لا يغير الصورة بشكل أساسي. سيكون الحل الصحيح هو إزالة الذاكرة من حالة الجهاز وتوفير الوصول إلى المتجه الخارجي باستخدام فئة Kleisley. في نفس الوقت ، جنبًا إلى جنب مع المتجهات النقية ، يمكنك استخدام ما يسمى المتجهات Data.Vector.Mutable
) Data.Vector.Mutable
.
سنقوم بتوصيل الوحدات المناسبة ونفكر في كيفية التعامل مع البيانات القابلة للتغيير في برنامج وظيفي نظيف.
import Control.Monad.Primitive import qualified Data.Vector.Unboxed as V import qualified Data.Vector.Unboxed.Mutable as M
من المفترض أن يتم عزل هذه الأنواع القذرة من الجمهور النقي. يتم تضمينها في PrimMonad
لفئة PrimMonad
(بما في ذلك ST
أو IO
) ، حيث تقوم البرامج النظيفة بإدراج تعليمات الإجراءات بعناية مكتوبة بلغة وظيفية بلورية على الرق الثمين. وبالتالي ، فإن سلوك هذه الحيوانات النجسة يتحدد من خلال السيناريوهات الأرثوذكسية البحتة وليس خطيرًا. لا تستخدم جميع البرامج الخاصة بجهازنا الذاكرة ، لذلك ليست هناك حاجة لإدانة العمارة بالكامل إلى الانغماس في mono IO
. إلى جانب مجموعة فرعية نظيفة من لغة monopig
أربعة تعليمات توفر الوصول إلى الذاكرة ، ولن يتمكنوا سوى من الوصول إلى المنطقة الخطرة.
نوع الآلة النظيفة أصبح أقصر:
data VM a = VM { stack :: !Stack , status :: Maybe String , journal :: !a }
بالكاد يلاحظ مصممو البرامج والبرامج أنفسهم هذا التغيير ، لكن أنواعهم ستتغير. بالإضافة إلى ذلك ، من المنطقي تحديد عدة أنواع من المرادفات لتبسيط التوقيعات.
type Memory m = M.MVector (PrimState m) Int type Logger ma = Memory m -> Code -> VM a -> m (VM a) type Program' ma = Logger ma -> Memory m -> Program ma
سيكون لدى البنّائين حجة أخرى تمثل الوصول إلى الذاكرة. سيتغير المنفذون بشكل كبير ، خاصة أولئك الذين يحتفظون بسجل حسابي ، لأنهم الآن بحاجة إلى طلب حالة المتغير المتغير لذلك. يمكن رؤية الكود الكامل ودراسته في المستودع ، ولكن هنا سأعطيك الأكثر إثارة للاهتمام: تنفيذ المكونات الأساسية للعمل مع الذاكرة لإظهار كيف يتم ذلك.
geti :: PrimMonad m => Int -> Program' ma geti i = programM (GETI i) $ \mem -> \s -> if (0 <= i && i < memSize) then \vm -> do x <- M.unsafeRead mem i setStack (x:s) vm else err "index out of range" puti :: PrimMonad m => Int -> Program' ma puti i = programM (PUTI i) $ \mem -> \case (x:s) -> if (0 <= i && i < memSize) then \vm -> do M.unsafeWrite mem ix setStack s vm else err "index out of range" _ -> err "expected an element" get :: PrimMonad m => Program' ma get = programM (GET) $ \m -> \case (i:s) -> \vm -> do x <- M.read mi setStack (x:s) vm _ -> err "expected an element" put :: PrimMonad m => Program' ma put = programM (PUT) $ \m -> \case (i:x:s) -> \vm -> M.write mix >> setStack s vm _ -> err "expected two elemets"
عرض برنامج التحسين على الفور توفير المزيد عند التحقق من قيم الفهرس المسموح بها في الذاكرة ، لأن geti
puti
و geti
معروفة في مرحلة إنشاء البرنامج ويمكن التخلص من القيم غير الصحيحة مقدمًا. لا تضمن الفهارس المحددة ديناميكيًا لأوامر put
الأوامر get
الأمان ، ولم يسمح الملاك الرياضي بإجراء مكالمات خطيرة لهم.
كل هذه الضجيج مع وضع الذاكرة في حجة منفصلة تبدو معقدة. ولكنها تُظهر بوضوح كبير البيانات التي يجب تغييرها من مكانها - يجب أن تكون في الخارج . أذكرك أننا نحاول إحضار رجل توصيل البيتزا إلى مختبر معقم. الوظائف النقية تعرف ماذا تفعل بها ، لكن هذه الأشياء لن تصبح أبداً مواطنين من الدرجة الأولى ، ولا يستحق إعداد البيتزا في المختبر.
دعنا نتحقق مما اشتريناه بهذه التغييرات:
$ ./Monopig5 +RTS -sstderr "Ok" 9,169,192,928 bytes allocated in the heap 2,006,680 bytes copied during GC 529,608 bytes maximum residency (2 sample(s)) 25,248 bytes maximum slop 2 MB total memory in use (0 MB lost due to fragmentation) Tot time (elapsed) Avg pause Max pause Gen 0 17693 colls, 0 par 0.094s 0.093s 0.0000s 0.0001s Gen 1 2 colls, 0 par 0.000s 0.000s 0.0002s 0.0003s INIT time 0.000s ( 0.000s elapsed) MUT time 7.228s ( 7.232s elapsed) GC time 0.094s ( 0.093s elapsed) EXIT time 0.000s ( 0.000s elapsed) Total time 7.325s ( 7.326s elapsed) %GC time 1.3% (1.3% elapsed) Alloc rate 1,268,570,828 bytes per MUT second Productivity 98.7% of total user, 98.7% of total elapsed
هذا تقدم بالفعل! تم تقليل استخدام الذاكرة ثماني مرات ، وزادت سرعة تنفيذ البرنامج 180 مرة ، وبقي جامع القمامة بدون عمل تقريبًا.

يبدو الحل monopig ش. موت. ، وهو أبطأ عشر مرات من الحل الأصلي على js
، ولكن بصرف النظر عن ذلك ، فإن الحل الأصلي على Haskell ، باستخدام ناقلات قابلة للتغيير. هنا هو رمزه:
fill' :: Int -> Int -> Memory IO -> IO (Memory IO) fill' knm | n > memSize-k = return m | otherwise = M.unsafeWrite mn 1 >> fill' k (n+k) m sieve' :: Int -> Memory IO -> IO (Memory IO) sieve' km | k*k < memSize = do x <- M.unsafeRead mk if x == 0 then fill' k (2*k) m >>= sieve' (k+1) else sieve' (k+1) m | otherwise = return m
يبدأ على النحو التالي
main = do m <- M.replicate memSize 0 stimes 100 (sieve' 2 m >> return ()) print "Ok"
والآن يظهر هاسكل أخيرًا أنه ليس لغة لعبة. تحتاج فقط إلى استخدامه بحكمة. بالمناسبة ، يستخدم الرمز أعلاه حقيقة أن النوع IO ()
يشكل مجموعة نصفية مع تشغيل التنفيذ المتسلسل للبرامج (>>)
، وبمساعدة stimes
كررنا 100 مرة حساب مشكلة الاختبار.
من الواضح الآن لماذا يوجد مثل هذا الكراهية للمصفوفات الوظيفية ولماذا لا يتذكر أحد كيفية العمل معها: بمجرد أن يحتاج مبرمج Haskell بشكل خطير إلى هياكل الوصول العشوائي ، فإنه يعيد التركيز على البيانات القابلة للتغيير ويعمل في mons ST أو IO.
إن استحضار جزء من الأوامر في منطقة خاصة يستدعي الشك في قانونية التماثل البرنامج . بعد كل شيء ، لا يمكننا تحويل الشفرة في وقت واحد إلى برامج خالصة وبرامج أحادية ، وهذا لا يسمح لنظام النوع بالقيام بذلك. ومع ذلك ، فإن فئات الكتابة مرنة بما يكفي لوجود هذا التشكل. كود التماثل ينقسم البرنامج الآن إلى عدة أشكال متجانسة لمجموعات فرعية مختلفة من اللغة. يمكن رؤية كيف يتم ذلك بالضبط في [كود] () كامل من البرنامج.
لا تتوقف عند هذا الحد
سيساعد التخلص من استدعاءات الوظائف غير الضرورية وتضمين التعليمات البرمجية الخاصة بها مباشرةً باستخدام {-# INLINE #-}
على تغيير إنتاجية البرنامج بشكل طفيف. هذه الطريقة غير مناسبة للوظائف العودية ، ولكنها رائعة للمكونات الأساسية ووظائف الضبط. يقلل من وقت تنفيذ برنامج الاختبار بنسبة 25٪ أخرى (انظر Monopig51.hs ).

ستكون الخطوة المعقولة التالية هي التخلص من أدوات التسجيل عندما لا تكون هناك حاجة إليها. في مرحلة تكوين الشكل الداخلي الذي يمثل البرنامج ، نستخدم حجة خارجية ، نحددها عند بدء التشغيل. يمكن تحذير programM
و programM
من أنه قد لا يكون هناك مُسجل حجة. في هذه الحالة ، لا يحتوي رمز المحول على أي شيء غير ضروري: فقط الوظيفة والتحقق من حالة الجهاز.
program code f = programM code (const f) programM code f (Just logger) mem = Program . ([code],) . ActionM $ \vm -> case status vm of Nothing -> logger mem code =<< f mem (stack vm) vm _ -> return vm programM code f _ mem = Program . ([code],) . ActionM $ \vm -> case status vm of Nothing -> f mem (stack vm) vm _ -> return vm
الآن ، يجب أن يشير تنفيذ الوظائف بشكل صريح إلى وجود أو عدم وجود تسجيل لا يستخدم كعب الروتين ، ولكن باستخدام نوع Maybe (Logger ma)
. لماذا يجب أن يعمل هذا ، لأنه إذا كان هناك تسجيل أم لا ، فإن مكونات البرنامج ستكتشف "في اللحظة الأخيرة" ، قبل التنفيذ؟ ألن يتم خياطة الرمز غير الضروري في مرحلة تكوين تركيبة البرنامج؟ هاسكل هي لغة كسولة وهنا تلعب في أيدينا. قبل التنفيذ ، تم تحسين الرمز النهائي لمهمة معينة. أدى هذا التحسين إلى تقليل وقت تنفيذ البرنامج بنسبة 40٪ أخرى (انظر Monopig52.hs ).

بهذا ، سنكمل العمل على تسريع الخنزير الصغير الأحادي. إنه يركض بالفعل بسرعة كافية بحيث يمكن لكل من الملاك والشيطان أن يهدأ. هذا بالطبع ليس C ، ما زلنا نستخدم قائمة نظيفة ككومة ، ولكن استبدالها بمصفوفة سيؤدي إلى حفر شامل للرمز ، ورفض استخدام قوالب أنيقة في تعريفات الأوامر الأساسية. كنت أرغب في الحصول على الحد الأدنى من التغييرات ، وبشكل أساسي على مستوى الأنواع.
تبقى بعض المشاكل مع التسجيل. يعمل عدد بسيط من عدد الخطوات أو استخدام المكدس بشكل جيد (لقد جعلنا حقل السجل صارمًا) ، ولكن اقترانها يبدأ بالفعل في تناول الذاكرة ، عليك العبث بالركلات باستخدام seq
، وهو أمر مزعج بالفعل. ولكن قل لي ، من سيسجل الخطوات الـ 14 مليار ، إذا كان يمكنك تصحيح المهمة في المئات الأولى؟ لذلك لن أقضي وقتي في التعجيل بالتسارع.
يبقى فقط أن نضيف أنه في المقالة حول الخنزير الصغير ، كإحدى طرق تحسين الحسابات ، يتم توفير التتبع: تخصيص الأقسام الخطية من التعليمات البرمجية ، والآثار التي يمكن إجراء العمليات الحسابية داخلها تجاوز دورة الإرسال الرئيسية للأوامر (كتلة switch
). في حالتنا ، فإن التكوين الأحادي لمكونات البرنامج يخلق مثل هذه الآثار سواء أثناء تكوين البرنامج من مكونات EDSL ، أو أثناء تشغيل الـ fromCode
. تذهب طريقة التحسين هذه إلينا مجانًا ، إذا جاز التعبير ، عن طريق البناء.
هناك العديد من الحلول Conduits
والسريعة في النظام البيئي هاسكل ، مثل Conduits
أو Pipes
تيارات ، وهناك بدائل String
ممتازة ومنشئي XML attoparsec
مثل blaze-html ، ومحلل attoparsec
هو معيار للتحليل التجميعي attoparsec
LL (∞). كل هذا ضروري للتشغيل العادي. ولكن هناك حاجة أكثر للبحث الذي يؤدي إلى هذه القرارات. كان هاسكل ولا يزال أداة بحث تلبي متطلبات محددة لا يحتاجها عامة الناس. رأيت في كامتشاتكا كيف أغلقت ارسالا ساحقا على طائرة هليكوبتر من طراز Mi-4 علب الثقاب بحجة ، ودفعت معدات الهبوط بعجلة أثناء تعليقها في الهواء. يمكن القيام بذلك ، وهو رائع ، ولكن ليس ضروريًا.
ولكن ، مع ذلك ، هذا رائع !!