"يقضي المبرمجون قدرًا كبيرًا من الوقت في القلق بشأن سرعة برامجهم ، وغالبًا ما يكون لمحاولات تحقيق الكفاءة تأثير سلبي كبير على القدرة على تصحيحها ودعمها. من الضروري نسيان التحسينات الصغيرة ، على سبيل المثال ، في 97 ٪ من الحالات. التحسين المبكر هو أصل كل الشر! ولكن يجب ألا يغيب عن بالنا تلك الـ 3٪ حيث من المهم حقًا! "
دونالد كنوت.

عند إجراء عمليات تدقيق للعقود الذكية ، نسأل أنفسنا أحيانًا ما إذا كان تطويرها يتعلق بتلك الـ 97٪ حيث لا توجد حاجة للتفكير في التحسين أو أننا نتعامل مع تلك الـ 3٪ فقط من الحالات التي تكون فيها مهمة. في رأينا ، على الأرجح الثانية. على عكس التطبيقات الأخرى ، لا يتم تحديث العقود الذكية ، ولا يمكن تحسينها "أثناء التنقل" (بشرط ألا يتم وضع الخوارزمية الخاصة بها ، ولكن هذا موضوع منفصل). الحجة الثانية لصالح التحسين
المبكر للعقد هو أنه ، على عكس معظم الأنظمة التي تتجلى فيها التحسينات الفرعية في الحجم فقط ، المتعلقة بخصائص الحديد والبيئة ، فإنه يقاس بعدد كبير من المقاييس ، العقد الذكي هو في الأساس مقياس الأداء الوحيد - استهلاك الغاز.
لذلك ، من الأسهل تقنيًا تقييم فعالية العقد ، لكن المطورين غالبًا ما يواصلون الاعتماد على حدسهم ويقومون بنفس "التحسين المبكر" الأعمى الذي تحدث عنه البروفيسور كنوت. سوف نتحقق من مدى توافق الحل مع الواقع من خلال اختيار عمق البت لمتغير ما. في هذا المثال ، كما هو الحال في معظم الحالات العملية ، لن نحقق مدخرات ، والعكس صحيح ، فإن عقدنا سيصبح أكثر تكلفة من حيث استهلاك الغاز.
ما نوع الغاز؟
يشبه Ethereum جهاز كمبيوتر عالمي ، "معالجه" هو الجهاز الظاهري EVM ، "رمز البرنامج" عبارة عن سلسلة من الأوامر والبيانات المسجلة في عقد ذكي ، والمكالمات هي معاملات من العالم الخارجي. يتم تجميع المعاملات في الهياكل ذات الصلة - وهي الكتل التي تحدث مرة كل بضع ثوانٍ. وبما أن حجم الكتلة محدود من حيث التعريف ، وبروتوكول المعالجة محدد (يتطلب معالجة موحدة لجميع المعاملات في الكتلة من قبل جميع عقد الشبكة) ، ثم لتلبية الطلب المحتمل غير المحدود مع مورد محدود من العقد والحماية من DoS ، يجب على النظام توفير خوارزمية عادلة لاختيار طلب الخدمة ، والذي يتجاهل ، كآلية في العديد من blockchains العامة ، هناك مبدأ بسيط - يمكن للمرسل اختيار مبلغ المكافأة لعمال المناجم لأداء تحويلاته ktsii ويختار عمال المناجم الذين الاحتياجات تشمل كتلة، والذي لم يكن كذلك، اختيار الأكثر ربحية لأنفسهم.
على سبيل المثال ، في Bitcoin ، حيث تقتصر الكتلة على ميغابايت واحد ، يختار المنجم تضمين المعاملة في الكتلة أو لا يعتمد على طولها والعمولة المقترحة (اختيار تلك التي تحتوي على الحد الأقصى لنسبة satoshis لكل بايت).
بالنسبة لبروتوكول Ethereum الأكثر تعقيدًا ، هذا النهج غير مناسب ، لأن البايت الواحد يمكن أن يمثل عدم وجود عملية (على سبيل المثال ، رمز STOP) وعملية الكتابة البطيئة والمكلفة إلى التخزين (SSTORE). لذلك ، يتم توفير سعر خاص لكل كود تشغيل على الهواء ، اعتمادًا على استهلاك موارده.
جدول الرسوم من مواصفات البروتوكول
جدول تدفق الغاز لأنواع مختلفة من العمليات. من مواصفات بروتوكول
الورق الأصفر Ethereum .
على عكس Bitcoin ، لا يقوم مرسل معاملة Ethereum بتعيين العمولة في العملة المشفرة ، ولكن الحد الأقصى لمقدار الغاز الذي يرغب في إنفاقه -
startGas والسعر لكل وحدة من الغاز -
gasPrice . عند تنفيذ الآلة الافتراضية للرمز ، يتم طرح كمية الغاز لكل عملية تالية من startGas حتى يتم الوصول إلى مخرج الشفرة أو نفاد الغاز. على ما يبدو ، هذا هو السبب في استخدام مثل هذا الاسم الغريب لوحدة العمل هذه - تمتلئ المعاملة بالغاز مثل السيارة ، وستصل إلى نقطة الوجهة أم لا تعتمد على ما إذا كان هناك حجم كافٍ مملوء في الخزان. عند الانتهاء من تنفيذ الرمز ، يتم خصم مبلغ الهواء الذي يتم تلقيه عن طريق ضرب الغاز المستهلك بالفعل في السعر المحدد من قبل المرسل (
وي لكل غاز) من مرسل المعاملة. في الشبكة العالمية ، يحدث هذا في لحظة "التعدين" للكتلة ، والتي تتضمن المعاملة المقابلة ، وفي بيئة ريمكس ، يتم "تعدين" المعاملة على الفور ، مجانًا ودون أي شروط.
أداتنا - Remix IDE
من أجل "تحديد" استهلاك الغاز ، سنستخدم البيئة عبر الإنترنت لتطوير عقود Ethereum الخاصة بـ
Remix IDE . يحتوي IDE هذا على محرر كود تمييز بناء الجملة ، وعارض قطعة أثرية ، وعرض واجهة العقد ، ومصحح بصري لآلة افتراضية ، ومجمعي JS لجميع الإصدارات الممكنة والعديد من الأدوات المهمة الأخرى. أوصي بشدة ببدء دراسة الأثير معه. ميزة إضافية هي أنها لا تتطلب التثبيت - فقط افتحها في متصفح من
الموقع الرسمي .
اختيار نوع متغير
تقدم مواصفات لغة Solidity للمطور ما يصل إلى اثنين وثلاثين بتًا من أنواع الأعداد الصحيحة - من 8 إلى 256 بت. تخيل أنك تطور عقدًا ذكيًا مصممًا لتخزين عمر الشخص لسنوات. ما عمق بت انت تختار؟
سيكون من الطبيعي تمامًا اختيار الحد الأدنى من النوع الكافي لمهمة معينة - سيكون uint8 مناسبًا رياضيًا هنا. سيكون من المنطقي أن نفترض أنه كلما صغر حجم الكائن الذي نخزنه على blockchain وقلت الذاكرة التي ننفقها على التنفيذ ، كلما قل حجم النفقات لدينا ، قل ما ندفعه. ولكن في معظم الحالات ، سيكون هذا الافتراض غير صحيح.
بالنسبة للتجربة ، نأخذ أبسط عقد مما تقدمه
وثائق Solidity الرسمية ونجمعه في نسختين - باستخدام النوع المتغير uint256 والنوع الأصغر 32 مرة - uint8.
simpleStorage_uint256.sol pragma solidity ^0.4.0; contract SimpleStorage { //uint is alias for uint256 uint storedData; function set(uint x) public { storedData = x; } function get() public view returns (uint) { return storedData; } }
pragma solidity ^0.4.0; contract SimpleStorage { //uint is alias for uint256 uint storedData; function set(uint x) public { storedData = x; } function get() public view returns (uint) { return storedData; } }
simpleStorage_uint8.sol pragma solidity ^0.4.0; contract SimpleStorage { uint8 storedData; function set(uint8 x) public { storedData = x; } function get() public view returns (uint) { return storedData; } }
قياس "المدخرات"
لذلك ، يتم إنشاء العقود ، وتحميلها في Remix ، ونشرها ، ويتم تنفيذ الاستدعاءات لطرق .set () من خلال المعاملات. ماذا نرى؟
تسجيل نوع طويل أكثر تكلفة من النوع القصير - 20464 مقابل وحدات الغاز 20205! كيف؟ لماذا؟ دعونا نكتشف ذلك!

متجر uint8 مقابل uint256
الكتابة إلى التخزين الدائم هي واحدة من أغلى العمليات في البروتوكول لأسباب واضحة: أولاً ، يؤدي تسجيل الحالة إلى زيادة حجم مساحة القرص التي تتطلبها العقدة الكاملة. يتزايد حجم هذا التخزين باستمرار ، وكلما تم تخزين المزيد من الحالات في العقد ، كلما كان التزامن أبطأ ، كلما زاد متطلبات البنية التحتية (حجم القسم ، عدد iops). في أوقات الذروة ، فإن عمليات الإدخال / الإخراج للقرص البطيء هي التي تحدد أداء الشبكة بالكامل.
سيكون من المنطقي توقع أن تكلفة تخزين uint8 يجب أن تكلف أرخص عشرات المرات من uint256. ومع ذلك ، في مصحح الأخطاء ، يمكنك رؤية أن كلا القيمتين تقعان تمامًا في فتحة التخزين كقيمة 256 بت.

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

يمكنك أن ترى أن العمليات مع uint8 لديها
تعليمات أكثر من uint256. وذلك لأن الجهاز يحول قيمة 8 بت إلى كلمة 256 بت الأصلية ، ونتيجة لذلك ، فإن التعليمات البرمجية محاطة بإرشادات إضافية يدفع المرسل ثمنها. ليس فقط الكتابة ، ولكن أيضًا تنفيذ التعليمات البرمجية بنوع uint8 في هذه الحالة أكثر تكلفة.
أين يمكن تبرير استخدام الأنواع القصيرة؟
انخرط فريقنا في تدقيق العقود الذكية لفترة طويلة ، وحتى الآن لم تكن هناك حالة عملية واحدة حيث يؤدي استخدام نوع صغير في الرمز المقدم للمراجعة إلى تحقيق وفورات. وفي الوقت نفسه ، في بعض الحالات المحددة للغاية ، تكون المدخرات ممكنة نظريًا. على سبيل المثال ، إذا كان عقدك يخزن عددًا كبيرًا من متغيرات أو هياكل الحالة الصغيرة ، فيمكن تعبئتها في فتحات تخزين أقل.
سيكون الفرق أكثر وضوحا في المثال التالي:
1. التعاقد مع 32 متغير uint256
simpleStorage_32x_uint256.sol pragma solidity ^0.4.0; contract SimpleStorage { uint storedData1; uint storedData2; uint storedData3; uint storedData4; uint storedData5; uint storedData6; uint storedData7; uint storedData8; uint storedData9; uint storedData10; uint storedData11; uint storedData12; uint storedData13; uint storedData14; uint storedData15; uint storedData16; uint storedData17; uint storedData18; uint storedData19; uint storedData20; uint storedData21; uint storedData22; uint storedData23; uint storedData24; uint storedData25; uint storedData26; uint storedData27; uint storedData28; uint storedData29; uint storedData30; uint storedData31; uint storedData32; function set(uint x) public { storedData1 = x; storedData2 = x; storedData3 = x; storedData4 = x; storedData5 = x; storedData6 = x; storedData7 = x; storedData8 = x; storedData9 = x; storedData10 = x; storedData11 = x; storedData12 = x; storedData13 = x; storedData14 = x; storedData15 = x; storedData16 = x; storedData17 = x; storedData18 = x; storedData19 = x; storedData20 = x; storedData21 = x; storedData22 = x; storedData23 = x; storedData24 = x; storedData25 = x; storedData26 = x; storedData27 = x; storedData28 = x; storedData29 = x; storedData30 = x; storedData31 = x; storedData32 = x; } function get() public view returns (uint) { return storedData1; } }
2. التعاقد مع 32 متغير uint8
simpleStorage_32x_uint8.sol pragma solidity ^0.4.0; contract SimpleStorage { uint8 storedData1; uint8 storedData2; uint8 storedData3; uint8 storedData4; uint8 storedData5; uint8 storedData6; uint8 storedData7; uint8 storedData8; uint8 storedData9; uint8 storedData10; uint8 storedData11; uint8 storedData12; uint8 storedData13; uint8 storedData14; uint8 storedData15; uint8 storedData16; uint8 storedData17; uint8 storedData18; uint8 storedData19; uint8 storedData20; uint8 storedData21; uint8 storedData22; uint8 storedData23; uint8 storedData24; uint8 storedData25; uint8 storedData26; uint8 storedData27; uint8 storedData28; uint8 storedData29; uint8 storedData30; uint8 storedData31; uint8 storedData32; function set(uint8 x) public { storedData1 = x; storedData2 = x; storedData3 = x; storedData4 = x; storedData5 = x; storedData6 = x; storedData7 = x; storedData8 = x; storedData9 = x; storedData10 = x; storedData11 = x; storedData12 = x; storedData13 = x; storedData14 = x; storedData15 = x; storedData16 = x; storedData17 = x; storedData18 = x; storedData19 = x; storedData20 = x; storedData21 = x; storedData22 = x; storedData23 = x; storedData24 = x; storedData25 = x; storedData26 = x; storedData27 = x; storedData28 = x; storedData29 = x; storedData30 = x; storedData31 = x; storedData32 = x; } function get() public view returns (uint) { return storedData1; } }
إن نشر العقد الأول (32 uint256) سيكلف أقل - فقط 89941 غاز ، لكن .set () سيكون أغلى بكثير منذ ستشغل 256 فتحة في التخزين ، والتي ستكلف 640،639 غاز لكل مكالمة. سيكون العقد الثاني (32 uint8) أغلى مرتين ونصف عند نشر (221663 غاز) ، ولكن كل استدعاء لطريقة .set () سيكون أرخص بكثير ، لأن يغير خلية واحدة فقط من المرحلة (185291 غاز).
هل يجب تطبيق هذا التحسين؟
مدى أهمية تأثير تحسين النوع هو نقطة خلافية. كما ترى ، حتى بالنسبة لمثل هذه الحالة الاصطناعية المختارة خصيصًا ،
لم نحصل على اختلافات متعددة. اختيار استخدام uint8 أو uint256 هو بالأحرى مثال على حقيقة أنه يجب تطبيق التحسين إما بشكل مفيد (مع فهم الأدوات ، والتنميط) ، أو عدم التفكير في ذلك على الإطلاق. فيما يلي بعض الإرشادات العامة:
- إذا كان العقد يحتوي على العديد من الأرقام الصغيرة أو الهياكل المدمجة في المستودع ، فيمكنك التفكير في التحسين ؛
- إذا كنت تستخدم النوع "المختصر" - تذكر نقاط الضعف الزائدة / المنخفضة التدفق ؛
- بالنسبة لمتغيرات الذاكرة ووسائط الوظائف التي لم تتم كتابتها إلى المستودع ، من الأفضل دائمًا استخدام النوع الأصلي uint256 (أو الاسم المستعار uint). على سبيل المثال ، ليس من المنطقي تعيين مكرر القائمة على uint8 - فقط خسر ؛
- من الأهمية بمكان للتغليف الصحيح في فتحات التخزين للمترجم ترتيب المتغيرات في العقد .
المراجع
سأنتهي بنصيحة لا تحتوي على أي موانع: تجربة أدوات التطوير ، ومعرفة مواصفات اللغة والمكتبة والأطر. فيما يلي الروابط الأكثر فائدة ، في رأيي ، لبدء التعرف على منصة Ethereum:
- بيئة تطوير عقد Remix هي بيئة تطوير متكاملة (IDE) قائمة على المستعرض وظيفية للغاية ؛
- مواصفات لغة الصلابة ، سيذهب الرابط على وجه التحديد إلى قسم تخطيط متغيرات الحالة ؛
- مستودع عقد مثير جدا للاهتمام من فريق OpenZeppelin الشهير. أمثلة على تنفيذ الرموز ، وعقود الحشود ، والأهم من ذلك - مكتبة SafeMath ، التي تساعد على العمل بأمان مع الأنواع ؛
- الورق الأصفر Ethereum ، المواصفات الرسمية للجهاز الظاهري Ethereum ؛
- كتاب Ethereum الأبيض ، مواصفات منصة Ethereum ، وثيقة أكثر عمومية وعالية المستوى مع عدد كبير من الروابط ؛
- Ethereum في 25 دقيقة ، وهي مقدمة تقنية قصيرة ولكن مع ذلك قوية لـ Ethereum من منشئ النظام الأساسي ، Vitalik Buterin ؛
- مستكشف Etherscan blockchain ، نافذة على عالم الأثير الحقيقي ، متصفح الكتل ، المعاملات ، الرموز المميزة ، العقود على الشبكة الرئيسية. ستجد على Etherscan مستكشفًا لشبكات الاختبار Rinkeby و Ropsten و Kovan (شبكات ذات بث مجاني مبنية على بروتوكولات إجماع مختلفة).