كيف كتبت مقدمة 4K في Rust - وفازت بها

لقد قمت مؤخرًا بكتابة مقدمة 4K الأولى الخاصة بي في Rust وعرضتها في Nova 2020 ، حيث فازت بالمركز الأول في مسابقة مقدمة المدرسة الجديدة. كتابة مقدمة 4K أمر صعب. هذا يتطلب معرفة العديد من المجالات المختلفة. سأركز هنا على تقنيات كيفية تقصير شفرة Rust قدر الإمكان.





يمكنك مشاهدة العرض التوضيحي على Youtube أو تنزيل الملف التنفيذي على Pouet أو الحصول على شفرة المصدر من Github .



مقدمة 4K هي عرض توضيحي حيث يبلغ حجم البرنامج بأكمله (بما في ذلك أي بيانات) 4096 بايت أو أقل ، لذلك من المهم أن يكون الرمز فعالاً قدر الإمكان. لدى Rust بعض السمعة في بناء الملفات التنفيذية المنتفخة ، لذلك أردت أن أرى ما إذا كان يمكن أن يكون رمزًا فعالاً وموجزًا.



ترتيب



تم كتابة المقدمة بأكملها في مزيج من الصدأ و glsl. يتم استخدام Glsl للعرض ، ولكن Rust يفعل كل شيء آخر: إنشاء العالم ، والتحكم في الكاميرا والعناصر ، وإنشاء الأدوات ، وتشغيل الموسيقى ، وما إلى ذلك.



هناك تبعيات في الرمز على بعض الميزات التي لم يتم تضمينها بعد في Rust Rust ، لذلك أستخدم صندوق الأدوات الصدأ الليلي. لتثبيت هذه الحزمة الافتراضية واستخدامها ، قم بتشغيل أوامر rustup التالية:



rustup toolchain install nightly
rustup default nightly


أنا أستخدم crinkler لضغط ملف كائن تم إنشاؤه بواسطة مترجم Rust.



لقد استخدمت أيضًا أداة التظليل للمعالجة المسبقة للتظليلglsl لجعلها أصغر وأكثر تجعدًا. لا يدعم مصغر التظليل الإخراج إلى .rs ، لذلك أخذت الإخراج الخام وقمت بنسخه يدويًا إلى ملف shader.rs الخاص بي (في وقت متأخر كان من الواضح أنني بحاجة لأتمتة هذه الخطوة بطريقة أو بأخرى. أو حتى كتابة طلب سحب لمصغر التظليل) ...



كانت نقطة البداية هي مقدمة 4K السابقة حول Rust ، والتي بدت مقتضبة جدًا في ذلك الوقت. توفر هذه المقالة أيضًا مزيدًا من التفاصيل حول تكوين الملف tomlوكيفية استخدام xargo لتجميع الملف الثنائي الصغير.



تحسين تصميم البرنامج لتقليل الكود



العديد من التحسينات الأكثر فعالية للحجم ليست قرصنة ذكية. هذا هو نتيجة إعادة التفكير في التصميم.



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



تحليل رمز المجمع



في مرحلة ما ، عليك أن تنظر إلى المجمع المجمع ومعرفة ما يتم تجميع الكود فيه وأي تحسينات للحجم تستحق العناء. لدى مترجم Rust خيارًا مفيدًا جدًا --emit=asmلإخراج رمز التجميع. يقوم الأمر التالي بإنشاء ملف مجمع .s:



xargo rustc --release --target i686-pc-windows-msvc -- --emit=asm


لا تحتاج إلى أن تكون خبيرًا في التجميع للاستفادة من تعلم مخرجات المجمّع ، ولكن من الأفضل بالتأكيد أن يكون لديك فهم أساسي للقواعد. هذا الخيار opt-level = "zيجبر المترجم على تحسين الكود قدر الإمكان لأصغر حجم. بعد ذلك ، يكون من الصعب قليلاً معرفة أي جزء من رمز التجميع يتوافق مع أي جزء من رمز Rust.



لقد وجدت أن مترجم Rust يمكن أن يكون جيدًا بشكل مدهش في التصغير ، وإزالة التعليمات البرمجية غير المستخدمة والمعلمات غير الضرورية. كما أنها تقوم ببعض الأشياء الغريبة ، لذلك من المهم جدًا دراسة النتيجة في التجميع من وقت لآخر.



وظائف اضافيه



لقد عملت مع نسختين من التعليمات البرمجية. يسجل المرء العملية ويسمح للمشاهد بمعالجة الكاميرا لإنشاء مسارات مثيرة للاهتمام. يسمح لك Rust بتعريف وظائف هذه الإجراءات الإضافية. يحتوي الملف tomlعلى قسم [الميزات] الذي يسمح لك بتعريف الميزات المتاحة وتبعياتها. في tomlمقدّمي 4K ، يكون الملف الشخصي التالي:



[features]
logger = []
fullscreen = []


لا تحتوي أي من الوظائف الإضافية على تبعيات ، لذا فهي تعمل بشكل فعال كعلامات تجميع شرطية. الكتل الشرطية من التعليمات البرمجية مسبوقة ببيان #[cfg(feature)]. لا يؤدي استخدام الوظائف في حد ذاته إلى جعل الرمز الخاص بك أصغر ، ولكنه يجعل عملية التطوير أسهل بكثير عندما تقوم بالتبديل بسهولة بين مجموعات مختلفة من الوظائف.



        #[cfg(feature = "fullscreen")]
        {
            //       ,    
        }

        #[cfg(not(feature = "fullscreen"))]
        {
            //       ,     
        }


بعد فحص الشفرة المترجمة ، أنا متأكد من أن الميزات المحددة فقط مضمنة.



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



باستخدام get_unchecked



