در مصرف حافظه ولخرجی کنیم یا ریسک ایجاد باگ را بپذیریم؟
فرض کنید داده بزرگی چه در یک آرایه numpy یا در یک دیتافریم pandas دارید و قصد دارید سلسلهای از عملیاتها را روی آنها انجام دهید. بصورت پیش فرض هر دو کتابخانه برای انجام عملیاتها از دادهها کپی درست میکنند که به معنی مصرف بیشتر حافطه است.
هر دو کتابخانه توابعی برای تغییر دادهها بصورت درجا دارند اما استفاده از این دست توابع شاید منتهی به ایجاد باگهای اعصاب خردکنی بشود.
پس باید چیکار کرد؟
در این نوشته الگوی hidden mutability و نحوه استفاده از آن را یاد میگیریم که یک مصالحه بین دو مورد مطرح شده است: پیش نیامدن باگهای اعصاب خورد کن و کاهش مصرف حافظه تا حدودی.
یک مثال: مصرف بیش از حد حافظه
تابع زیر را در نظر بگیرید (تابع یک آرایه float را میگیرد و نسخه نرمال شده آن که دارای مقادیر بین صفر و یک است را برمیگرداند):
این تابع چقدر حافظه مصرف میکند؟ اگر آرایه ورودی N بایت باشد٬ تابع از N*3 بایت حافظه استفاده میکند.
- آرایه ورودی که بدون تغییر میماند (N بایت)
- آرایه موقتی که نتیجه array - low در آن نگه داری میشود (N بایت)
- نتیجه نهایی که تابع باز میگرداند (N بایت)
خب چطوری میشه این مصرف حافظه رو کمتر کرد؟
تغییر درجا
برای کاهش مصرف حافظه میتوانیم از عملگرهای درجا مثل =+ که عملیاتها را روی آرایه اصلی انجام دهند استفاده کنیم:
همچنین:
- تعدادی از توابع numpy در ورودی یک آرگومان out میگیرند که به شما اجازه میدهد نتیجه را در یک آرایه موجود٬ که معمولا شامل خود آرایه اصلی هم میشود٬ بریزید.
- توابع pandas هم معمولا یک آرگومان inplace دارند که خود شی را بجای بازگرداند یکی شی جدید دستخوش تغییر میکنند.
در تمام موارد بالا شما در حال تغییر خود شی یا داده اصلی هستید و همین کار باعث صرفهجویی در مصرف حافظه میشود. در مثال بالا تقریبا تابع فقط با N بایت داده سر و کار دارد در مقابل حالت قبل که سه برابر این مورد با حافظه سر و کار داشت.
مشکلات ناشی از تغییر درجا
تغییر درجا (mutating) میتواند باعث ایجاد نتایج عجیب و باگ شود. برای مثال اگر هدف شما برای استفاده از تابع normalization جهت نمایش دادهها باشد:
این کد دارای باگ است، چرا؟ تابع do_something انتظار دارد که داده اصلی به آن پاس داده شود و نه نسخه normalize شده آن. اگر در حال DEBUG کد باشید تابع do_something یک ورودی و اگر در حال دیباگ نباشید یک ورودی دیگر میگیرد.
معمولا افرادی که از کد شما استفاده میکنند انتظار ندارند توابع روی خود داده بصورت مستقیم تغییری ایجاد کند، حتی بعد از مدتی ممکنه خود شما هم این نکته را فراموش کنید.
خب بالاخره که چی؟ پس چی کنیم؟
تغییر درجا پنهان
معمولا توابع، ورودی خودشون رو بصورت درجا تغییر نمیدهند اما این به این معنا نیست که تابع ما درون خودش نمیتواند تغییر درجایی داشته باشد که پنهان از فراخوانی کننده بیرون تابع باشد.
اگر قرار باشد کد ما این الگو را رعایت کند باید بصورت زیر نوشته شود.
از دیدگاه فراخوانی کننده، این تابع همانند تابع اصلی اولیه است: ورودی تابع هیچوقت دستخوش تغییر نمیشود اما ما با حذف آن قسمتی از کد که منتج به ایجاد یک آرایه موقتی میشد (که بلافاصله هم دور ریخته میشد)، مصرف حافظه را از N*3 به N*2 کاهش دادیم.
Reference: https://pythonspeed.com/articles/minimizing-copying/
مطلبی دیگر از این انتشارات
پوچ و خالی!
مطلبی دیگر از این انتشارات
نظریه بازی چیست؟ - قسمت اول
مطلبی دیگر از این انتشارات
آشنایی با قیف سنجه ها (Metrics funnel)