آنچه در این مقاله میخوانید [پنهانسازی]
اگر می خواهید هزینه اجرای تراکنش ها را در اتریوم کاهش دهید، نقطه شروع شناخت و پیاده سازی اصول Gas Optimization در سالیدیتی است. این راهنما به شما کمک می کند با فهم مدل هزینه در EVM، الگوهای درست کدنویسی، و ابزارهای اندازه گیری، قراردادهایی بنویسید که سریع تر و ارزان تر اجرا شوند بدون اینکه امنیت یا خوانایی را قربانی کنید.
درک مدل هزینه در EVM
گس سوخت اجرای دستورالعمل های ماشین مجازی اتریوم است. هر عملیات مانند خواندن یا نوشتن در فضای ذخیره سازی، محاسبات عددی، یا فراخوانی قرارداد دیگر هزینه متفاوتی دارد. گران ترین بخش ها معمولا نوشتن در storage، ایجاد قرارداد جدید و لاگ های سنگین هستند. هدف شما کمینه کردن نوشتارهای غیر ضروری و جابجا کردن منطق به حافظه ارزان تر است.
دسترسی به storage دو مرحله دارد: بار اول اسلات سرد است و گران تر، اما دسترسی های بعدی در همان تراکنش گرم می شوند و ارزان تر. بنابراین تجمیع عملیات روی داده های یکسان در یک تابع می تواند موثر باشد. همچنین بازپرداخت گس برای پاکسازی داده ها محدود است، پس بر پاک کردن صرف برای بازگشت گس حساب نکنید، بیشتر برای سلامت وضعیت استفاده کنید.
بهینه سازی کامپایلر هم مهم است. فعال کردن optimizer در solc و انتخاب مقدار runs مناسب بر اساس الگوی استفاده از توابع شما، می تواند جریان کد را برای سناریوهای پرتکرار ارزان تر کند. برای توابعی که زیاد فراخوانی می شوند runs بالاتر مفید است، و برای توابع سنگین اما کم تکرار runs پایین تر گاهی بهتر جواب می دهد.
الگوهای کدنویسی کم هزینه
اولین اصل، کاهش دسترسی به storage است. مقدارهای storage را در ابتدای تابع یک بار بخوانید، در متغیرهای محلی تغییر دهید، و در پایان تنها در صورت نیاز بنویسید. هر بار خواندن یا نوشتن مستقیم در حلقه ها هزینه را چند برابر می کند.
// بد
function addMany(address u, uint256 n) external {
for (uint256 i = 0; i < n; i++) {
counts[u] = counts[u] + 1; // هر بار SLOAD + SSTORE
}
}
// بهتر
function addMany(address u, uint256 n) external {
uint256 c = counts[u];
for (uint256 i = 0; i < n; i++) {
unchecked { c++; } // بدون بررسی سرریز در حوزه کنترل شده
}
counts[u] = c; // یک SSTORE
}
نوع دهی هوشمندانه هم موثر است. استفاده از uint256 معمولا از انواع کوچکتر ارزان تر است چون با معماری 256 بیتی EVM هماهنگ است. اگر به فشرده سازی داده نیاز دارید، از packing در struct ها استفاده کنید، اما مراقب هزینه های تبدیل و شیفت باشید.
struct User {
// دو مقدار 128 بیتی در یک اسلات بسته می شوند
uint128 balance;
uint128 reward;
// پرچم ها را در یک uint8 جمع کنید
uint8 flags; // بیت 0 فعال، بیت 1 مسدود و ...
}
در پارامترهای ورودی توابع external تا حد امکان از calldata استفاده کنید. کپی کردن به memory پرهزینه است، مخصوصا برای آرایه ها و رشته ها. همچنین immutable و constant برای مقادیر ثابت ارزان تر از storage هستند، چون یا به صورت ثابت در کد قرار می گیرند یا در اسلاتی با هزینه خواندن پایین تر نگهداری می شوند.
contract C {
address public immutable owner;
uint256 public constant BASIS = 10000;
constructor(address _owner) {
owner = _owner; // تنظیم یک بار، خواندن ارزان
}
function sum(uint256[] calldata xs) external pure returns (uint256 s) {
for (uint256 i; i < xs.length; ) {
s += xs[i];
unchecked { i++; } // حذف بررسی overflow برای i
}
}
}
در شرط ها از کوتاه سازی منطقی استفاده کنید و از محاسبات تکراری بپرهیزید. اگر نتیجه یک عبارت چند بار استفاده می شود، آن را در متغیر محلی ذخیره کنید. برای مدیریت خطاها از custom error به جای رشته های طولانی revert استفاده کنید تا کد بایت شما کوچک تر و ارزان تر شود.
error NotAuthorized();
function doThing() external {
if (msg.sender != owner) revert NotAuthorized();
// منطق تابع
}
الگوهای طراحی کم هزینه در سطح معماری
پرداخت ها را با الگوی pull پیاده کنید نه push. یعنی به جای ارسال خودکار وجه به گیرنده ها و تحمل هزینه های فراخوانی، موجودی آنها را در قرارداد ثبت کنید تا هر زمان خواستند برداشت کنند. این کار علاوه بر کاهش هزینه و ریسک، از شکست تراکنش های گروهی جلوگیری می کند.
برای استقرارهای پرتعداد از مینیمال پروکسی استاندارد ERC 1167 استفاده کنید. این الگو با اشتراک گذاری منطق و استقرار پروکسی های سبک، هزینه استقرار را به شدت کاهش می دهد. اگر با کتابخانه ها کار می کنید، توجه داشته باشید لینک شدن کتابخانه خارجی می تواند هم اندازه قرارداد و هم هزینه فراخوانی را متاثر کند؛ گاهی inline کردن توابع ساده ارزان تر است.
اندازه کد اهمیت دارد. محدودیت اندازه کد قرارداد وجود دارد و هر بایت اضافی، هم هزینه استقرار و هم هزینه دسترسی به کد را افزایش می دهد. حذف کد مرده، ادغام توابع به اشتراک گذار، و جایگزینی رشته های طولانی با ثابت های کوتاه یا error های سفارشی به کوچک شدن کد کمک می کنند.
ساختار داده مناسب انتخاب کنید. برای بررسی عضویت مجموعه ها mapping بهتر از جستجوی خطی در آرایه است. اگر نیاز به شمارش دارید، یک آرایه اندیس دار در کنار mapping نگه دارید تا اضافه و حذف مقرون به صرفه باقی بماند. برای داده های گذرا مثل نتایج میانی، از memory استفاده کنید نه storage.
اندازه گیری، پروفایل و تنظیمات کامپایلر
بدون اندازه گیری، بهینه سازی کور است. از فریم ورک هایی مثل Foundry و Hardhat برای گزارش گس استفاده کنید. با تست های هدفمند، مسیرهای پرتکرار را بسنجید و قبل و بعد از هر تغییر، تفاوت را ثبت کنید. داشتن snapshot از گس توابع، به شما دیدی روشن از تاثیر هر اصلاح می دهد.
- Hardhat Gas Reporter: افزونه ای که در تست ها هزینه هر تابع را نشان می دهد.
- Foundry gas snapshots: مقایسه خودکار گس قبل و بعد از تغییرات.
- trace و debug: نمایش فراخوانی های تودرتو و هزینه هر قدم برای شناسایی گلوگاه.
تنظیمات کامپایلر را آگاهانه انتخاب کنید. optimizer را روشن کنید و پارامتر runs را بر اساس بار کاری واقعی تعیین کنید. فعال سازی IR و Yul optimizer می تواند مسیرهای پرهزینه را ساده تر کند، اما همواره خروجی را تست کنید تا از عدم تغییر رفتار اطمینان حاصل شود.
- رکورد گیری از گس توابع کلیدی در وضعیت فعلی.
- اعمال یک تغییر کوچک و ایزوله با فرض بهبود.
- اجرای مجدد تست ها و مقایسه عددی با snapshot.
اندازه گیری را به بخشی از CI تبدیل کنید تا هر تغییری که باعث پسرفت گس می شود، زود تشخیص داده شود. شفافیت عددی، تصمیم گیری شما را دقیق تر و تیم را همسو می کند.
جدول میانبرهای کم هزینه
الگوهای زیر به تصمیم گیری سریع در زمان نوشتن کد کمک می کنند.
| الگوی پر هزینه | جایگزین یا راهکار |
|---|---|
| نوشتن مکرر در storage در حلقه | کش در متغیر محلی و یک بار نوشتن در پایان |
| کپی آرایه به memory در توابع external | استفاده از calldata و دسترسی مستقیم |
| رشته طولانی در require و revert | custom error با پارامترهای لازم |
| جستجوی عضویت در آرایه | mapping برای O(1) و نگهداری شمارنده |
| انواع کوچک ناهمراستا در struct | packing آگاهانه یا استفاده از uint256 برای محاسبه |
| فراخوانی قرارداد پرهزینه برای پرداخت | الگوی pull و برداشت توسط گیرنده |
نکات ظریف که معمولا نادیده می مانند
انواع کوچکتر همیشه ارزان تر نیستند. وقتی چند مقدار کوچک را در یک اسلات نمی گنجانید، استفاده از uint256 به دلیل همخوانی با EVM عموما کاراتر است. تنها زمانی سراغ انواع کوچک بروید که packing واقعی انجام می دهید یا محدودیت دامنه منطقی وجود دارد.
unchecked را با احتیاط به کار ببرید. در حلقه های با گام و شرط کنترل شده، حذف بررسی سرریز روی شمارنده ها مفید است، اما برای مقادیری که از ورودی کاربر می آیند یا ممکن است جمع شوند، از ایمنی عبور نکنید. هزینه یک باگ عددی به مراتب بیشتر از صرفه جویی اندک در گس است.
وقتی حالت را به مقدار پیش فرض برمی گردانید، delete و انتساب صفر تفاوت رفتاری در گس می تواند داشته باشد. هدف شما باید حداقل نوشتن باشد، بنابراین اگر نیازی به پاکسازی مکرر نیست، از ایجاد و حذف بیهوده داده خودداری کنید.
external در مقایسه با public برای توابعی که فقط از خارج صدا زده می شوند اندکی بهینه تر است، زیرا پارامترها از calldata خوانده می شوند. با این حال معیار اصلی شما باید قراردادهای فراخواننده و مرزهای معماری باشد. انتخاب اشتباه سطح دسترسی به خاطر چند واحد گس ارزش ریسک امنیتی ندارد.
نمونه هایی از بازطراحی های موثر
جایگزینی آرایه قابل جستجو با mapping
// قبل
address[] users;
function isUser(address u) public view returns (bool) {
for (uint256 i; i < users.length; i++) {
if (users[i] == u) return true;
}
return false;
}
// بعد
mapping(address => bool) isUserMap;
function isUser(address u) public view returns (bool) {
return isUserMap[u];
}
کاهش رویدادهای سنگین
// قبل: سه اندیس، داده زیاد
event Transfer(address indexed from, address indexed to, address indexed ref, uint256 amount, bytes data);
// بعد: اندیس های ضروری و حذف داده حجیم
event TransferLight(address indexed from, address indexed to, uint256 amount);
تجمیع کارها در یک گذر
// قبل: دو تابع جدا، دو بار دسترسی به storage
function incA() internal { a = a + 1; }
function incB() internal { b = b + 1; }
// بعد: تجمیع برای استفاده از اسلات های گرم
function incBoth() internal {
uint256 _a = a;
uint256 _b = b;
unchecked { _a++; _b++; }
a = _a;
b = _b;
}
مسیر عملی برای شروع
برای شروع به صورت سیستماتیک عمل کنید. یک چک لیست کوچک داشته باشید و روی بخش های پرمصرف تمرکز کنید، سپس به سراغ بهینه سازی های ظریف بروید. استفاده ابزاری از assembly را فقط وقتی در دسترس بودن عملکرد بهتر ثابت شد در نظر بگیرید، زیرا هزینه های نگهداری و ریسک امنیتی بالاتری دارد.
- فعال کردن optimizer و تعیین runs مبتنی بر پروفایل واقعی.
- کاهش نوشتار در storage با کش محلی و بازطراحی حلقه ها.
- تعویض require های متنی با custom error و سبک کردن رویدادها.
در این فرآیند، از تست های واحد فراوان و شبیه سازی سناریوهای بدترین حالت استفاده کنید. هدف کاهش هزینه بدون تغییر معنی و تضمین امنیت است. بهینه سازی های کوچک اما درست، در مقیاس کاربران زیاد، صرفه جویی بزرگی ایجاد می کند.
جمع بندی
بهینه سازی گس ترکیبی از درک عمیق EVM، انتخاب ساختار داده مناسب، و انضباط در اندازه گیری است. با تمرکز بر کاهش دسترسی به storage، استفاده از calldata و constant ها، ساده کردن رویدادها و اعمال تغییرات مبتنی بر داده، به نتایج قابل توجه می رسید. این اصول را تدریجی و مستند به کار ببرید تا هم هزینه ها کاهش یابد و هم نگهداری و امنیت قراردادها حفظ شود. در نهایت، Gas Optimization در سالیدیتی یک تمرین مستمر است که با هر به روزرسانی ابزارها و پروتکل، فرصت های تازه ای برای بهبود پیش روی شما می گذارد.