عند وضع الرمز داخل الكتلة ، unsafe{}افترضت نوعًا ما أنه سيتم تعطيل جميع عمليات الفحص الأمني ​​، ولكن هذا ليس هو الحال. لا تزال جميع الفحوصات المعتادة تتم هناك ، وهي مكلفة.



بشكل افتراضي ، يتحقق النطاق من جميع المكالمات إلى الصفيف. خذ كود الصدأ التالي:



    delay_counter = sequence[ play_pos ];


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



دعنا نحول الكود كما يلي:



    delay_counter = *sequence.get_unchecked( play_pos );


هذا يخبر المترجم بعدم القيام بأي فحوصات للمدى وإلقاء نظرة على الجدول. من الواضح أن هذه عملية خطيرة وبالتالي لا يمكن إجراؤها إلا داخل الرمز unsafe.



دورات أكثر كفاءة



في البداية ، تم تشغيل كل حلقاتي بشكل اصطلاحي كما هو متوقع في Rust باستخدام بناء الجملة for x in 0..10. افترضت أنه سيتم تجميعها في حلقة ضيقة قدر الإمكان. والمثير للدهشة، وهذا ليس هو الحال. أبسط حالة:



for x in 0..10 {
    // do code
}


سيتم تجميعها في كود التجميع الذي يقوم بما يلي:



    setup loop variable
loop:
          
      ,   end
    //    
       loop
end:


بينما الكود التالي



let x = 0;
loop{
    // do code
    x += 1;
    if x == 10 {
        break;
    }
}


يجمع مباشرة إلى:



    setup loop variable
loop:
    //    
          
       ,   loop
end:


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



مشكلة أخرى ، أكثر صعوبة في فهم المشكلة مع حلقة روست الاصطلاحية هي أنه في بعض الحالات أضاف المترجم بعض كود إعداد المكرر الإضافي الذي انتفخ الرمز حقًا. ما زلت لم أحسب بعد سبب هذا الإعداد الإضافي للمكرر ، حيث كان دائمًا من السهل استبدال التركيبات for {}بالبنية loop{}.



باستخدام تعليمات المتجه



قضيت الكثير من الوقت في تحسين الشفرة glsl، وأحد أفضل التحسينات (التي عادة ما تجعل الشفرة تعمل بشكل أسرع) هي العمل مع المتجه بالكامل في نفس الوقت ، بدلاً من كل مكون بدوره.



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



لاستخدام الوظائف المضمنة ، أود أن أحول الكود التالي



        global_spheres[ CAMERA_ROT_IDX ][ 0 ] += camera_rot_speed[ 0 ]*camera_speed;
        global_spheres[ CAMERA_ROT_IDX ][ 1 ] += camera_rot_speed[ 1 ]*camera_speed;
        global_spheres[ CAMERA_ROT_IDX ][ 2 ] += camera_rot_speed[ 2 ]*camera_speed;


في هذا:



        let mut dst:x86::__m128 = core::arch::x86::_mm_load_ps(global_spheres[ CAMERA_ROT_IDX ].as_mut_ptr());
        let mut src:x86::__m128 = core::arch::x86::_mm_load_ps(camera_rot_speed.as_mut_ptr());
        dst = core::arch::x86::_mm_add_ps( dst, src);
        core::arch::x86::_mm_store_ss( (&mut global_spheres[ CAMERA_ROT_IDX ]).as_mut_ptr(), dst );


والتي ستكون أصغر قليلاً (وأقل قراءة). لسوء الحظ ، لسبب ما كسر هذا البناء التصحيح ، على الرغم من أنه يعمل بشكل جيد في بناء الإصدار. من الواضح أن المشكلة هنا تكمن في معرفتي بالصدأ الداخلي ، وليس اللغة نفسها. يجدر قضاء المزيد من الوقت في هذا عند إعداد مقدمة 4K التالية ، نظرًا لأن تقليل كمية الرمز كان مهمًا.



باستخدام برنامج OpenGL



هناك العديد من صناديق الصدأ القياسية لتحميل وظائف OpenGL ، ولكن بشكل افتراضي ، تقوم جميعها بتحميل مجموعة كبيرة جدًا من الوظائف. تستهلك كل وظيفة محملة بعض المساحة لأن المحمل يحتاج إلى معرفة اسمه. يعتبر Crinkler جيدًا جدًا في ضغط هذا النوع من التعليمات البرمجية ، ولكن لا يمكنه التخلص من النفقات العامة تمامًا ، لذلك كان علي إنشاء إصدار خاص بي gl.rsيتضمن ميزات OpenGL التي احتاجها فقط.



خاتمة



كان الهدف الرئيسي هو كتابة مقدمة 4K صحيحة تنافسيًا وإثبات أن Rust مناسبة للسيناريوهات والسيناريوهات حيث يتم حساب كل بايت وتحتاج حقًا إلى تحكم منخفض المستوى. كقاعدة ، تم التفكير في المجمع و C فقط في هذا المجال. وكان الهدف الإضافي هو الاستفادة القصوى من الصدأ الاصطلاحي.



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



المهمة الثانية أقل نجاحًا. هناك الكثير من التعليمات البرمجية غير الآمنة التي لا يجب أن تكون موجودة بالفعل.unsafeله تأثير مدمر. من السهل جدًا استخدامه لتنفيذ شيء ما بسرعة (على سبيل المثال ، باستخدام المتغيرات الثابتة القابلة للتغيير) ، ولكن بمجرد ظهور رمز غير آمن ، فإنه يولد رمزًا أكثر غير آمن ، وفجأة في كل مكان. في المستقبل ، سأكون أكثر حرصًا على الاستخدام unsafeفقط عندما لا يكون هناك بديل حقًا.



All Articles