برنامه نویسی

تخصیص حافظه در جامعه Rust – DEV

Summarize this content to 400 words in Persian Lang

معرفی

به این آموزش عمیق در مورد تخصیص حافظه در Rust خوش آمدید. به عنوان یک توسعه دهنده، درک اینکه Rust چگونه حافظه را مدیریت می کند برای نوشتن برنامه های کارآمد و ایمن بسیار مهم است. این راهنما نتیجه تجزیه و تحلیل چندین منبع خبره است تا یک نمای کلی جامع از مدیریت حافظه نه تنها در Rust، بلکه به طور کلی در زبان های برنامه نویسی به شما ارائه دهد.

توجه به این نکته مهم است که برخی از مفاهیمی که در اینجا مورد بحث قرار می‌گیرند می‌توانند بسیار پیچیده باشند و برای درک کامل ممکن است نیاز به تحقیقات بیشتری از جانب شما داشته باشند. اگر موضوعات خاصی را چالش برانگیز می دانید، ناامید نشوید – مدیریت حافظه موضوعی عمیق است و حتی توسعه دهندگان با تجربه به طور مداوم جنبه های جدیدی از آن را یاد می گیرند.

ما با مفاهیم اولیه ای که برای بسیاری از زبان های برنامه نویسی اعمال می شود شروع می کنیم و سپس بر روی پیاده سازی های خاص Rust تمرکز می کنیم. در پایان این آموزش، شما یک پایه محکم در استراتژی های تخصیص حافظه Rust و نحوه اجرای موثر آنها در پروژه های خود خواهید داشت.

مبانی چیدمان حافظه

قبل از اینکه به مفاهیم خاص Rust بپردازیم، درک طرح بندی حافظه اصلی یک برنامه ضروری است.

ساختار باینری اجرایی

هنگامی که یک برنامه Rust را کامپایل می کنید، نتیجه یک باینری قابل اجرا است. در سیستم های لینوکس، این معمولا در قالب ELF 64 است. هسته سیستم عامل طیف پیوسته ای از آدرس های حافظه مجازی را که به آدرس های حافظه فیزیکی نگاشت شده اند برای برنامه شما فراهم می کند تا از آنها استفاده کند.

بخش ها در اجرایی

یک باینری اجرایی به چند بخش تقسیم می شود:

بخش متن:

حاوی دستورالعمل های اجرایی
فقط خواندنی
بر اساس معماری CPU متفاوت است

بخش داده:

حاوی متغیرهای استاتیک اولیه (هم جهانی و هم محلی)

بخش BSS:

شامل متغیرهای جهانی بدون مقدار اولیه است

بخش پشته:

در انتهای بالای حافظه اختصاص داده شده است
به سمت پایین رشد می کند

بخش هیپ:

بین همه موضوعات به اشتراک گذاشته شده است
به سمت بالا رشد می کند

درک این بخش‌ها بسیار مهم است زیرا نقش‌های متفاوتی در تخصیص و مدیریت حافظه دارند.

پشته در مقابل هیپ

حال، بیایید روی دو نوع اصلی تخصیص حافظه در Rust تمرکز کنیم: پشته و هیپ.

پشته

پشته ناحیه ای از حافظه است که از دستور Last-In-First-Out (LIFO) پیروی می کند. استفاده می شود برای:

متغیرهای محلی
پارامترهای تابع
آدرس ها را برگردانید

ویژگی های کلیدی تخصیص پشته:

تخصیص و انتقال سریع
اندازه محدود (معمولاً 8 مگابایت در سیستم های لینوکس 64 بیتی برای رشته اصلی، 2 مگابایت برای موضوعات دیگر)
غیر تکه تکه شده

پشته

Heap ناحیه ای از حافظه است که برای تخصیص پویا استفاده می شود. توسط ویژگی اختصاص دهنده جهانی Rust مدیریت می شود که اغلب از malloc کتابخانه C در زیر کاپوت استفاده می کند. ویژگی های کلیدی عبارتند از:

اندازه انعطاف پذیر
تخصیص و توزیع کندتر در مقایسه با پشته
می تواند منجر به پراکندگی شود
بین همه موضوعات به اشتراک گذاشته شده است

تابع Stack Frames

هنگامی که یک تابع فراخوانی می شود، یک قاب پشته جدید ایجاد می شود. این قاب ذخیره می کند:

پارامترهای تابع
متغیرهای محلی
آدرس بازگشت

نشانگر پشته بالای پشته را ردیابی می کند و با فراخوانی و بازگشت توابع تغییر می کند.

رویکرد Rust به مدیریت حافظه

مدیریت حافظه Rust بر اساس دو مفهوم کلیدی ساخته شده است: مالکیت و قرض گرفتن. این قوانین به Rust اجازه می دهد تا حافظه را بدون جمع کننده زباله مدیریت کند، از ایمنی حافظه اطمینان حاصل می کند و از مشکلات رایج مانند نشانگرهای پوچ یا آویزان جلوگیری می کند.

قوانین مالکیت

هر مقدار در Rust دارای یک متغیر است که “مالک” آن است.
در هر زمان فقط یک مالک می تواند وجود داشته باشد.
وقتی مالک از محدوده خارج شود، مقدار حذف می شود.

بیایید به یک مثال نگاه کنیم:

fn main() {
let s1 = String::from(“hello”);
let s2 = s1; // ownership of the string moves to s2

// println!(“{}”, s1); // This would cause a compile-time error
println!(“{}”, s2); // This is fine
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این مثال، s1 در ابتدا صاحب رشته است. وقتی تعیین می کنیم s1 به s2، مالکیت منتقل می شود و s1 دیگر معتبر نیست

قرض گرفتن

وام گرفتن به شما این امکان را می دهد که بدون در نظر گرفتن مالکیت به یک ارزش اشاره کنید. دو نوع قرض وجود دارد:

قرض های فقط خواندنی: چندین قرض فقط خواندنی به طور همزمان مجاز است.

وام های قابل تغییر: فقط یک وام قابل تغییر در هر زمان مجاز است.

در اینجا یک مثال است:

fn main() {
let mut s = String::from(“hello”);

let r1 = &s; // read-only borrow
let r2 = &s; // another read-only borrow
println!(“{} and {}”, r1, r2);

let r3 = &mut s; // mutable borrow
r3.push_str(“, world”);
println!(“{}”, r3);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این سیستم قرض‌گیری به Rust اجازه می‌دهد تا از مسابقه داده‌ها در زمان کامپایل جلوگیری کند، که یک مزیت قابل توجه نسبت به بسیاری از زبان‌های برنامه‌نویسی دیگر است.

انواع داده ها و تخصیص حافظه

درک نحوه تخصیص انواع داده های مختلف در حافظه برای نوشتن کد Rust کارآمد بسیار مهم است.

انواع داده های اولیه

انواع داده های اولیه در Rust اندازه ثابتی دارند و در پشته ذخیره می شوند. در اینجا لیست گسترده ای از انواع اولیه آمده است:

اعداد صحیح: i8، i16، i32، i64، i128، isize، u8، u16، u32، u64، u128، usize

اعداد اعشاری: f32، f64

بولی: bool

شخصیت ها: char (مقادیر اسکالر یونیکد)
نوع واحد: () (یک تاپل خالی)

این انواع پیاده سازی می کنند Copy صفت، به این معنی که وقتی به توابع تخصیص داده می شوند یا به آنها منتقل می شوند، با مقدار کپی می شوند.

انواع داده های مرکب

تاپل ها

Tuples مقادیر مختلف را ذخیره می کند و در پشته تخصیص می یابد. آنها به طور پیوسته در حافظه قرار می گیرند، با بالشتک بالقوه برای تراز کردن. مثلا:

let tup: (i32, f64, u8) = (500, 6.4, 1);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در حافظه، این تاپل ممکن است به شکل زیر باشد:

[4 bytes for i32][4 bytes padding][8 bytes for f64][1 byte for u8][7 bytes padding]

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بالشتک تضمین می کند که هر عنصر به درستی در حافظه تراز شده است.

سازه ها

سازه ها را می توان نامگذاری کرد یا تاپل مانند. آنها معمولاً در پشته تخصیص داده می شوند، اما اگر حاوی انواعی مانند باشد، محتوای آنها می تواند در پشته باشد String یا Vec. چیدمان حافظه آنها شبیه به تاپل ها، از جمله padding بالقوه است. مثلا:

struct Point {
x: i32,
y: i32,
}

let p = Point { x: 0, y: 0 };

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Enums

Enum ها به عنوان یک متمایز (معمولا یک عدد صحیح) ذخیره می شوند تا نشان دهند کدام نوع است، به علاوه فضای کافی برای ذخیره بزرگترین نوع. این اجازه می دهد تا Rust استفاده از حافظه را بهینه کند و در عین حال ایمنی نوع را فراهم کند. تخصیص حافظه می تواند پیچیده تر از آن چیزی باشد که در ابتدا به نظر می رسد:

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این فهرست:

Quit به هیچ فضای اضافی فراتر از متمایز کننده نیاز ندارد.

Move به فضای دو نفره نیاز دارد i32 ارزش های.

Write نیاز به فضا برای a String، که یک اشاره گر به حافظه پشته است.

ChangeColor به فضای سه نفره نیاز دارد i32 ارزش های.

enum فضای کافی را برای بزرگترین نوع (احتمالا ChangeColor در این مورد)، به علاوه ممیز. این یعنی حتی Quit نوع از همان مقدار حافظه استفاده می کند ChangeColor، اما این رویکرد امکان تطبیق بسیار سریع را فراهم می کند و از نیاز به تخصیص پشته برای خود enum جلوگیری می کند.

انواع داده های پویا

آرایه

آرایه‌ها در Rust اندازه ثابتی دارند که در زمان کامپایل شناخته می‌شوند و در پشته ذخیره می‌شوند. این با برخی از زبان‌های محبوب مانند پایتون یا جاوا اسکریپت متفاوت است، جایی که آرایه‌ها (یا لیست‌ها) به صورت پویا و به صورت heap تخصیص داده می‌شوند. در رست:

let arr: [i32; 5] = [1, 2, 3, 4, 5];

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این آرایه کاملاً به‌صورت پشته‌ای تخصیص داده شده است، که می‌تواند منجر به استفاده بسیار کارآمد از حافظه و الگوهای دسترسی برای مجموعه‌های با اندازه ثابت شود.

بردار

بردارها قابل تغییر اندازه هستند و داده های خود را روی پشته ذخیره می کنند. آنها ظرفیت و طول را پیگیری می کنند:

let mut vec: Vec<i32> = Vec::new();
vec.push(1);
vec.push(2);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تکه

برش ها نماهایی از عناصر یک آرایه یا بردار هستند. آنها از یک اشاره گر چربی برای مرجع استفاده می کنند که هم نشانگر داده و هم طول دارد. نشانگر چربی نشانگری است که اطلاعات اضافی را فراتر از آدرس حافظه حمل می کند. در مورد یک برش، نشانگر چربی حاوی:

اشاره گر به اولین عنصر برش در حافظه
طول برش

این اطلاعات اضافی به Rust اجازه می دهد تا بدون نیاز به ذخیره این اطلاعات به طور جداگانه یا پرس و جو در زمان اجرا، کرانه ها را بررسی و بر روی برش به طور موثر تکرار کند.

let slice: &[i32] = &arr[1..3];

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

رشته

رشته ها در Rust شبیه بردارها هستند اما تضمین شده است که UTF-8 رمزگذاری شده اند. این تضمین یعنی:

هر کاراکتر در رشته با یک دنباله بایت معتبر UTF-8 نشان داده می شود.
رشته می تواند حاوی هر کاراکتر یونیکد باشد، اما آنها به طور موثر ذخیره می شوند.
عملیات رشته ای (مانند نمایه سازی) بر روی مرزهای UTF-8 کار می کند، نه بایت های خام.

این ضمانت UTF-8 به Rust اجازه می دهد تا مدیریت رشته ای ایمن و کارآمد را ارائه دهد، و از مسائلی مانند توالی بایت های نامعتبر یا مرزهای کاراکترهای نادرست که می تواند در زبان هایی با رمزگذاری رشته های کمتر سختگیرانه رخ دهد، جلوگیری کند.

let s = String::from(“hello”);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تخصیص دهنده های حافظه در Rust

Rust انعطاف پذیری را در انتخاب تخصیص دهنده های حافظه فراهم می کند. بیایید برخی از گزینه های رایج را بررسی کنیم:

تخصیص دهنده استاندارد

تخصیص دهنده استاندارد در Rust از تخصیص دهنده پیش فرض سیستم (اغلب کتابخانه C) استفاده می کند malloc). این یک تخصیص دهنده همه منظوره خوب است اما ممکن است برای همه سناریوها کارآمدترین نباشد.

مشخصات:

استفاده می کند sbrk برای رشد پشته
حافظه به عنوان Resident Set Size (RSS) محاسبه می شود
سریعترین یا کارآمدترین حافظه نیست
ردپای حافظه کم هنگام شروع اولیه

جمالوک

jemalloc یک تخصیص دهنده جایگزین محبوب است که به دلیل کارایی آن در محیط های چند رشته ای شناخته شده است.

مشخصات:

استفاده می کند mmap برای تخصیص حافظه
حافظه فقط زمانی که روی RSS نوشته شود به حساب می آید
کارآمد در مدیریت صفحات “کثیف” (حافظه آزاد شده اما به سیستم عامل بازگردانده نمی شود)
ردپای حافظه اولیه بالا
می تواند برای عملکرد یا کارایی حافظه برای بارهای کاری سنگین تنظیم شود

برای استفاده از jemalloc در پروژه Rust:

آن را به خود اضافه کنید Cargo.toml:

[dependencies] jemallocator = “0.3.2”

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آن را به عنوان تخصیص دهنده جهانی در فایل Rust اصلی خود تنظیم کنید:

use jemallocator::Jemalloc;

#[global_allocator] static GLOBAL: Jemalloc = Jemalloc;

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

mimalloc مایکروسافت

mimalloc یکی دیگر از تخصیص‌دهنده‌های با کارایی بالا است که به دلیل سرعت و حافظه اولیه کم شناخته شده است.

مشخصات:

خیلی سریع
ردپای حافظه اولیه کم
انتخاب خوبی برای برنامه هایی که نیاز به زمان راه اندازی سریع دارند

تکنیک های پیشرفته مدیریت حافظه

استفاده كردن Box برای تخصیص هیپ

هنگامی که شما نیاز به تخصیص حافظه بر روی پشته دارید، Rust آن را فراهم می کند Box نوع این برای ساختارهای داده بازگشتی یا زمانی که باید اطمینان حاصل کنید که یک مقدار دارای آدرس حافظه پایدار است مفید است.

fn main() {
let b = Box::new(5);
println!(“b = {}”, b);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

چه زمانی b از محدوده خارج می شود، حافظه پشته به طور خودکار توزیع می شود.

شمارش مرجع با Rc

برای سناریوهایی که به مالکیت مشترک داده ها نیاز دارید (مثلاً در ساختارهای گراف مانند)، Rust ارائه می دهد. Rc (مرجع شمارش شده است).

use std::rc::Rc;

fn main() {
let a = Rc::new(String::from(“Hello”));
let b = a.clone(); // Increases the reference count

println!(“a: {}, b: {}”, a, b);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Rc تعداد ارجاعات به یک مقدار را ردیابی می کند و تنها زمانی که تعداد ارجاعات به صفر برسد، مقدار را به آن اختصاص می دهد.

شمارش مرجع اتمی با Arc

برای شمارش ارجاع ایمن نخ، Rust فراهم می کند Arc (شماره مرجع اتمی). شبیه به Rc اما برای استفاده در چندین رشته ایمن است.

use std::sync::Arc;
use std::thread;

fn main() {
let s = Arc::new(String::from(“shared data”));

for _ in 0..10 {
let s = Arc::clone(&s);
thread::spawn(move || {
println!(“{}”, s);
});
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بهینه سازی استفاده از حافظه

طرح بندی ساختار

Rust به شما امکان می دهد تا با در نظر گرفتن طرح ساختار، استفاده از حافظه را بهینه کنید. بیایید به یک مثال نگاه کنیم و بالشتک ها را توضیح دهیم:

struct Efficient {
a: i32,
b: i32,
c: i16,
}

struct Inefficient {
a: i32,
c: i16,
b: i32,
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در Efficient ساختار:

a 4 بایت را اشغال می کند

b 4 بایت بعدی را اشغال می کند

c 2 بایت بعدی را اشغال می کند
مجموع: 10 بایت

در Inefficient ساختار:

a 4 بایت را اشغال می کند

c 2 بایت بعدی را اشغال می کند
2 بایت بالشتک برای تراز اضافه می شود b

b 4 بایت بعدی را اشغال می کند
مجموع: 12 بایت

این Efficient struct به دلیل تراز بهتر و padding کمتر از حافظه کمتری استفاده می کند. کامپایلر برای اطمینان از اینکه هر فیلد با تراز طبیعی خود (معمولاً اندازه آن) تراز است، padding اضافه می کند. با ترتیب دادن فیلدها از بزرگترین به کوچک‌ترین، اغلب می‌توانیم مقدار padding مورد نیاز را کاهش دهیم.

کپی در مقابل کلون

درک تفاوت بین Copy و Clone ویژگی ها می توانند به شما در بهینه سازی استفاده از حافظه کمک کنند:

Copy: امکان کپی بیتی مقادیر را می دهد. برای انواع کوچک و دارای پشته استفاده کنید.

Clone: به منطق کپی پیچیده تر اجازه می دهد. برای انواع تخصیص داده شده یا بزرگتر استفاده کنید.

#[derive(Copy, Clone)] struct Point {
x: i32,
y: i32,
}

#[derive(Clone)] struct ComplexData {
data: Vec<i32>,
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بهینه سازی نوع گزینه

زنگ Option نوع بهینه شده است تا از نشانگرهای پوچ جلوگیری شود. برای انواعی که نمی توانند null باشند (مانند BoxRust از یک بهینه سازی هوشمندانه استفاده می کند که در آن None نسخه هیچ فضای اضافی را اشغال نمی کند.

enum Option<T> {
Some(T),
None,
}

let x: Option<Box<i32>> = None;

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این مورد، x هیچ حافظه پشته ای را اختصاص نمی دهد.

مفاهیم پیشرفته

صفحات حافظه و حافظه مجازی

درک نحوه مدیریت حافظه توسط سیستم عامل می تواند به شما در نوشتن کد Rust کارآمدتر کمک کند. سیستم عامل حافظه را در صفحات (معمولاً 4096 بایت) اختصاص می دهد. هنگامی که برنامه شما درخواست حافظه می کند، در چند برابر از این صفحات داده می شود.

حافظه مجازی به برنامه شما اجازه می دهد تا از حافظه ای که به صورت فیزیکی در دسترس است استفاده کند. سیستم عامل آدرس های حافظه مجازی را به حافظه فیزیکی یا ذخیره سازی دیسک نگاشت می کند.

اندازه مجموعه مقیم (RSS) در مقابل حافظه مجازی

حافظه مجازی: مقدار حافظه ای که برنامه شما می تواند استفاده کند.

RSS (اندازه مجموعه مقیم): حافظه واقعی استفاده شده توسط برنامه شما.

تخصیص‌دهنده‌های مختلف این موارد را متفاوت مدیریت می‌کنند. به عنوان مثال، jemalloc استفاده می کند mmap برای تخصیص حافظه، که تنها زمانی که روی RSS نوشته می شود به حساب می آید.

تیونینگ jemalloc

jemalloc گزینه های مختلف تنظیم را ارائه می دهد:

عرصه های متعدد برای محدود کردن پراکندگی
موضوعات پاکسازی پس زمینه
گزینه های پروفایل برای نظارت بر مصرف حافظه

اینها را می توان از طریق متغیرهای محیطی یا در زمان اجرا پیکربندی کرد.

بهترین روش ها برای مدیریت حافظه در Rust

در صورت امکان از تخصیص پشته استفاده کنید: تخصیص پشته سریعتر است و نیازی به توزیع صریح ندارد.
از سیستم مالکیت Rust استفاده کنید: اجازه دهید قوانین مالکیت و قرض گیری Rust تا حد امکان حافظه را برای شما مدیریت کند.
از ساختارهای داده مناسب استفاده کنید: ساختارهای داده ای را انتخاب کنید که با الگوهای دسترسی و نیازهای حافظه شما مطابقت داشته باشد.
تخصیص دهنده های سفارشی را برای موارد استفاده خاص در نظر بگیرید: اگر برنامه شما نیازمند حافظه منحصر به فرد است، یک اختصاص دهنده سفارشی را در نظر بگیرید.
پروفایل اپلیکیشن خود: از ابزارهایی مانند valgrind یا پروفایلرهای مخصوص Rust برای شناسایی تنگناهای حافظه.
از بهینه سازی زودرس خودداری کنید: ابتدا روی نوشتن کد Rust واضح و اصطلاحی تمرکز کنید. فقط در صورت لزوم و پس از نمایه سازی بهینه سازی کنید.
استفاده کنید Box برای اشیاء بزرگ یا ساختارهای داده بازگشتی: این داده ها را به پشته منتقل می کند، که می تواند برای اجسام بزرگ کارآمدتر باشد.
حواستان به عمرها باشد: سیستم مادام العمر Rust را درک کرده و از آن استفاده کنید تا اطمینان حاصل کنید که مراجع معتبر هستند.
استفاده کنید Rc و Arc عاقلانه: این انواع برای مالکیت مشترک مفید هستند اما با هزینه عملکرد همراه هستند.
استفاده از تخصیص دهنده های عرصه را برای اشیاء با عمر کوتاه در نظر بگیرید: این می تواند به طور قابل توجهی سربار تخصیص را در برخی سناریوها کاهش دهد.

نتیجه

مدیریت حافظه در Rust یک ویژگی قدرتمند است که آن را از بسیاری از زبان های برنامه نویسی دیگر متمایز می کند. با درک و استفاده از مدل مالکیت Rust، قوانین استقراض و استراتژی های تخصیص، می توانید کد کارآمد، ایمن و کارآمد بنویسید.

به یاد داشته باشید که تسلط بر مدیریت حافظه در Rust یک سفر است. مفاهیمی که در اینجا پوشش داده‌ایم، پایه محکمی را فراهم می‌کنند، اما همیشه چیزهای بیشتری برای یادگیری وجود دارد. از غواصی عمیق‌تر در مستندات Rust تردید نکنید، استراتژی‌های تخصیص مختلف را آزمایش کنید و با جامعه Rust تعامل کنید تا درک خود را بیشتر کنید.

با ادامه کار با Rust، در مدیریت کارآمد حافظه مهارت بیشتری خواهید داشت. این منجر به برنامه های قوی و با کارایی بالا می شود که عاری از بسیاری از اشکالات رایج مرتبط با حافظه هستند.

به تمرین ادامه دهید، به یادگیری ادامه دهید و چالش ها را در آغوش بگیرید – آنها فرصت هایی برای رشد هستند.

منابع

معرفی

به این آموزش عمیق در مورد تخصیص حافظه در Rust خوش آمدید. به عنوان یک توسعه دهنده، درک اینکه Rust چگونه حافظه را مدیریت می کند برای نوشتن برنامه های کارآمد و ایمن بسیار مهم است. این راهنما نتیجه تجزیه و تحلیل چندین منبع خبره است تا یک نمای کلی جامع از مدیریت حافظه نه تنها در Rust، بلکه به طور کلی در زبان های برنامه نویسی به شما ارائه دهد.

توجه به این نکته مهم است که برخی از مفاهیمی که در اینجا مورد بحث قرار می‌گیرند می‌توانند بسیار پیچیده باشند و برای درک کامل ممکن است نیاز به تحقیقات بیشتری از جانب شما داشته باشند. اگر موضوعات خاصی را چالش برانگیز می دانید، ناامید نشوید – مدیریت حافظه موضوعی عمیق است و حتی توسعه دهندگان با تجربه به طور مداوم جنبه های جدیدی از آن را یاد می گیرند.

ما با مفاهیم اولیه ای که برای بسیاری از زبان های برنامه نویسی اعمال می شود شروع می کنیم و سپس بر روی پیاده سازی های خاص Rust تمرکز می کنیم. در پایان این آموزش، شما یک پایه محکم در استراتژی های تخصیص حافظه Rust و نحوه اجرای موثر آنها در پروژه های خود خواهید داشت.

مبانی چیدمان حافظه

قبل از اینکه به مفاهیم خاص Rust بپردازیم، درک طرح بندی حافظه اصلی یک برنامه ضروری است.

ساختار باینری اجرایی

هنگامی که یک برنامه Rust را کامپایل می کنید، نتیجه یک باینری قابل اجرا است. در سیستم های لینوکس، این معمولا در قالب ELF 64 است. هسته سیستم عامل طیف پیوسته ای از آدرس های حافظه مجازی را که به آدرس های حافظه فیزیکی نگاشت شده اند برای برنامه شما فراهم می کند تا از آنها استفاده کند.

بخش ها در اجرایی

یک باینری اجرایی به چند بخش تقسیم می شود:

  1. بخش متن:

    • حاوی دستورالعمل های اجرایی
    • فقط خواندنی
    • بر اساس معماری CPU متفاوت است
  2. بخش داده:

    • حاوی متغیرهای استاتیک اولیه (هم جهانی و هم محلی)
  3. بخش BSS:

    • شامل متغیرهای جهانی بدون مقدار اولیه است
  4. بخش پشته:

    • در انتهای بالای حافظه اختصاص داده شده است
    • به سمت پایین رشد می کند
  5. بخش هیپ:

    • بین همه موضوعات به اشتراک گذاشته شده است
    • به سمت بالا رشد می کند

درک این بخش‌ها بسیار مهم است زیرا نقش‌های متفاوتی در تخصیص و مدیریت حافظه دارند.

پشته در مقابل هیپ

حال، بیایید روی دو نوع اصلی تخصیص حافظه در Rust تمرکز کنیم: پشته و هیپ.

پشته

پشته ناحیه ای از حافظه است که از دستور Last-In-First-Out (LIFO) پیروی می کند. استفاده می شود برای:

  • متغیرهای محلی
  • پارامترهای تابع
  • آدرس ها را برگردانید

ویژگی های کلیدی تخصیص پشته:

  • تخصیص و انتقال سریع
  • اندازه محدود (معمولاً 8 مگابایت در سیستم های لینوکس 64 بیتی برای رشته اصلی، 2 مگابایت برای موضوعات دیگر)
  • غیر تکه تکه شده

پشته

Heap ناحیه ای از حافظه است که برای تخصیص پویا استفاده می شود. توسط ویژگی اختصاص دهنده جهانی Rust مدیریت می شود که اغلب از malloc کتابخانه C در زیر کاپوت استفاده می کند. ویژگی های کلیدی عبارتند از:

  • اندازه انعطاف پذیر
  • تخصیص و توزیع کندتر در مقایسه با پشته
  • می تواند منجر به پراکندگی شود
  • بین همه موضوعات به اشتراک گذاشته شده است

تابع Stack Frames

هنگامی که یک تابع فراخوانی می شود، یک قاب پشته جدید ایجاد می شود. این قاب ذخیره می کند:

  • پارامترهای تابع
  • متغیرهای محلی
  • آدرس بازگشت

نشانگر پشته بالای پشته را ردیابی می کند و با فراخوانی و بازگشت توابع تغییر می کند.

رویکرد Rust به مدیریت حافظه

مدیریت حافظه Rust بر اساس دو مفهوم کلیدی ساخته شده است: مالکیت و قرض گرفتن. این قوانین به Rust اجازه می دهد تا حافظه را بدون جمع کننده زباله مدیریت کند، از ایمنی حافظه اطمینان حاصل می کند و از مشکلات رایج مانند نشانگرهای پوچ یا آویزان جلوگیری می کند.

قوانین مالکیت

  1. هر مقدار در Rust دارای یک متغیر است که “مالک” آن است.
  2. در هر زمان فقط یک مالک می تواند وجود داشته باشد.
  3. وقتی مالک از محدوده خارج شود، مقدار حذف می شود.

بیایید به یک مثال نگاه کنیم:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // ownership of the string moves to s2

    // println!("{}", s1);  // This would cause a compile-time error
    println!("{}", s2);  // This is fine
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این مثال، s1 در ابتدا صاحب رشته است. وقتی تعیین می کنیم s1 به s2، مالکیت منتقل می شود و s1 دیگر معتبر نیست

قرض گرفتن

وام گرفتن به شما این امکان را می دهد که بدون در نظر گرفتن مالکیت به یک ارزش اشاره کنید. دو نوع قرض وجود دارد:

  1. قرض های فقط خواندنی: چندین قرض فقط خواندنی به طور همزمان مجاز است.
  2. وام های قابل تغییر: فقط یک وام قابل تغییر در هر زمان مجاز است.

در اینجا یک مثال است:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;  // read-only borrow
    let r2 = &s;  // another read-only borrow
    println!("{} and {}", r1, r2);

    let r3 = &mut s;  // mutable borrow
    r3.push_str(", world");
    println!("{}", r3);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این سیستم قرض‌گیری به Rust اجازه می‌دهد تا از مسابقه داده‌ها در زمان کامپایل جلوگیری کند، که یک مزیت قابل توجه نسبت به بسیاری از زبان‌های برنامه‌نویسی دیگر است.

انواع داده ها و تخصیص حافظه

درک نحوه تخصیص انواع داده های مختلف در حافظه برای نوشتن کد Rust کارآمد بسیار مهم است.

انواع داده های اولیه

انواع داده های اولیه در Rust اندازه ثابتی دارند و در پشته ذخیره می شوند. در اینجا لیست گسترده ای از انواع اولیه آمده است:

  • اعداد صحیح: i8، i16، i32، i64، i128، isize، u8، u16، u32، u64، u128، usize
  • اعداد اعشاری: f32، f64
  • بولی: bool
  • شخصیت ها: char (مقادیر اسکالر یونیکد)
  • نوع واحد: () (یک تاپل خالی)

این انواع پیاده سازی می کنند Copy صفت، به این معنی که وقتی به توابع تخصیص داده می شوند یا به آنها منتقل می شوند، با مقدار کپی می شوند.

انواع داده های مرکب

تاپل ها

Tuples مقادیر مختلف را ذخیره می کند و در پشته تخصیص می یابد. آنها به طور پیوسته در حافظه قرار می گیرند، با بالشتک بالقوه برای تراز کردن. مثلا:

let tup: (i32, f64, u8) = (500, 6.4, 1);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در حافظه، این تاپل ممکن است به شکل زیر باشد:

[4 bytes for i32][4 bytes padding][8 bytes for f64][1 byte for u8][7 bytes padding]
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بالشتک تضمین می کند که هر عنصر به درستی در حافظه تراز شده است.

سازه ها

سازه ها را می توان نامگذاری کرد یا تاپل مانند. آنها معمولاً در پشته تخصیص داده می شوند، اما اگر حاوی انواعی مانند باشد، محتوای آنها می تواند در پشته باشد String یا Vec. چیدمان حافظه آنها شبیه به تاپل ها، از جمله padding بالقوه است. مثلا:

struct Point {
    x: i32,
    y: i32,
}

let p = Point { x: 0, y: 0 };
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Enums

Enum ها به عنوان یک متمایز (معمولا یک عدد صحیح) ذخیره می شوند تا نشان دهند کدام نوع است، به علاوه فضای کافی برای ذخیره بزرگترین نوع. این اجازه می دهد تا Rust استفاده از حافظه را بهینه کند و در عین حال ایمنی نوع را فراهم کند. تخصیص حافظه می تواند پیچیده تر از آن چیزی باشد که در ابتدا به نظر می رسد:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این فهرست:

  • Quit به هیچ فضای اضافی فراتر از متمایز کننده نیاز ندارد.
  • Move به فضای دو نفره نیاز دارد i32 ارزش های.
  • Write نیاز به فضا برای a String، که یک اشاره گر به حافظه پشته است.
  • ChangeColor به فضای سه نفره نیاز دارد i32 ارزش های.

enum فضای کافی را برای بزرگترین نوع (احتمالا ChangeColor در این مورد)، به علاوه ممیز. این یعنی حتی Quit نوع از همان مقدار حافظه استفاده می کند ChangeColor، اما این رویکرد امکان تطبیق بسیار سریع را فراهم می کند و از نیاز به تخصیص پشته برای خود enum جلوگیری می کند.

انواع داده های پویا

آرایه

آرایه‌ها در Rust اندازه ثابتی دارند که در زمان کامپایل شناخته می‌شوند و در پشته ذخیره می‌شوند. این با برخی از زبان‌های محبوب مانند پایتون یا جاوا اسکریپت متفاوت است، جایی که آرایه‌ها (یا لیست‌ها) به صورت پویا و به صورت heap تخصیص داده می‌شوند. در رست:

let arr: [i32; 5] = [1, 2, 3, 4, 5];
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این آرایه کاملاً به‌صورت پشته‌ای تخصیص داده شده است، که می‌تواند منجر به استفاده بسیار کارآمد از حافظه و الگوهای دسترسی برای مجموعه‌های با اندازه ثابت شود.

بردار

بردارها قابل تغییر اندازه هستند و داده های خود را روی پشته ذخیره می کنند. آنها ظرفیت و طول را پیگیری می کنند:

let mut vec: Vec<i32> = Vec::new();
vec.push(1);
vec.push(2);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تکه

برش ها نماهایی از عناصر یک آرایه یا بردار هستند. آنها از یک اشاره گر چربی برای مرجع استفاده می کنند که هم نشانگر داده و هم طول دارد. نشانگر چربی نشانگری است که اطلاعات اضافی را فراتر از آدرس حافظه حمل می کند. در مورد یک برش، نشانگر چربی حاوی:

  1. اشاره گر به اولین عنصر برش در حافظه
  2. طول برش

این اطلاعات اضافی به Rust اجازه می دهد تا بدون نیاز به ذخیره این اطلاعات به طور جداگانه یا پرس و جو در زمان اجرا، کرانه ها را بررسی و بر روی برش به طور موثر تکرار کند.

let slice: &[i32] = &arr[1..3];
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

رشته

رشته ها در Rust شبیه بردارها هستند اما تضمین شده است که UTF-8 رمزگذاری شده اند. این تضمین یعنی:

  1. هر کاراکتر در رشته با یک دنباله بایت معتبر UTF-8 نشان داده می شود.
  2. رشته می تواند حاوی هر کاراکتر یونیکد باشد، اما آنها به طور موثر ذخیره می شوند.
  3. عملیات رشته ای (مانند نمایه سازی) بر روی مرزهای UTF-8 کار می کند، نه بایت های خام.

این ضمانت UTF-8 به Rust اجازه می دهد تا مدیریت رشته ای ایمن و کارآمد را ارائه دهد، و از مسائلی مانند توالی بایت های نامعتبر یا مرزهای کاراکترهای نادرست که می تواند در زبان هایی با رمزگذاری رشته های کمتر سختگیرانه رخ دهد، جلوگیری کند.

let s = String::from("hello");
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تخصیص دهنده های حافظه در Rust

Rust انعطاف پذیری را در انتخاب تخصیص دهنده های حافظه فراهم می کند. بیایید برخی از گزینه های رایج را بررسی کنیم:

تخصیص دهنده استاندارد

تخصیص دهنده استاندارد در Rust از تخصیص دهنده پیش فرض سیستم (اغلب کتابخانه C) استفاده می کند malloc). این یک تخصیص دهنده همه منظوره خوب است اما ممکن است برای همه سناریوها کارآمدترین نباشد.

مشخصات:

  • استفاده می کند sbrk برای رشد پشته
  • حافظه به عنوان Resident Set Size (RSS) محاسبه می شود
  • سریعترین یا کارآمدترین حافظه نیست
  • ردپای حافظه کم هنگام شروع اولیه

جمالوک

jemalloc یک تخصیص دهنده جایگزین محبوب است که به دلیل کارایی آن در محیط های چند رشته ای شناخته شده است.

مشخصات:

  • استفاده می کند mmap برای تخصیص حافظه
  • حافظه فقط زمانی که روی RSS نوشته شود به حساب می آید
  • کارآمد در مدیریت صفحات “کثیف” (حافظه آزاد شده اما به سیستم عامل بازگردانده نمی شود)
  • ردپای حافظه اولیه بالا
  • می تواند برای عملکرد یا کارایی حافظه برای بارهای کاری سنگین تنظیم شود

برای استفاده از jemalloc در پروژه Rust:

  1. آن را به خود اضافه کنید Cargo.toml:
   [dependencies]
   jemallocator = "0.3.2"
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  1. آن را به عنوان تخصیص دهنده جهانی در فایل Rust اصلی خود تنظیم کنید:
   use jemallocator::Jemalloc;

   #[global_allocator]
   static GLOBAL: Jemalloc = Jemalloc;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

mimalloc مایکروسافت

mimalloc یکی دیگر از تخصیص‌دهنده‌های با کارایی بالا است که به دلیل سرعت و حافظه اولیه کم شناخته شده است.

مشخصات:

  • خیلی سریع
  • ردپای حافظه اولیه کم
  • انتخاب خوبی برای برنامه هایی که نیاز به زمان راه اندازی سریع دارند

تکنیک های پیشرفته مدیریت حافظه

استفاده كردن Box برای تخصیص هیپ

هنگامی که شما نیاز به تخصیص حافظه بر روی پشته دارید، Rust آن را فراهم می کند Box نوع این برای ساختارهای داده بازگشتی یا زمانی که باید اطمینان حاصل کنید که یک مقدار دارای آدرس حافظه پایدار است مفید است.

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

چه زمانی b از محدوده خارج می شود، حافظه پشته به طور خودکار توزیع می شود.

شمارش مرجع با Rc

برای سناریوهایی که به مالکیت مشترک داده ها نیاز دارید (مثلاً در ساختارهای گراف مانند)، Rust ارائه می دهد. Rc (مرجع شمارش شده است).

use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("Hello"));
    let b = a.clone();  // Increases the reference count

    println!("a: {}, b: {}", a, b);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Rc تعداد ارجاعات به یک مقدار را ردیابی می کند و تنها زمانی که تعداد ارجاعات به صفر برسد، مقدار را به آن اختصاص می دهد.

شمارش مرجع اتمی با Arc

برای شمارش ارجاع ایمن نخ، Rust فراهم می کند Arc (شماره مرجع اتمی). شبیه به Rc اما برای استفاده در چندین رشته ایمن است.

use std::sync::Arc;
use std::thread;

fn main() {
    let s = Arc::new(String::from("shared data"));

    for _ in 0..10 {
        let s = Arc::clone(&s);
        thread::spawn(move || {
            println!("{}", s);
        });
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بهینه سازی استفاده از حافظه

طرح بندی ساختار

Rust به شما امکان می دهد تا با در نظر گرفتن طرح ساختار، استفاده از حافظه را بهینه کنید. بیایید به یک مثال نگاه کنیم و بالشتک ها را توضیح دهیم:

struct Efficient {
    a: i32,
    b: i32,
    c: i16,
}

struct Inefficient {
    a: i32,
    c: i16,
    b: i32,
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در Efficient ساختار:

  • a 4 بایت را اشغال می کند
  • b 4 بایت بعدی را اشغال می کند
  • c 2 بایت بعدی را اشغال می کند
  • مجموع: 10 بایت

در Inefficient ساختار:

  • a 4 بایت را اشغال می کند
  • c 2 بایت بعدی را اشغال می کند
  • 2 بایت بالشتک برای تراز اضافه می شود b
  • b 4 بایت بعدی را اشغال می کند
  • مجموع: 12 بایت

این Efficient struct به دلیل تراز بهتر و padding کمتر از حافظه کمتری استفاده می کند. کامپایلر برای اطمینان از اینکه هر فیلد با تراز طبیعی خود (معمولاً اندازه آن) تراز است، padding اضافه می کند. با ترتیب دادن فیلدها از بزرگترین به کوچک‌ترین، اغلب می‌توانیم مقدار padding مورد نیاز را کاهش دهیم.

کپی در مقابل کلون

درک تفاوت بین Copy و Clone ویژگی ها می توانند به شما در بهینه سازی استفاده از حافظه کمک کنند:

  • Copy: امکان کپی بیتی مقادیر را می دهد. برای انواع کوچک و دارای پشته استفاده کنید.
  • Clone: به منطق کپی پیچیده تر اجازه می دهد. برای انواع تخصیص داده شده یا بزرگتر استفاده کنید.
#[derive(Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

#[derive(Clone)]
struct ComplexData {
    data: Vec<i32>,
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بهینه سازی نوع گزینه

زنگ Option نوع بهینه شده است تا از نشانگرهای پوچ جلوگیری شود. برای انواعی که نمی توانند null باشند (مانند BoxRust از یک بهینه سازی هوشمندانه استفاده می کند که در آن None نسخه هیچ فضای اضافی را اشغال نمی کند.

enum Option<T> {
    Some(T),
    None,
}

let x: Option<Box<i32>> = None;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این مورد، x هیچ حافظه پشته ای را اختصاص نمی دهد.

مفاهیم پیشرفته

صفحات حافظه و حافظه مجازی

درک نحوه مدیریت حافظه توسط سیستم عامل می تواند به شما در نوشتن کد Rust کارآمدتر کمک کند. سیستم عامل حافظه را در صفحات (معمولاً 4096 بایت) اختصاص می دهد. هنگامی که برنامه شما درخواست حافظه می کند، در چند برابر از این صفحات داده می شود.

حافظه مجازی به برنامه شما اجازه می دهد تا از حافظه ای که به صورت فیزیکی در دسترس است استفاده کند. سیستم عامل آدرس های حافظه مجازی را به حافظه فیزیکی یا ذخیره سازی دیسک نگاشت می کند.

اندازه مجموعه مقیم (RSS) در مقابل حافظه مجازی

  • حافظه مجازی: مقدار حافظه ای که برنامه شما می تواند استفاده کند.
  • RSS (اندازه مجموعه مقیم): حافظه واقعی استفاده شده توسط برنامه شما.

تخصیص‌دهنده‌های مختلف این موارد را متفاوت مدیریت می‌کنند. به عنوان مثال، jemalloc استفاده می کند mmap برای تخصیص حافظه، که تنها زمانی که روی RSS نوشته می شود به حساب می آید.

تیونینگ jemalloc

jemalloc گزینه های مختلف تنظیم را ارائه می دهد:

  • عرصه های متعدد برای محدود کردن پراکندگی
  • موضوعات پاکسازی پس زمینه
  • گزینه های پروفایل برای نظارت بر مصرف حافظه

اینها را می توان از طریق متغیرهای محیطی یا در زمان اجرا پیکربندی کرد.

بهترین روش ها برای مدیریت حافظه در Rust

  1. در صورت امکان از تخصیص پشته استفاده کنید: تخصیص پشته سریعتر است و نیازی به توزیع صریح ندارد.

  2. از سیستم مالکیت Rust استفاده کنید: اجازه دهید قوانین مالکیت و قرض گیری Rust تا حد امکان حافظه را برای شما مدیریت کند.

  3. از ساختارهای داده مناسب استفاده کنید: ساختارهای داده ای را انتخاب کنید که با الگوهای دسترسی و نیازهای حافظه شما مطابقت داشته باشد.

  4. تخصیص دهنده های سفارشی را برای موارد استفاده خاص در نظر بگیرید: اگر برنامه شما نیازمند حافظه منحصر به فرد است، یک اختصاص دهنده سفارشی را در نظر بگیرید.

  5. پروفایل اپلیکیشن خود: از ابزارهایی مانند valgrind یا پروفایلرهای مخصوص Rust برای شناسایی تنگناهای حافظه.

  6. از بهینه سازی زودرس خودداری کنید: ابتدا روی نوشتن کد Rust واضح و اصطلاحی تمرکز کنید. فقط در صورت لزوم و پس از نمایه سازی بهینه سازی کنید.

  7. استفاده کنید Box برای اشیاء بزرگ یا ساختارهای داده بازگشتی: این داده ها را به پشته منتقل می کند، که می تواند برای اجسام بزرگ کارآمدتر باشد.

  8. حواستان به عمرها باشد: سیستم مادام العمر Rust را درک کرده و از آن استفاده کنید تا اطمینان حاصل کنید که مراجع معتبر هستند.

  9. استفاده کنید Rc و Arc عاقلانه: این انواع برای مالکیت مشترک مفید هستند اما با هزینه عملکرد همراه هستند.

  10. استفاده از تخصیص دهنده های عرصه را برای اشیاء با عمر کوتاه در نظر بگیرید: این می تواند به طور قابل توجهی سربار تخصیص را در برخی سناریوها کاهش دهد.

نتیجه

مدیریت حافظه در Rust یک ویژگی قدرتمند است که آن را از بسیاری از زبان های برنامه نویسی دیگر متمایز می کند. با درک و استفاده از مدل مالکیت Rust، قوانین استقراض و استراتژی های تخصیص، می توانید کد کارآمد، ایمن و کارآمد بنویسید.

به یاد داشته باشید که تسلط بر مدیریت حافظه در Rust یک سفر است. مفاهیمی که در اینجا پوشش داده‌ایم، پایه محکمی را فراهم می‌کنند، اما همیشه چیزهای بیشتری برای یادگیری وجود دارد. از غواصی عمیق‌تر در مستندات Rust تردید نکنید، استراتژی‌های تخصیص مختلف را آزمایش کنید و با جامعه Rust تعامل کنید تا درک خود را بیشتر کنید.

با ادامه کار با Rust، در مدیریت کارآمد حافظه مهارت بیشتری خواهید داشت. این منجر به برنامه های قوی و با کارایی بالا می شود که عاری از بسیاری از اشکالات رایج مرتبط با حافظه هستند.

به تمرین ادامه دهید، به یادگیری ادامه دهید و چالش ها را در آغوش بگیرید – آنها فرصت هایی برای رشد هستند.

منابع

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا