در مصرف حافظه ولخرجی کنیم یا ریسک ایجاد باگ را بپذیریم؟

https://medium.com/@gough.cory/performance-of-numpy-array-vs-python-list-194c8e283b65
https://medium.com/@gough.cory/performance-of-numpy-array-vs-python-list-194c8e283b65


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

هر دو کتابخانه توابعی برای تغییر داده‌ها بصورت درجا دارند اما استفاده از این دست توابع شاید منتهی به ایجاد باگ‌های اعصاب خردکنی بشود.

پس باید چیکار کرد؟

در این نوشته الگوی hidden mutability و نحوه استفاده از آن را یاد می‌گیریم که یک مصالحه بین دو مورد مطرح شده است: پیش نیامدن باگ‌های اعصاب خورد کن و کاهش مصرف حافظه تا حدودی.

یک مثال: مصرف بیش از حد حافظه

تابع زیر را در نظر بگیرید (تابع یک آرایه float را می‌گیرد و نسخه نرمال شده آن که دارای مقادیر بین صفر و یک است را برمی‌گرداند):

https://gist.github.com/salehmontazeran/4115d7fcbc8b8192f9b0439bb948c4b3

این تابع چقدر حافظه مصرف میکند؟ اگر آرایه ورودی N بایت باشد٬ تابع از N*3 بایت حافظه استفاده می‌کند.

  • آرایه ورودی که بدون تغییر می‌ماند (N بایت)
  • آرایه موقتی که نتیجه array - low در آن نگه داری می‌شود (N بایت)
  • نتیجه‌ نهایی که تابع باز می‌گرداند ‌(N بایت)

خب چطوری میشه این مصرف حافظه رو کمتر کرد؟

تغییر درجا

برای کاهش مصرف حافظه می‌توانیم از عملگرهای درجا مثل =+ که عملیات‌ها را روی آرایه اصلی انجام دهند استفاده کنیم:

https://gist.github.com/salehmontazeran/c6c55868a4b12b7d3cda9d8c1f398f58

همچنین:

  • تعدادی از توابع numpy در ورودی یک آرگومان out می‌گیرند که به شما اجازه می‌دهد نتیجه را در یک آرایه موجود٬ که معمولا شامل خود آرایه اصلی هم می‌شود٬ بریزید.
  • توابع pandas هم معمولا یک آرگومان inplace دارند که خود شی را بجای بازگرداند یکی شی جدید دستخوش تغییر می‌کنند.

در تمام موارد بالا شما در حال تغییر خود شی یا داده اصلی هستید و همین کار باعث صرفه‌جویی در مصرف حافظه می‌شود. در مثال بالا تقریبا تابع فقط با N بایت داده سر و کار دارد در مقابل حالت قبل که سه برابر این مورد با حافظه سر و کار داشت.

مشکلات ناشی از تغییر درجا

تغییر درجا (mutating) می‌تواند باعث ایجاد نتایج عجیب و باگ شود. برای مثال اگر هدف شما برای استفاده از تابع normalization جهت نمایش داده‌ها باشد:

https://gist.github.com/salehmontazeran/f21c582458d770fa58e3d99302176d87#file-minimizing-copying_3-py

این کد دارای باگ است، چرا؟ تابع do_something انتظار دارد که داده اصلی به آن پاس داده شود و نه نسخه normalize شده آن. اگر در حال DEBUG کد باشید تابع do_something یک ورودی و اگر در حال دیباگ نباشید یک ورودی دیگر میگیرد.

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

خب بالاخره که چی؟ پس چی کنیم؟

تغییر درجا پنهان

معمولا توابع، ورودی خودشون رو بصورت درجا تغییر نمی‌دهند اما این به این معنا نیست که تابع ما درون خودش نمی‌تواند تغییر درجایی داشته باشد که پنهان از فراخوانی کننده بیرون تابع باشد.

اگر قرار باشد کد ما این الگو را رعایت کند باید بصورت زیر نوشته شود.

https://gist.github.com/salehmontazeran/40f8e4454ec6e66ce47fccb8b43994ee

از دیدگاه فراخوانی کننده، این تابع همانند تابع اصلی اولیه است: ورودی تابع هیچوقت دستخوش تغییر نمی‌شود اما ما با حذف آن قسمتی از کد که منتج به ایجاد یک آرایه موقتی می‌شد (که بلافاصله هم دور ریخته می‌شد)، مصرف حافظه را از N*3 به N*2 کاهش دادیم.


Reference: https://pythonspeed.com/articles/minimizing-copying/