تحسين المحلل اللغوي البسيط الذي يتم استدعاؤه عدة مرات
-
02-01-2020 - |
سؤال
لقد كتبت محللًا لملف مخصص باستخدام attoparsec
.أشار تقرير ملفات التعريف إلى أن حوالي 67% من تخصيص الذاكرة يتم في وظيفة مسماة tab
, ، والذي يستهلك أيضًا معظم الوقت.ال tab
الوظيفة بسيطة جدًا:
tab :: Parser Char
tab = char '\t'
تقرير الملف الشخصي بأكمله هو كما يلي:
ASnapshotParser +RTS -p -h -RTS
total time = 37.88 secs (37882 ticks @ 1000 us, 1 processor)
total alloc = 54,255,105,384 bytes (excludes profiling overheads)
COST CENTRE MODULE %time %alloc
tab Main 83.1 67.7
main Main 6.4 4.2
readTextDevice Data.Text.IO.Internal 5.5 24.0
snapshotParser Main 4.7 4.0
individual inherited
COST CENTRE MODULE no. entries %time %alloc %time %alloc
MAIN MAIN 75 0 0.0 0.0 100.0 100.0
CAF Main 149 0 0.0 0.0 100.0 100.0
tab Main 156 1 0.0 0.0 0.0 0.0
snapshotParser Main 153 1 0.0 0.0 0.0 0.0
main Main 150 1 6.4 4.2 100.0 100.0
doStuff Main 152 1000398 0.3 0.0 88.1 71.8
snapshotParser Main 154 0 4.7 4.0 87.7 71.7
tab Main 157 0 83.1 67.7 83.1 67.7
readTextDevice Data.Text.IO.Internal 151 40145 5.5 24.0 5.5 24.0
CAF Data.Text.Array 142 0 0.0 0.0 0.0 0.0
CAF Data.Text.Internal 140 0 0.0 0.0 0.0 0.0
CAF GHC.IO.Handle.FD 122 0 0.0 0.0 0.0 0.0
CAF GHC.Conc.Signal 103 0 0.0 0.0 0.0 0.0
CAF GHC.IO.Encoding 101 0 0.0 0.0 0.0 0.0
CAF GHC.IO.FD 100 0 0.0 0.0 0.0 0.0
CAF GHC.IO.Encoding.Iconv 89 0 0.0 0.0 0.0 0.0
main Main 155 0 0.0 0.0 0.0 0.0
كيف يمكنني تحسين هذا؟
الكود بأكمله للمحلل هنا. يبلغ حجم الملف الذي أقوم بتحليله حوالي 77 ميجابايت.
المحلول
tab
هو كبش فداء.إذا قمت بتحديد boo :: Parser (); boo = return ()
وأدخل أ boo
قبل كل ربط في snapshotParser
التعريف، فإن تخصيصات التكلفة ستصبح شيئًا مثل:
main Main 255 0 11.8 13.8 100.0 100.0
doStuff Main 258 2097153 1.1 0.5 86.2 86.2
snapshotParser Main 260 0 0.4 0.1 85.1 85.7
boo Main 262 0 71.0 73.2 84.8 85.5
tab Main 265 0 13.8 12.3 13.8 12.3
لذلك يبدو أن منشئ ملفات التعريف يقوم بإلقاء اللوم على تخصيصات نتائج التحليل، ويرجع ذلك على الأرجح إلى التضمين الشامل لـ attoparsec
الكود، كما اقترح John L في التعليقات.
أما بالنسبة لقضايا الأداء، فالنقطة الأساسية هي أنه أثناء قيامك بتحليل ملف نصي بحجم 77 ميجابايت لإنشاء قائمة تحتوي على مليون عنصر، فإنك تريد أن تكون معالجة الملف كسولة وليست صارمة.بمجرد حل ذلك، قم بفصل الإدخال/الإخراج والتحليل doStuff
ومن المفيد أيضًا إنشاء قائمة اللقطات بدون مُراكم.إليك نسخة معدلة من برنامجك مع أخذ ذلك في الاعتبار.
{-# LANGUAGE BangPatterns #-}
module Main where
import Data.Maybe
import Data.Attoparsec.Text.Lazy
import Control.Applicative
import qualified Data.Text.Lazy.IO as TL
import Data.Text (Text)
import qualified Data.Text.Lazy as TL
buildStuff :: TL.Text -> [Snapshot]
buildStuff text = case maybeResult (parse endOfInput text) of
Just _ -> []
Nothing -> case parse snapshotParser text of
Done !i !r -> r : buildStuff i
Fail _ _ _ -> []
main :: IO ()
main = do
text <- TL.readFile "./snap.dat"
let ss = buildStuff text
print $ listToMaybe ss
>> Just (fromIntegral (length $ show ss) / fromIntegral (length ss))
newtype VehicleId = VehicleId Int deriving Show
newtype Time = Time Int deriving Show
newtype LinkID = LinkID Int deriving Show
newtype NodeID = NodeID Int deriving Show
newtype LaneID = LaneID Int deriving Show
tab :: Parser Char
tab = char '\t'
-- UNPACK pragmas. GHC 7.8 unboxes small strict fields automatically;
-- however, it seems we still need the pragmas while profiling.
data Snapshot = Snapshot {
vehicle :: {-# UNPACK #-} !VehicleId,
time :: {-# UNPACK #-} !Time,
link :: {-# UNPACK #-} !LinkID,
node :: {-# UNPACK #-} !NodeID,
lane :: {-# UNPACK #-} !LaneID,
distance :: {-# UNPACK #-} !Double,
velocity :: {-# UNPACK #-} !Double,
vehtype :: {-# UNPACK #-} !Int,
acceler :: {-# UNPACK #-} !Double,
driver :: {-# UNPACK #-} !Int,
passengers :: {-# UNPACK #-} !Int,
easting :: {-# UNPACK #-} !Double,
northing :: {-# UNPACK #-} !Double,
elevation :: {-# UNPACK #-} !Double,
azimuth :: {-# UNPACK #-} !Double,
user :: {-# UNPACK #-} !Int
} deriving (Show)
-- No need for bang patterns here.
snapshotParser :: Parser Snapshot
snapshotParser = do
sveh <- decimal
tab
stime <- decimal
tab
slink <- decimal
tab
snode <- decimal
tab
slane <- decimal
tab
sdistance <- double
tab
svelocity <- double
tab
svehtype <- decimal
tab
sacceler <- double
tab
sdriver <- decimal
tab
spassengers <- decimal
tab
seasting <- double
tab
snorthing <- double
tab
selevation <- double
tab
sazimuth <- double
tab
suser <- decimal
endOfLine <|> endOfInput
return $ Snapshot
(VehicleId sveh) (Time stime) (LinkID slink) (NodeID snode)
(LaneID slane) sdistance svelocity svehtype sacceler sdriver
spassengers seasting snorthing selevation sazimuth suser
يجب أن يتمتع هذا الإصدار بأداء مقبول حتى لو قمت بإدخال قائمة اللقطات بأكملها في الذاكرة، كما فعلت في ذلك الوقت main
هنا.لقياس ما هو "مقبول"، ضع في اعتبارك أنه بالنظر إلى الحقول الستة عشر (الصغيرة وغير المعبأة) في كل منها Snapshot
بالإضافة إلى تكاليف غير مباشرة التابع Snapshot
ومنشئو القائمة، نحن نتحدث عن 152 بايت لكل خلية قائمة، والتي تتلخص في حوالي 152 ميجابايت لبيانات الاختبار الخاصة بك.على أية حال، هذا الإصدار كسول قدر الإمكان، كما ترون من خلال إزالة التقسيم main
, ، أو استبداله last ss
.
ملحوظة:تم إجراء اختباراتي باستخدام attoparsec-0.12.
نصائح أخرى
بعد تحديث attoparsec إلى الإصدار الأحدث (0.12.0.0) ، انخفض الوقت المستغرق للتنفيذ من 38 ثانية إلى 16 ثانية.هذا أكثر من 50٪ تسريع.كما انخفضت الذاكرة التي يستهلكها بشكل كبير.كما لاحظ @JohnL، مع تمكين التوصيف، تختلف النتائج بشكل كبير.عندما حاولت تعريفه بأحدث إصدار من مكتبة attoparsec، استغرق تنفيذ البرنامج بأكمله حوالي 64 ثانية.