يؤدي تحسين الغاز في Ethereum إلى إعادة كتابة كود Solidity لإنجاز نفس منطق العمل مع استهلاك عدد أقل من وحدات الغاز في جهاز Ethereum الظاهري (EVM).
حيل تحسين الغاز لا تعمل دائمًا
بعض حيل تحسين الغاز تعمل فقط في سياق معين. على سبيل المثال، بشكل حدسي، يبدو ذلك
if (!cond) {
// branch False
}
else {
// branch True
}
أقل كفاءة من
if (cond) {
// branch True
}
else {
// branch False
}
لأنه يتم إستخدام أكواد تشغيل (opcodes) إضافية لعكس الحالة. وعلى عكس ما هو متوقع، هناك العديد من الحالات التي يؤدي فيها هذا التحسين إلى زيادة تكلفة المعاملة. يمكن أن يكون solidity compiler غير متوقع في بعض الأحيان.
لذلك، يجب عليك في الواقع قياس تأثير البدائل قبل الاستقرار على خوارزمية معينة. فكر في أن بعض هذه الحيل تعمل على توعية المناطق التي قد يتصرف فيها الـ solidity compiler بشكل غير متوقع.
تعتمد حيل تحسين الغاز أحيانًا على ما يفعله الـ compiler محليًا. يجب عليك عمومًا اختبار الإصدار الأمثل من الكود والإصدار غير الأمثل لترى أنك تحصل بالفعل على تحسن. سوف نقوم بتوثيق بعض الحالات المدهشة حيث ما يفترض أن يؤدي إلى التحسين يؤدي في الواقع إلى ارتفاع التكلفة.
ثانيًا، قد تتغير بعض سلوكيات التحسين هذه عند استخدام خيار –via-ir في solidity compile.
احذر من التعقيد وسهولة القراءة
عادةً ما تجعل تحسينات الغاز الكود أقل قابلية للقراءة وأكثر تعقيدًا. يجب على المهندس الجيد إجراء مقايضة ذاتية حول التحسينات التي تستحق العناء، وأيها لا تستحق ذلك.
1. الأهم: تجنب حالات الكتابه من صفر إلى واحد في الـ storage
تعد تهيئة متغير التخزين (storage) إحدى أغلى العمليات التي يمكن أن يقوم بها العقد.
عندما ينتقل متغير التخزين من صفر إلى غير صفر، يجب على المستخدم دفع إجمالي 22,100 غاز (20,000 غاز مقابل كتابة صفر إلى غير صفر و2,100 للوصول إلى التخزين البارد).
هذا هو السبب في أن عقود reentrancy guard من Openzeppelin تسجل الوظائف على أنها نشطة أو غير نشطة بإستخدام 1 و2 بدلاً من 0 و1. مما لا يكلف سوى 5000 غاز لتغيير متغير تخزين من غير صفر إلى غير صفر.
2. الحفظ المؤقت لمتغيرات الـ storage : كتابة وقراءة متغيرات التخزين مرة واحدة بالضبط
سترى النمط التالي بشكل متكرر في كود الSolidity . القراءة من متغير تخزين تكلف ما لا يقل عن 100 غاز لأن Solidity لا تقوم بالحفظ المؤقت لقراءة التخزين . الكتابة هي أكثر تكلفة بكثير. لذلك، يجب عليك تخزين المتغير يدويًا مؤقتًا لإجراء قراءة تخزينية واحدة بالضبط وكتابة تخزينية واحدة بالضبط.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract Counter1 {
uint256 public number;
function increment() public {
require(number < 10);
number = number + 1;
}
}
contract Counter2 {
uint256 public number;
function increment() public {
uint256 _number = number;
require(_number < 10);
number = _number + 1;
}
}
Solidityتقرأ الوظيفة الأولى العداد مرتين، بينما يقرأه الكود الثاني مرة واحدة.
3. حشو المتغيرات ذات الصلة
يؤدي حشو المتغيرات ذات الصلة في نفس الفتحة (Slot) إلى تقليل تكاليف الغاز عن طريق تقليل العمليات المكلفة المتعلقة بالتخزين.
الحشو اليدوي هو الأكثر كفاءة حيث نقوم بتخزين واسترجاع قيمتين uint80 في متغير واحد (uint160) باستخدام تبديل البت. سيستخدم هذا فتحة تخزين واحدة فقط ويكون أرخص عند تخزين أو قراءة القيم الفردية في معاملة واحدة.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract GasSavingExample {
uint160 public packedVariables;
function packVariables(uint80 x, uint80 y) external {
packedVariables = uint160(x) << 80 | uint160(y);
}
function unpackVariables() external view returns (uint80, uint80) {
uint80 x = uint80(packedVariables >> 80);
uint80 y = uint80(packedVariables);
return (x, y);
}
}
يعتبر حشو EVM أقل كفاءة قليلاً
هذا المثال يستخدم أيضًا فتحة واحدة مثل المثال أعلاه، ولكنها قد تكون مكلفة بعض الشيء عند تخزين القيم أو قراءتها في معاملة واحدة. وذلك لأن EVM سيقوم بتغيير البت بنفسه.
contract GasSavingExample2 {
uint80 public var1;
uint80 public var2;
function updateVars(uint80 x, uint80 y) external {
var1 = x;
var2 = y;
}
function loadVars() external view returns (uint80, uint80) {
return (var1, var2);
}
}
عدم إستخدام الحشو هو الأقل كفاءة.
لا يستخدم هذا أي تحسين، ويكون أكثر تكلفة عند تخزين القيم أو قراءتها.
على عكس الأمثلة الأخرى، يستخدم هذا فتحتين للتخزين لتخزين المتغيرات.
contract NonGasSavingExample {
uint256 public var1;
uint256 public var2;
function updateVars(uint256 x, uint256 y) external {
var1 = x;
var2 = y;
}
function loadVars() external view returns (uint256, uint256) {
return (var1, var2);
}
}
4. حشو الـ structs
يمكن أن يساعد حشو عناصر الـ struct ، مثل حشو متغيرات الحالة ذات الصلة، في توفير الغاز. (من المهم ملاحظة أنه في Solidity، يتم تخزين عناصر الـstruct بشكل تسلسلي في مخزن العقد (contract’s storage)، بدءًا من موضع الفتحة حيث تتم تهيئتهم).
خذ بعين الاعتبار الأمثلة التالية:
الـ structs بدون حشو
يحتوي الـ structs التالي على ثلاثة عناصر سيتم تخزينها في ثلاث فتحات منفصلة. ومع ذلك، إذا تم حشو هذه العناصر، فسيتم استخدام فتحتين فقط وهذا سيجعل القراءة والكتابة لعناصر الـ structs أرخص.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract Unpacked_Struct {
struct unpackedStruct {
uint64 time; // Takes one slot - although it only uses 64 bits (8 bytes) out of 256 bits (32 bytes).
uint256 money; // This will take a new slot because it is a complete 256 bits (32 bytes) value and thus cannot be packed with the previous value.
address person; // An address occupies only 160 bits (20 bytes).
}
// Starts at slot 0
unpackedStruct details = unpackedStruct(53_000, 21_000, address(0xdeadbeef));
function unpack() external view returns (unpackedStruct memory) {
return details;
}
}
الـ structs المحشو
يمكننا أن نجعل المثال أعلاه يستخدم كمية أقل من الغاز عن طريق حشو عناصر الـ structs كالأتي.
contract Packed_Struct {
struct packedStruct {
uint64 time; // In this case, both `time` (64 bits) and `person` (160 bits) are packed in the same slot since they can both fit into 256 bits (32 bytes)
address person; // Same slot as `time`. Together they occupy 224 bits (28 bytes) out of 256 bits (32 bytes).
uint256 money; // This will take a new slot because it is a complete 256 bits (32 bytes) value and thus cannot be packed with the previous value.
}
// Starts at slot 0
packedStruct details = packedStruct(53_000, address(0xdeadbeef), 21_000);
function unpack() external view returns (packedStruct memory) {
return details;
}
}
5. احتفظ بسلاسل أصغر من 32 بايت
في Solidity، السلاسل هي أنواع بيانات ديناميكية متغيرة الطول، مما يعني أن طولها يمكن أن يتغير وينمو حسب الحاجة.
إذا كان الطول 32 بايت أو أكثر، فإن الفتحة التي تم تعريفها فيها تخزن طول السلسلة * 2 + 1، بينما يتم تخزين بياناتها الفعلية في مكان آخر (تجزئة keccak لتلك الفتحة).
ومع ذلك، إذا كانت السلسلة أقل من 32 بايت، فسيتم تخزين الطول * 2 عند البايت الأقل أهمية في فتحة تخزينها ويتم تخزين البيانات الفعلية للسلسلة بدءًا من البايت الأكثر أهمية في الفتحة التي تم تعريفها فيها.
مثال على السلسلة (أقل من 32 بايت)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract StringStorage1 {
// Uses only one slot
// slot 0: 0x(len * 2)00...(hex"hello")
// Has smaller gas cost due to size.
string public exampleString = "hello";
function getString() public view returns (string memory) {
return exampleString;
}
}
مثال على السلسلة (أكبر من 32 بايت)
contract StringStorage2 {
// Length is more than 32 bytes.
// Slot 0: 0x00...(length*2+1).
// keccak256(0x00): stores hex representation of "hello"
// Has increased gas cost due to size.
string public exampleString = "This is a string that is slightly over 32 bytes!";
function getStringLongerThan32bytes() public view returns (string memory) {
return exampleString;
}
}
ويمكننا إختبار الناتج بإختبار foundry الأتي :
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "forge-std/Test.sol";
import "../src/StringLessThan32Bytes.sol";
contract StringStorageTest is Test {
StringStorage1 public store1;
StringStorage2 public store2;
function setUp() public {
store1 = new StringStorage1();
store2 = new StringStorage2();
}
function testStringStorage1() public {
// test for string less than 32 bytes
store1.getString();
bytes32 data = vm.load(address(store1), 0); // slot 0
emit log_named_bytes32("Full string plus length", data); // the full string and its length*2 is stored at slot 0, because it is less than 32 bytes
}
function testStringStorage2() public {
// test for string longer than 32 bytes
store2.getStringLongerThan32bytes();
bytes32 length = vm.load(address(store2), 0); // slot 0 stores the length*2+1
emit log_named_bytes32("Length of string", length);
// uncomment to get original length as number
// emit log_named_uint("Real length of string (no. of bytes)", uint256(length) / 2);
// divide by 2 to get the original length
bytes32 data1 = vm.load(address(store2), keccak256(abi.encode(0))); // slot keccak256(0)
emit log_named_bytes32("First string chunk", data1);
bytes32 data2 = vm.load(address(store2), bytes32(uint256(keccak256(abi.encode(0))) + 1));
emit log_named_bytes32("Second string chunk", data2);
}
}
وهذه هي النتيجة بعد إجراء الاختبار.
إذا قمنا بربط القيمة السداسية للسلسلة (أطول من 32 بايت) بدون الطول، فإننا نقوم بتحويلها مرة أخرى إلى السلسلة الأصلية (باستخدام بايثون).
إذا كان طول السلسلة أقل من 32 بايت، فمن المفيد أيضًا تخزينها في متغير bytes32 واستخدام assembly لاستخدامها عند الحاجة.
مثال:
contract EfficientString {
bytes32 shortString;
function getShortString() external view returns(string memory) {
string memory value;
assembly {
// get slot 0
let slot0Value := sload(shortString.slot)
// get the byte that holds the length info and divide it by 2 to get the length
let len := div(shr(248, slot0Value), 2)
// get string, shift by 256 - (len * 8) to get it to the most significant byte
let str := shl(sub(256, mul(len, 8)), slot0Value)
// store length in memory
mstore(0x80, len)
// store string in memory
mstore(0xa0, str)
// make `value` reference 0x80 so that solidity does the returning for us
value := 0x80
// update the free memory pointer
mstore(0x40, 0xc0)
}
return value;
}
function storeShortString(string calldata value) external {
assembly {
// require that the length is less than 32
if gt(value.length, 31) {
revert(0, 0)
}
// get the length, multiply it by 2 (following solidity pattern) and push the result to the most significant byte
let shiftedLen := shl(248, mul(value.length, 2))
// get the string itself
let str := shr(sub(256, mul(value.length, 8)), calldataload(value.offset))
// or the shiftedLen and str to get what we need to store in storage
let toBeStored := or(shiftedLen, str)
// store it in storage
sstore(shortString.slot, toBeStored)
}
}
}
يمكن تحسين الكود أعلاه بشكل أكبر ولكن يتم الاحتفاظ به بهذه الطريقة لتسهيل فهمه.
6. المتغيرات التي لا يتم تحديثها أبدًا يجب أن تكون immutable أو constant
في Solidity، يجب أن تكون المتغيرات التي لا يُقصد تحديثها ثابتة أو غير قابلة للتغيير.
وذلك لأن الثوابت والقيم غير القابلة للتغيير مضمنة مباشرة في bytecode للعقد الذي تم تعريفه ولا تستخدم التخزين لهذا السبب.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract Constants {
uint256 constant MAX_UINT256 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
function get_max_value() external pure returns (uint256) {
return MAX_UINT256;
}
}
// This uses more gas than the above contract
contract NoConstants {
uint256 MAX_UINT256 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
function get_max_value() external view returns (uint256) {
return MAX_UINT256;
}
}
وهذا يوفر الكثير من الغاز لأننا لا نجري أي قراءات تخزين مكلفة.
7. استخدام mappings بدلاً من arrays لتجنب التحقق من الطول
عند تخزين قائمة أو مجموعة من العناصر التي ترغب في تنظيمها بترتيب معين وجلبها باستخدام مفتاح/فهرس ثابت، فمن الشائع استخدام بنية بيانات المصفوفة (arrays ) . يعمل هذا بشكل جيد، ولكن هل تعلم أنه يمكن تنفيذ خدعة لتوفير أكثر من 2000 غاز في كل قراءة باستخدام التعيين (mappings ٍ) ؟
انظر المثال أدناه
/// get(0) gas cost: 4860
contract Array {
uint256[] a;
constructor() {
a.push() = 1;
a.push() = 2;
a.push() = 3;
}
function get(uint256 index) external view returns(uint256) {
return a[index];
}
}
/// get(0) gas cost: 2758
contract Mapping {
mapping(uint256 => uint256) a;
constructor() {
a[0] = 1;
a[1] = 2;
a[2] = 3;
}
function get(uint256 index) external view returns(uint256) {
return a[index];
}
}
فقط باستخدام mappings ، نحصل على توفير الغاز بمقدار 2102 غاز. لماذا؟ ما يحدث هو أنه عندما تقرأ قيمة فهرس مصفوفة، تضيف الSolidity محتوى bytecode يتحقق من أنك تقرأ من فهرس صالح (أي فهرس أقل من طول المصفوفة) وإلا فإنه سيرجع خطا يعرف بـ Panic(0x32) . وهذا يمنع من قراءة الذاكرة الفارغة أو الأسوء من ذلك قراءة مواقع التخزين/الذاكرة المخصصة أو الأسوأ من ذلك.
نظرًا للطريقة التي تتم بها mappings (ببساطة مفتاح => زوج القيمة)، لا يوجد فحص مثل هذا ويمكننا القراءة من فتحة التخزين مباشرة. من المهم ملاحظة أنه عند استخدام التعيينات بهذه الطريقة، يجب أن يضمن الكود الخاص بك أنك لا تقرأ فهرسًا خارج نطاق المصفوفة الأساسية الخاصة بك.
8. استخدام unsafeAccess على المصفوفات لتجنب عمليات التحقق من الطول الزائدة
هناك بديل لاستخدام التعيينات لتجنب عمليات التحقق من الطول التي تقوم بها الSolidity عند القراءة من المصفوفات (مع الاستمرار في استخدام المصفوفات)، وهو استخدام وظيفة unsafeAccess في مكتبة Arrays.sol في Openzeppelin. يسمح هذا للمطورين بالوصول مباشرة إلى قيم أي فهرس معين لمصفوفة أثناء تخطي فحص تجاوز الطول. لا يزال من المهم استخدام هذا فقط في حالة أنك متأكدًا من أن الفهارس المستخدمة لن تتجاوز طول المصفوفة.
9. استخدم bitmaps بدلاً من bools عند استخدام كمية كبيرة من القيم boolس
النمط الشائع، خاصة في عمليات العيديات (airdrops) هو وضع علامة على العنوان على أنه “مستخدم بالفعل” عند المطالبة بالعيدية أو NFT mint.
ومع ذلك، نظرًا لأن تخزين هذه المعلومات لا يستغرق سوى بت واحد، وكل فتحة 256 بت، فهذا يعني أنه يمكن تخزين 256 علامة/منطقية في فتحة تخزين واحدة.
يمكنك معرفة المزيد عن هذه التقنية من هذه الموارد:
10. استخدم SSTORE2 أو SSTORE3 لتخزين الكثير من البيانات
SSTORE
SSTORE هو كود تشغيل EVM يسمح لنا بتخزين البيانات المستمرة على أساس المفتاح و القيمة . كما هو الحال في كل شيء في EVM، فإن المفتاح والقيمة هما قيمتان تبلغان 32 بايت.
تكاليف الكتابة (SSTORE) والقراءة (SLOAD) باهظة جدًا من حيث استهلاك الغاز. كتابة 32 بايت تكلف 22100 غاز، وهو ما يترجم إلى حوالي 690 غاز لكل بايت. من ناحية أخرى، فإن كتابة bytecode للعقد الذكي تكلف 200 غاز لكل بايت.
SSTORE2
يعد SSTORE2 مفهومًا فريدًا من حيث أنه يستخدم bytecode للعقد لكتابة البيانات وتخزينها. ولتحقيق ذلك، نستخدم خاصية immutability المتأصلة في bytecode .
بعض خصائص SSTORE2:
- يمكننا أن نكتب مرة واحدة فقط. استخدام CREATE بشكل فعال بدلاً من SSTORE.
- للقراءة، بدلًا من استخدام SLOAD، نستخدم الآن EXTCODECOPY على العنوان المنشور حيث يتم تخزين البيانات المحددة كـ bytecode.
- تصبح كتابة البيانات أرخص بكثير عندما يلزم تخزين المزيد والمزيد من البيانات.
مثال:
كتابة البيانات
هدفنا هو تخزين بيانات محددة (بتنسيق بايت) كرمز بايت للعقد. ولتحقيق ذلك علينا القيام بأمرين:-
- انسخ بياناتنا إلى الذاكرة أولاً، حيث يقوم EVM بعد ذلك بأخذ هذه البيانات من الذاكرة وتخزينها ككود وقت التشغيل (runtime code). يمكنك معرفة المزيد في مقالتنا حول رمز إنشاء العقد.
- قم بإرجاع وتخزين عنوان العقد الذي تم نشره حديثًا لاستخدامه في المستقبل.
- نضيف حجم كود العقد بدلاً من الأصفار الأربعة (0000) بين 61 و 80 في الكود أدناه 0x61000080600a3d393df300. ومن ثم إذا كان حجم الكود 65، فسيصبح 0x61004180600a3d393df300(0x0041 = 65)
- هذا bytecode هو المسؤول عن الخطوة 1 التي ذكرناها.
- نعيد الآن العنوان الذي تم نشره حديثًا للخطوة 2.
و بالتاليbytecode للعقد النهائي = 00 + البيانات (00 = STOP مُلحق مسبقًا لضمان عدم إمكانية تنفيذ bytecode عن طريق الاتصال بالعنوان عن طريق الخطأ)
قراءة البيانات
- للحصول على البيانات ذات الصلة، تحتاج إلى العنوان الذي قمت بتخزين البيانات فيه.
- نعود إذا كان حجم الكود = 0 لأسباب واضحة.
- الآن نقوم ببساطة بإرجاع bytecode للعقد من موضع البداية ذي الصلة والذي يكون بعد 1 بايت (تذكر أن البايت الأول هو STOP OPCODE(0x00)).
معلومات إضافية للفضوليين:
- يمكننا أيضًا استخدام العنوان المحدد مسبقًا باستخدام CREATE2 لحساب عنوان المؤشر خارج السلسلة أو على السلسلة دون الاعتماد على تخزين المؤشر.
المرجع: سولادي
SSTORE3
لفهم SSTORE3، دعونا أولاً نلخص خاصية مهمة لـ SSTORE2.
- يعتمد العنوان المنشور حديثًا على البيانات التي نعتزم تخزينها.
كتابة البيانات
ينفذ SSTORE3 تصميمًا بحيث يكون العنوان المنشور حديثًا مستقلاً عن البيانات المقدمة لدينا. يتم تخزين البيانات المقدمة أولاً في وحدة التخزين باستخدام SSTORE. ثم نقوم بتمرير INIT_CODE ثابت كبيانات في CREATE2 والذي يقرأ داخليًا البيانات المقدمة المخزنة في وحدة التخزين لنشرها ككود.
يمكّننا خيار التصميم هذا من حساب عنوان المؤشر لبياناتنا بكفاءة فقط من خلال توفير الملح (الذي يمكن أن يكون أقل من 20 بايت). وبالتالي يمكننا من تعبئة المؤشر الخاص بنا بمتغيرات أخرى، وبالتالي تقليل تكاليف التخزين.
قراءة البيانات
حاول أن تتخيل كيف يمكننا قراءة البيانات.
- الإجابة هي أنه يمكننا بسهولة حساب العنوان المنشور فقط عن طريق توفير الملح.
- ثم بعد أن نتلقى عنوان المؤشر، استخدم نفس كود التشغيل EXTCODECOPY للحصول على البيانات المطلوبة.
كي تختصر:
- يعد SSTORE2 مفيدًا في الحالات التي تكون فيها عمليات الكتابة نادرة، وتكون عمليات القراءة الكبيرة متكررة (والمؤشر> 14 بايت)
- يكون SSTORE3 أفضل عندما تكتب نادرًا جدًا، ولكن تقرأ كثيرًا. (والمؤشر <14 بايت)
المرجع: philogy
11. استخدم مؤشرات التخزين بدلاً من الذاكرة حيثما كان ذلك مناسباً
في Solidity، مؤشرات التخزين هي متغيرات تشير إلى موقع تخزين العقد. إنها ليست تمامًا نفس المؤشرات في لغات مثل C/C++.
من المفيد معرفة كيفية استخدام مؤشرات التخزين بكفاءة لتجنب قراءات التخزين غير الضرورية وإجراء تحديثات تخزين موفرة للغاز.
فيما يلي مثال يوضح أين يمكن أن تكون مؤشرات التخزين مفيدة.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
contract StoragePointerUnOptimized {
struct User {
uint256 id;
string name;
uint256 lastSeen;
}
constructor() {
users[0] = User(0, "John Doe", block.timestamp);
}
mapping(uint256 => User) public users;
function returnLastSeenSecondsAgo(uint256 _id) public view returns (uint256) {
User memory _user = users[_id];
uint256 lastSeen = block.timestamp - _user.lastSeen;
return lastSeen;
}
}
أعلاه، لدينا وظيفة تقوم بإرجاع آخر ظهور للمستخدم في فهرس معين. يحصل على قيمة lastSeen ويطرحها من block.timestamp الحالي. ثم نقوم بنسخ الـ struct بأكملها إلى الذاكرة ونحصل على lastSeen الذي نستخدمه في حساب آخر ظهور منذ ثوانٍ. تعمل هذه الطريقة بشكل جيد ولكنها ليست فعالة جدًا، وذلك لأننا نقوم بنسخ كل الـ struct من التخزين إلى الذاكرة بما في ذلك المتغيرات التي لا نحتاجها. ماذا لو كانت هناك طريقة للقراءة فقط من فتحة تخزين lastSeen (بدون إستخدام assembly). هذا هو المكان الذي تأتي فيه مؤشرات التخزين.
// This results in approximately 5,000 gas savings compared to the previous version.
contract StoragePointerOptimized {
struct User {
uint256 id;
string name;
uint256 lastSeen;
}
constructor() {
users[0] = User(0, "John Doe", block.timestamp);
}
mapping(uint256 => User) public users;
function returnLastSeenSecondsAgoOptimized(uint256 _id) public view returns (uint256) {
User storage _user = users[_id];
uint256 lastSeen = block.timestamp - _user.lastSeen;
return lastSeen;
}
}
“يؤدي التنفيذ المذكور أعلاه إلى توفير ما يقرب من 5000 غاز مقارنة بالإصدار الأول”. لماذا إذن، التغيير الوحيد هنا هو تغيير memory إلى storage وقيل لنا أن أي شيء في التخزين باهظ الثمن ويجب تجنبه؟
نقوم هنا بتخزين مؤشر التخزين users[_id] في متغير بحجم ثابت على المكدس (مؤشر الـ struct هو عبارة عن فتحة التخزين لبداية الـ struct وفي هذه الحالة، ستكون هذه هي فتحة تخزين user[_id].id ). نظرًا لأن مؤشرات التخزين كسولة (بمعنى أنها تودي فقط (القراءة أو الكتابة) عند استدعائها أو الرجوع إليها).
بعد ذلك، يمكننا الوصول فقط إلى مفتاح lastSeen الخاص بالـ struct. بهذه الطريقة نقوم بإنشاء حمل تخزين واحد ثم نقوم بتخزينه على المكدس، بدلاً من 3 أحمال تخزين أو ربما أكثر بالإضافة إلى تحزين ذاكرة قبل أخذ جزء صغير من الذاكرة إلى المكدس.
ملاحظة: عند استخدام مؤشرات التخزين، من المهم الحرص على عدم الإشارة إلى Dangling References.
12. تجنب وصول أرصدة توكنات ERC20 إلى الصفر، واحتفظ دائمًا بمبلغ صغير
يرتبط هذا بقسم تجنب الكتابة الصفرية أعلاه، لكن الأمر يستحق الإشارة إليه بشكل منفصل لأن التنفيذ دقيق بعض الشيء.
إذا كان العنوان يقوم بإفراغ (وإعادة تحميل) رصيد حسابه بشكل متكرر، فسيؤدي ذلك إلى الكثير من كتابات صفر إلى واحد.
13. العد من n إلى الصفر بدلاً من العد من صفر إلى n
عند ضبط متغير التخزين إلى الصفر، يتم استرداد المبلغ، وبالتالي فإن صافي الغاز الذي يتم إنفاقه على العد سيكون أقل إذا كانت الحالة النهائية لمتغير التخزين هي صفر.
إطرح رأيك ؟
أظهر التعليقات / إنرك تعليق