در حالی که جاوا اسکریپت در پاکسازی پس از خود نسبتاً بخشنده است، زبان های ایستا قطعاً ...
Squoosh.app یک PWA است که نشان می دهد کدک ها و تنظیمات مختلف تصویر چقدر می توانند اندازه فایل تصویر را بدون تأثیر قابل توجهی بر کیفیت بهبود دهند. با این حال، این همچنین یک نسخه ی نمایشی فنی است که نشان می دهد چگونه می توانید کتابخانه های نوشته شده در C++ یا Rust را بگیرید و آنها را به وب بیاورید.
توانایی انتقال کد از اکوسیستمهای موجود بسیار ارزشمند است، اما تفاوتهای کلیدی بین این زبانهای ثابت و جاوا اسکریپت وجود دارد. یکی از آنها در رویکردهای متفاوت آنها به مدیریت حافظه است.
در حالی که جاوا اسکریپت در پاکسازی پس از خود نسبتاً بخشنده است، چنین زبان های ایستا قطعاً چنین نیستند. شما باید صریحاً یک حافظه اختصاص داده شده جدید بخواهید و واقعاً باید مطمئن شوید که پس از آن آن را پس داده اید و دیگر هرگز از آن استفاده نکنید. اگر این اتفاق نیفتد، شما نشت می کنید... و در واقع به طور منظم اتفاق می افتد. بیایید نگاهی بیندازیم که چگونه میتوانید این نشتهای حافظه را اشکال زدایی کنید و حتی بهتر، چگونه میتوانید کد خود را طراحی کنید تا دفعه بعد از آنها جلوگیری کنید.
الگوی مشکوک
اخیراً هنگام شروع کار بر روی Squoosh، متوجه الگوی جالبی در بستهبندی کدک C++ نشدم. بیایید نگاهی به پوشش ImageQuant به عنوان مثال بیندازیم (کاهش داده شده تا فقط قسمتهای ایجاد و توزیع شیء را نشان دهد):
liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;
RawImage quantize(std::string rawimage,
int image_width,
int image_height,
int num_colors,
float dithering) {
const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
int size = image_width * image_height;
attr = liq_attr_create();
image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_set_max_colors(attr, num_colors);
liq_image_quantize(image, attr, &res);
liq_set_dithering_level(res, dithering);
uint8_t* image8bit = (uint8_t*)malloc(size);
result = (uint8_t*)malloc(size * 4);
// …
free(image8bit);
liq_result_destroy(res);
liq_image_destroy(image);
liq_attr_destroy(attr);
return {
val(typed_memory_view(image_width * image_height * 4, result)),
image_width,
image_height
};
}
void free_result() {
free(result);
}
جاوا اسکریپت (خوب، TypeScript):
export async function process(data: ImageData, opts: QuantizeOptions) {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
}
const module = await emscriptenModule;
const result = module.quantize(/* … */);
module.free_result();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}
آیا مشکلی را تشخیص می دهید؟ نکته: پس از استفاده رایگان است، اما در جاوا اسکریپت!
در Emscripten، typed_memory_view
یک جاوا اسکریپت Uint8Array
را برمیگرداند که توسط بافر حافظه WebAssembly (Wasm) پشتیبانی میشود و byteOffset
و byteLength
روی اشارهگر و طول داده شده تنظیم شدهاند. نکته اصلی این است که این یک نمای TypedArray در بافر حافظه WebAssembly است، نه یک کپی متعلق به جاوا اسکریپت از داده ها.
هنگامی که ما free_result
از جاوا اسکریپت فراخوانی می کنیم، آن نیز به نوبه خود، یک تابع استاندارد C را فراخوانی می free
تا این حافظه را به عنوان در دسترس برای هر تخصیص آینده علامت گذاری کند، به این معنی که داده هایی که دیدگاه Uint8Array
ما به آنها اشاره می کند، می توانند با داده های دلخواه با هر فراخوانی آینده بازنویسی شوند. به Wasm.
یا، برخی از پیادهسازی free
حتی ممکن است تصمیم بگیرند که بلافاصله حافظه آزاد شده را صفر کنند. free
که Emscripten استفاده می کند این کار را نمی کند، اما ما در اینجا به جزئیات پیاده سازی تکیه می کنیم که نمی توان آن را تضمین کرد.
یا حتی اگر حافظه پشت اشاره گر حفظ شود، ممکن است نیاز به تخصیص جدید برای رشد حافظه WebAssembly باشد. هنگامی که WebAssembly.Memory
یا از طریق JavaScript API یا دستورالعمل مربوط به memory.grow
رشد می کند، ArrayBuffer
موجود و به طور موقت، هر نما که توسط آن پشتیبانی می شود، بی اعتبار می شود.
اجازه دهید از کنسول DevTools (یا Node.js) برای نشان دادن این رفتار استفاده کنم:
> memory = new WebAssembly.Memory({ initial: 1 })
Memory {}
> view = new Uint8Array(memory.buffer, 42, 10)
Uint8Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// ^ all good, we got a 10 bytes long view at address 42
> view.buffer
ArrayBuffer(65536) {}
// ^ its buffer is the same as the one used for WebAssembly memory
// (the size of the buffer is 1 WebAssembly "page" == 64KB)
> memory.grow(1)
1
// ^ let's say we grow Wasm memory by +1 page to fit some new data
> view
Uint8Array []
// ^ our original view is no longer valid and looks empty!
> view.buffer
ArrayBuffer(0) {}
// ^ its buffer got invalidated as well and turned into an empty one
در نهایت، حتی اگر صراحتاً بین free_result
و new Uint8ClampedArray
مجدداً Wasm را فراخوانی نکنیم، ممکن است در برخی مواقع پشتیبانی چند رشتهای را به کدکهای خود اضافه کنیم. در این صورت ممکن است یک رشته کاملاً متفاوت باشد که درست قبل از اینکه بتوانیم آن را شبیه سازی کنیم، داده ها را بازنویسی می کند.
به دنبال اشکالات حافظه
در هر صورت، من تصمیم گرفتم بیشتر بروم و بررسی کنم که آیا این کد در عمل مشکلی را نشان می دهد یا خیر. به نظر میرسد این یک فرصت عالی برای آزمایش پشتیبانی جدید (ish) ضد عفونیکنندههای Emscripten است که سال گذشته اضافه شد و در سخنرانی WebAssembly ما در جلسه Chrome Dev Summit ارائه شد:
در این مورد، ما به AddressSanitizer علاقه مند هستیم که می تواند مسائل مختلف مربوط به اشاره گر و حافظه را شناسایی کند. برای استفاده از آن، باید کدک خود را با -fsanitize=address
دوباره کامپایل کنیم:
emcc \
--bind \
${OPTIMIZE} \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="imagequant"' \
-I node_modules/libimagequant \
-o ./imagequant.js \
--std=c++11 \
imagequant.cpp \
-fsanitize=address \
node_modules/libimagequant/libimagequant.a
این به طور خودکار بررسی های ایمنی اشاره گر را فعال می کند، اما ما همچنین می خواهیم نشت حافظه احتمالی را پیدا کنیم. از آنجایی که ما از ImageQuant به عنوان کتابخانه به جای برنامه استفاده می کنیم، هیچ "نقطه خروجی" وجود ندارد که Emscripten بتواند به طور خودکار آزاد شدن تمام حافظه را تأیید کند.
درعوض، برای چنین مواردی، LeakSanitizer (که در AddressSanitizer موجود است) توابع __lsan_do_leak_check
و __lsan_do_recoverable_leak_check
را ارائه میکند، که میتوان هر زمان که انتظار داشتیم تمام حافظه آزاد شود و بخواهیم آن فرض را تأیید کنیم، به صورت دستی فراخوانی میشوند. __lsan_do_leak_check
برای استفاده در پایان یک برنامه کاربردی در حال اجرا است، زمانی که میخواهید در صورت شناسایی هرگونه نشتی، فرآیند را متوقف کنید، در حالی که __lsan_do_recoverable_leak_check
برای موارد استفاده کتابخانهای مانند ما مناسبتر است، زمانی که میخواهید نشت اطلاعات را در کنسول چاپ کنید. ، اما برنامه را بدون توجه به اجرا نگه دارید.
بیایید کمک کننده دوم را از طریق Embind در معرض نمایش بگذاریم تا بتوانیم هر زمان که بخواهیم آن را از جاوا اسکریپت فراخوانی کنیم:
#include <sanitizer/lsan_interface.h>
// …
void free_result() {
free(result);
}
EMSCRIPTEN_BINDINGS(my_module) {
function("zx_quantize", &zx_quantize);
function("version", &version);
function("free_result", &free_result);
function("doLeakCheck", &__lsan_do_recoverable_leak_check);
}
و هنگامی که کار با تصویر تمام شد، آن را از سمت جاوا اسکریپت فراخوانی کنید. انجام این کار از سمت جاوا اسکریپت، به جای C++، کمک می کند تا اطمینان حاصل شود که تمام محدوده ها خارج شده اند و تمام اشیاء موقت C++ تا زمانی که آن بررسی ها را اجرا می کنیم آزاد شده اند:
// …
const result = opts.zx
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
: module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);
module.free_result();
module.doLeakCheck();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}
این گزارشی مانند زیر را در کنسول به ما می دهد:
اوه-اوه، چند نشت کوچک وجود دارد، اما stacktrace چندان مفید نیست زیرا همه نامهای توابع مخدوش هستند. بیایید با یک اطلاعات اولیه اشکال زدایی دوباره کامپایل کنیم تا آنها را حفظ کنیم:
emcc \
--bind \
${OPTIMIZE} \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="imagequant"' \
-I node_modules/libimagequant \
-o ./imagequant.js \
--std=c++11 \
imagequant.cpp \
-fsanitize=address \
-g2 \
node_modules/libimagequant/libimagequant.a
این خیلی بهتر به نظر می رسد:
برخی از قسمتهای stacktrace همچنان مبهم به نظر میرسند زیرا به قسمتهای داخلی Emscripten اشاره میکنند، اما میتوانیم بگوییم که نشت از تبدیل RawImage
به "نوع سیم" (به یک مقدار جاوا اسکریپت) توسط Embind است. در واقع، وقتی به کد نگاه میکنیم، میبینیم که نمونههای RawImage
C++ را به جاوا اسکریپت برمیگردانیم، اما هرگز آنها را در هر دو طرف آزاد نمیکنیم.
به عنوان یادآوری، در حال حاضر هیچ ادغام جمع آوری زباله بین جاوا اسکریپت و WebAssembly وجود ندارد، اگرچه یکی در حال توسعه است . در عوض، پس از اتمام کار با شی، باید به صورت دستی هر حافظه و مخربکنندههایی را از سمت جاوا اسکریپت فراخوانی کنید. به طور خاص برای Embind، اسناد رسمی پیشنهاد میکنند که یک متد .delete()
در کلاسهای C++ در معرض نمایش فراخوانی کنید:
کد جاوا اسکریپت باید صراحتاً هر دسته شیء C++ را که دریافت کرده است حذف کند، در غیر این صورت پشته Emscripten به طور نامحدود رشد خواهد کرد.
var x = new Module.MyClass; x.method(); x.delete();
در واقع، وقتی این کار را در جاوا اسکریپت برای کلاس خود انجام می دهیم:
// …
const result = opts.zx
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
: module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);
module.free_result();
result.delete();
module.doLeakCheck();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}
نشتی همانطور که انتظار می رفت برطرف می شود.
کشف مشکلات بیشتر با مواد ضدعفونی کننده
ساخت دیگر کدکهای Squoosh با ضدعفونیکنندهها هم مشکلات مشابه و هم برخی از مسائل جدید را آشکار میکند. به عنوان مثال، من این خطا را در اتصالات MozJPEG دارم:
در اینجا، این یک نشت نیست، بلکه ما به خاطره ای خارج از مرزهای اختصاص داده شده می نویسیم.
با کاوش در کد MozJPEG، متوجه میشویم که مشکل اینجاست که jpeg_mem_dest
- تابعی که برای تخصیص یک مقصد حافظه برای JPEG استفاده میکنیم - از مقادیر موجود outbuffer
استفاده میکند و زمانی که آنها غیر صفر هستند، outsize
استفاده میکند :
if (*outbuffer == NULL || *outsize == 0) {
/* Allocate initial buffer */
dest->newbuffer = *outbuffer = (unsigned char *) malloc(OUTPUT_BUF_SIZE);
if (dest->newbuffer == NULL)
ERREXIT1(cinfo, JERR_OUT_OF_MEMORY, 10);
*outsize = OUTPUT_BUF_SIZE;
}
با این حال، ما آن را بدون مقداردهی اولیه هیچ یک از آن متغیرها فراخوانی می کنیم، به این معنی که MozJPEG نتیجه را در یک آدرس حافظه بالقوه تصادفی می نویسد که اتفاقاً در آن متغیرها در زمان فراخوانی ذخیره می شود!
uint8_t* output;
unsigned long size;
// …
jpeg_mem_dest(&cinfo, &output, &size);
صفر کردن هر دو متغیر قبل از فراخوانی این مشکل را حل می کند و اکنون کد به جای آن به بررسی نشت حافظه می رسد. خوشبختانه، بررسی با موفقیت انجام شد، که نشان می دهد ما هیچ گونه نشتی در این کدک نداریم.
مسائل مربوط به حالت مشترک
... یا ما؟
ما می دانیم که پیوندهای کدک ما مقداری از حالت را ذخیره می کند و همچنین منجر به متغیرهای ثابت جهانی می شود و MozJPEG ساختارهای پیچیده ای دارد.
uint8_t* last_result;
struct jpeg_compress_struct cinfo;
val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
// …
}
چه میشود اگر برخی از آنها در اولین اجرا بهطور تنبلی مقداردهی اولیه شوند، و سپس بهطور نادرست در اجراهای بعدی دوباره استفاده شوند؟ سپس یک تماس با یک ضد عفونی کننده آنها را به عنوان مشکل ساز گزارش نمی کند.
بیایید سعی کنیم تصویر را چند بار با کلیک کردن تصادفی روی سطوح مختلف کیفیت در رابط کاربری پردازش کنیم. در واقع، اکنون گزارش زیر را دریافت می کنیم:
262144 بایت—به نظر می رسد که کل تصویر نمونه از jpeg_finish_compress
لو رفته است!
پس از بررسی اسناد و نمونه های رسمی، معلوم شد که jpeg_finish_compress
حافظه اختصاص داده شده توسط تماس قبلی jpeg_mem_dest
ما را آزاد نمی کند - بلکه فقط ساختار فشرده سازی را آزاد می کند، حتی اگر ساختار فشرده سازی از قبل از مقصد حافظه ما مطلع باشد... آه.
ما میتوانیم این مشکل را با آزاد کردن دادهها به صورت دستی در تابع free_result
برطرف کنیم:
void free_result() {
/* This is an important step since it will release a good deal of memory. */
free(last_result);
jpeg_destroy_compress(&cinfo);
}
من میتوانستم آن اشکالات حافظه را یکی یکی دنبال کنم، اما فکر میکنم تا کنون به اندازه کافی واضح است که رویکرد فعلی به مدیریت حافظه منجر به مشکلات سیستماتیک بدی میشود.
برخی از آنها را می توان بلافاصله توسط ضدعفونی کننده گرفت. برخی دیگر برای دستگیر شدن به ترفندهای پیچیده نیاز دارند. در نهایت، مسائلی مانند ابتدای پست وجود دارد که همانطور که از لاگ ها می بینیم، به هیچ وجه توسط ضدعفونی کننده دستگیر نمی شوند. دلیل آن این است که استفاده نادرست واقعی در سمت جاوا اسکریپت اتفاق میافتد، جایی که ضدعفونیکننده در آن قابل مشاهده نیست. این مسائل تنها در زمان تولید یا پس از تغییرات ظاهرا نامرتبط در کد در آینده خود را نشان خواهند داد.
ساخت لفاف امن
بیایید چند قدم به عقب برگردیم و در عوض همه این مشکلات را با بازسازی کد به روشی امن تر برطرف کنیم. من دوباره از ImageQuant wrapper به عنوان مثال استفاده خواهم کرد، اما قوانین refactoring مشابه برای همه کدک ها و همچنین سایر پایگاه های کد مشابه اعمال می شود.
اول از همه، بیایید مشکل استفاده پس از رایگان را از ابتدای پست برطرف کنیم. برای این کار، باید داده ها را از نمای WebAssembly-backed قبل از علامت گذاری به عنوان رایگان در سمت جاوا اسکریپت کلون کنیم:
// …
const result = /* … */;
const imgData = new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
module.free_result();
result.delete();
module.doLeakCheck();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
return imgData;
}
اکنون، بیایید مطمئن شویم که هیچ حالتی را در متغیرهای سراسری بین فراخوان ها به اشتراک نمی گذاریم. این هم برخی از مشکلاتی را که قبلاً دیدهایم برطرف میکند و هم استفاده از کدکهای ما را در یک محیط چند رشتهای در آینده آسانتر میکند.
برای انجام این کار، C++ wrapper را تغییر می دهیم تا مطمئن شویم که هر فراخوانی به تابع داده های خود را با استفاده از متغیرهای محلی مدیریت می کند. سپس، میتوانیم امضای تابع free_result
خود را تغییر دهیم تا نشانگر را بپذیریم:
liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;
RawImage quantize(std::string rawimage,
int image_width,
int image_height,
int num_colors,
float dithering) {
const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
int size = image_width * image_height;
attr = liq_attr_create();
image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_attr* attr = liq_attr_create();
liq_image* image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_set_max_colors(attr, num_colors);
liq_result* res = nullptr;
liq_image_quantize(image, attr, &res);
liq_set_dithering_level(res, dithering);
uint8_t* image8bit = (uint8_t*)malloc(size);
result = (uint8_t*)malloc(size * 4);
uint8_t* result = (uint8_t*)malloc(size * 4);
// …
}
void free_result() {
void free_result(uint8_t *result) {
free(result);
}
اما، از آنجایی که ما در حال حاضر از Embind در Emscripten برای تعامل با جاوا اسکریپت استفاده میکنیم، ممکن است API را با پنهان کردن کامل جزئیات مدیریت حافظه C++ ایمنتر کنیم!
برای آن، اجازه دهید بخش new Uint8ClampedArray(…)
از جاوا اسکریپت به سمت C++ با Embind منتقل کنیم. سپس، میتوانیم از آن برای کلون کردن دادهها در حافظه جاوا اسکریپت حتی قبل از بازگشت از تابع استفاده کنیم:
class RawImage {
public:
val buffer;
int width;
int height;
RawImage(val b, int w, int h) : buffer(b), width(w), height(h) {}
};
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
RawImage quantize(/* … */) {
val quantize(/* … */) {
// …
return {
val(typed_memory_view(image_width * image_height * 4, result)),
image_width,
image_height
};
val js_result = Uint8ClampedArray.new_(typed_memory_view(
image_width * image_height * 4,
result
));
free(result);
return js_result;
}
توجه داشته باشید که چگونه با یک تغییر، هم اطمینان حاصل می کنیم که آرایه بایت حاصل متعلق به جاوا اسکریپت است و توسط حافظه WebAssembly پشتیبانی نمی شود و هم از شر بسته بندی RawImage
که قبلاً فاش شده بود خلاص می شویم.
اکنون جاوا اسکریپت دیگر نیازی به نگرانی در مورد آزاد کردن داده ها ندارد و می تواند از نتیجه مانند هر شیء جمع آوری زباله دیگری استفاده کند:
// …
const result = /* … */;
const imgData = new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
module.free_result();
result.delete();
// module.doLeakCheck();
return imgData;
return new ImageData(result, result.width, result.height);
}
این همچنین به این معنی است که ما دیگر نیازی به اتصال free_result
سفارشی در سمت C++ نداریم:
void free_result(uint8_t* result) {
free(result);
}
EMSCRIPTEN_BINDINGS(my_module) {
class_<RawImage>("RawImage")
.property("buffer", &RawImage::buffer)
.property("width", &RawImage::width)
.property("height", &RawImage::height);
function("quantize", &quantize);
function("zx_quantize", &zx_quantize);
function("version", &version);
function("free_result", &free_result, allow_raw_pointers());
}
در مجموع، کد بسته بندی ما در عین حال تمیزتر و ایمن تر شد.
پس از این، چند پیشرفت جزئی در کد بسته بندی ImageQuant انجام دادم و اصلاحات مدیریت حافظه مشابه را برای سایر کدک ها تکرار کردم. اگر به جزئیات بیشتر علاقه دارید، می توانید PR حاصل را اینجا ببینید: رفع حافظه برای کدک های ++C .
غذای آماده
چه درسهایی میتوانیم یاد بگیریم و چه درسهایی را از این refactoring به اشتراک بگذاریم که میتواند در سایر پایگاههای کد اعمال شود؟
- از نماهای حافظه ای که توسط WebAssembly پشتیبانی می شود - مهم نیست که از کدام زبان ساخته شده است - فراتر از یک فراخوان استفاده نکنید. شما نمی توانید به زنده ماندن آنها بیشتر از این تکیه کنید و نمی توانید این اشکالات را با روش های معمولی پیدا کنید، بنابراین اگر نیاز به ذخیره داده ها برای بعد دارید، آن را در سمت جاوا اسکریپت کپی کنید و در آنجا ذخیره کنید.
- در صورت امکان، به جای کار کردن مستقیم با اشاره گرهای خام، از یک زبان مدیریت حافظه ایمن یا حداقل از نوع پوشش های امن استفاده کنید. این شما را از اشکالات موجود در مرز JavaScript ↔ WebAssembly نجات نمی دهد، اما حداقل سطح اشکالات را کاهش می دهد که توسط کد زبان ثابت وجود دارند.
- مهم نیست از چه زبانی استفاده میکنید، کد را با ضدعفونیکنندهها در حین توسعه اجرا کنید—آنها میتوانند نه تنها به رفع مشکلات در کد زبان ثابت، بلکه برخی مشکلات در سراسر مرز JavaScript ↔ WebAssembly، مانند فراموش کردن فراخوانی
.delete()
یا عبور کمک کنند. در نشانگرهای نامعتبر از سمت جاوا اسکریپت. - در صورت امکان، از قرار دادن داده ها و اشیاء مدیریت نشده از WebAssembly به طور کلی در معرض جاوا اسکریپت خودداری کنید. جاوا اسکریپت یک زبان جمع آوری زباله است و مدیریت دستی حافظه در آن رایج نیست. این را می توان نشت انتزاعی از مدل حافظه زبانی در نظر گرفت که WebAssembly شما از آن ساخته شده است، و مدیریت نادرست به راحتی در یک پایگاه کد جاوا اسکریپت نادیده گرفته می شود.
- این ممکن است بدیهی باشد، اما، مانند هر پایگاه کد دیگری، از ذخیره حالت تغییرپذیر در متغیرهای سراسری خودداری کنید. شما نمیخواهید مشکلات مربوط به استفاده مجدد از آن را در فراخوانها یا حتی رشتههای مختلف اشکالزدایی کنید، بنابراین بهتر است تا حد امکان آن را مستقل نگه دارید.