۲۷ - ۴فرم‌های اپلیکتیو ثابت

وقتی صحبت از مصرفِ حافظه و اشتراک‌گذاری در هسکل میشه، حتماً باید از CAF‌ها هم صحبت کرد: فرم‌های اپلیکتیو ثابت. CAF‌ها بیانیه‌هایی بدون متغیرِ آزاد هستن که در حافظه برای اشتراک‌گذاری بین همه‌ی بیانیه‌های دیگه‌ی یه ماژول نگه داشته میشن. ممکنه مقادیر لیترال، یا تابع‌های نیمه اعمال‌شده‌ای باشن که هیچ آرگومانِ اسم‌داری ندارن.

الان یه CAF ِ خیلی بزرگ درست می‌کنیم. دقت کنین که روی یه لیستِ بینهایت نگاشت کردیم و می‌خوایم بدونیم این کار چقدر حافظه مصرف کرده. احتمالاً خیلی زیاد:

module Main where 

incdInts :: [Integer]
incdInts = map (+1) [1..] 

main :: IO ()
main = do
  print (incdInts !! 1000) 
  print (incdInts !! 9001) 
  print (incdInts !! 90010) 
  print (incdInts !! 9001000) 
  print (incdInts !! 9501000) 
  print (incdInts !! 9901000)

اگه پروفایل‌ِش کنیم:

Thu Jan 21 23:25 2016
Time and Allocation
  Profiling Report  (Final)

cafSaturation +RTS -p -RTS
total time  =        0.28 secs
  (283 ticks @ 1000 us, 1 processor)
total alloc = 1,440,216,712 bytes
  (excludes profiling overheads)

COST CENTRE MODULE  %time %alloc
incdInts    Main     90.1  100.0
main        Main      9.9    0.0

-- یه کم از شلوغی‌های بی‌ربط حذف شدن
COST CENTRE MODULE no. entries  %time %alloc
MAIN        MAIN   45  0        0.0    0.0
 CAF        Main   89  0        0.0    0.0
  incdInts  Main   91  1        90.1  100.0
  main      Main   90  1        9.9    0.0

ببینید چطور ‏‎incdInts‎‏ برای خودش، جدای از ‏‎main‎‏، یه فرمِ اپلیکتیو ثابت‌ (CAF) ِه. به سایزِ اون تخصیصِ حافظه هم دقت کنین. دلیل‌ش اینه که اون نگاشت روی لیست بینهایت، یه مقدارِ سطحِ بالا هست که می‌تونه در کلِ ماژول به اشتراک گذاشته بشه، پس باید برای اشتراک‌گذاری حساب و جواب‌ش توی حافظه نگه داشته بشه.

CAFها شاملِ این موارد میشن:

  • مقادیر؛

  • توابعِ نیمه اعمال‌شده بدونِ آرگومان‌های اسم‌دار؛

  • تابع‌های تماماً اعمال شده مثلِ ‏‎incdInts‎‏ در مثال بالا، البته چنین چیزی در کدِ واقعی خیلی کم پیش میاد.

    CAFها، چون جلوی محاسبه‌ی تکراریِ مقادیرِ به اشتراک گذاشته شده رو می‌گیرن، ممکنه کمک به تسریعِ بعضی برنامه‌ها کنن؛ اما خیلی سریع ممکنه حافظه‌پرکُن بشن. چیزِ مهمی که باید به خاطر بسپارین، اینه که اگه دیدین برنامه‌تون بیشتر از اونی که انتظار دارین حافظه مصرف می‌کنه، دنبالِ CAF ِ طلایی بگردین تا بکُشین‌ِش.

    خوشبختانه CAFها اکثراً در چنین کدهای بازیچه‌ای پیدا میشن. کدِ واقعی معمولاً داده رو از جای دیگه میاره و این مشکلِ نگهداریِ مقادیرِ زیادِ داده در حافظه رو حذف می‌کنه.

    ببینیم چطور با اضافه کردنِ یه آرگومان به ‏‎incdInts‎‏، میشه از ساخت CAF جلوگیری کرد:

    module Main where 
    
    -- نیست CAF
    incdInts :: [Integer] -> [Integer]
    incdInts x = map (+1) x
    
    main :: IO ()
    main = do
      print (incdInts !! 1000)

    اگه پروفایل‌ش رو ببینیم:

    CAF
      main
        incdInts

    تعاریفِ سطح بالا ِ بی‌نقطه، CAF میشن، اما نقطه‌دار‌ها نه. این رو در فصلِ قبل هم تا حدی توضیح دادیم. عموماً دلیلِ اهمیتِ این تفاوت، میزانِ حافظه‌ی تخصیص داده شده‌ای که پروفایل گزارش میده نیست (که معمولاً هم گمراه‌کننده‌ست). به این دلیل مهمه که ساخت یه لیست (که آناً دور انداخته میشه) تو GHC خیلی کم هزینه‌ست. این کار ممکنه مقدار حافظه‌ای که در کل اختصاص داده میشه رو بیشتر کنه، اما مثل یه CAF توی هیپ نمی‌مونه که همین ممکنه حداکثر حافظه ِ مصرفی، و زمانی که صرفِ زباله‌روبی میشه رو کاهش بده.

    خب الان که به خودیِ‌خودِش یه CAF نیست. اما اگه کاهش اِتا بدیم (یعنی آرگومان‌های اسم‌دارِش رو حذف کنیم) تا بی‌نقطه بشه چطور؟

    module Main where 
    
    -- ‎‏میشه.‏‎ CAF این بار
    incdInts :: [Integer]
    incdInts = map (+1)
    
    main :: IO ()
    main = do
      print (incdInts [1..] !! 1000)

    این بار که پروفایل‌ش رو نگاه کنین، CAF ِ خودش میشه:

    CAF
      incdInts
      main
        incdInts

    عجب!

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