खोज…


कुशल पुनरावृत्ति के लिए पूंछ-पुनरावृत्ति का उपयोग करना

अनिवार्य भाषाओं से आने वाले कई डेवलपर्स आश्चर्यचकित करते हैं कि एक फॉरेस्ट for-loop कैसे लिखना है जो F# रूप में जल्दी बाहर निकलता है, break , continue या return समर्थन नहीं करता है। F# में उत्तर पूंछ-पुनरावृत्ति का उपयोग करना है जो कि उत्कृष्ट प्रदर्शन प्रदान करते हुए पुनरावृति के लिए एक लचीला और मुहावरेदार तरीका है।

हम List लिए tryFind लागू करना चाहते हैं। अगर F# समर्थित return हम tryFind इस तरह थोड़ा लिखेंगे:

let tryFind predicate vs =
  for v in vs do
    if predicate v then
      return Some v
  None

यह F# में काम नहीं करता है। इसके बजाय हम पूंछ-पुनरावृत्ति का उपयोग करके फ़ंक्शन लिखते हैं:

let tryFind predicate vs =
  let rec loop = function
    | v::vs -> if predicate v then 
                   Some v 
               else 
                   loop vs
    | _ -> None
  loop vs

टेल-रिकर्सन F# में परफॉर्मेंट है क्योंकि जब F# कंपाइलर यह पता लगाता है कि कोई फंक्शन टेल-रिकर्सिव है तो यह रिकर्सन को एक कुशल while-loop में फिर से लिखता है। ILSpy का उपयोग ILSpy हम देख सकते हैं कि यह हमारे फंक्शन loop लिए सही है:

internal static FSharpOption<a> loop@3-10<a>(FSharpFunc<a, bool> predicate, FSharpList<a> _arg1)
{
  while (_arg1.TailOrNull != null)
  {
    FSharpList<a> fSharpList = _arg1;
    FSharpList<a> vs = fSharpList.TailOrNull;
    a v = fSharpList.HeadOrDefault;
    if (predicate.Invoke(v))
    {
      return FSharpOption<a>.Some(v);
    }
    FSharpFunc<a, bool> arg_2D_0 = predicate;
    _arg1 = vs;
    predicate = arg_2D_0;
  }
  return null;
}

कुछ अनावश्यक असाइनमेंट के अलावा (जो उम्मीद है कि जेआईटी-एर समाप्त हो जाता है) यह अनिवार्य रूप से एक कुशल लूप है।

इसके अलावा, पूंछ-पुनरावृत्ति F# लिए मुहावरेदार है क्योंकि यह हमें पारस्परिक स्थिति से बचने की अनुमति देता है। एक sum फ़ंक्शन पर विचार करें जो सभी तत्वों को एक List में रखता है। एक स्पष्ट पहली कोशिश यह होगी:

let sum vs =
  let mutable s = LanguagePrimitives.GenericZero
  for v in vs do
    s <- s + v
  s

यदि हम पूँछ को पुनरावृत्ति में लिखते हैं तो हम परिवर्तनशील अवस्था से बच सकते हैं:

let sum vs =
  let rec loop s = function
    | v::vs -> loop (s + v) vs
    | _ -> s
  loop LanguagePrimitives.GenericZero vs

दक्षता के लिए F# संकलक इसे एक while-loop में बदल देता है जो कि उत्परिवर्तनीय स्थिति का उपयोग करता है।

अपनी प्रदर्शन मान्यताओं को मापें और सत्यापित करें

यह उदाहरण F# को ध्यान में रखते हुए लिखा गया है लेकिन यह विचार सभी वातावरणों में लागू हैं

प्रदर्शन के लिए अनुकूलन करते समय पहला नियम धारणा पर भरोसा नहीं करना है; हमेशा अपनी मान्यताओं को मापें और सत्यापित करें।

जैसा कि हम सीधे मशीन कोड नहीं लिख रहे हैं, यह अनुमान लगाना कठिन है कि कंपाइलर और जेआईटी: एर आपके प्रोग्राम को मशीन कोड में कैसे बदल देते हैं। इसलिए हमें यह देखने के लिए निष्पादन समय को मापने की आवश्यकता है कि हमें प्रदर्शन में सुधार की उम्मीद है और सत्यापित करें कि वास्तविक कार्यक्रम में कोई छिपा हुआ ओवरहेड नहीं है।

सत्यापन काफी सरल प्रक्रिया है जिसमें ILSpy जैसे उदाहरण उपकरण के लिए निष्पादन योग्य इंजीनियरिंग को उल्टा करना शामिल है। JIT: er इसमें पुष्टि करता है कि वास्तविक मशीन कोड को देखना मुश्किल है, लेकिन उल्लेखनीय है। हालांकि, आमतौर पर IL-code जांच बड़े लाभ देती है।

कठिन समस्या मापने की है; कठिन है क्योंकि यह यथार्थवादी स्थितियों को सेट करने के लिए मुश्किल है जो कोड में सुधार को मापने की अनुमति देता है। फिर भी मापन अमूल्य है।

सरल एफ # कार्यों का विश्लेषण

चलो कुछ सरल जांच F# कार्यों कि में सभी पूर्णांकों जम जाता है 1..n विभिन्न अलग अलग तरीकों से लिखा है। चूंकि सीमा एक सरल अंकगणितीय श्रृंखला है, परिणाम को सीधे गणना की जा सकती है लेकिन इस उदाहरण के उद्देश्य से हम सीमा पर पुनरावृति करते हैं।

सबसे पहले हम किसी फंक्शन को मापने के लिए कुछ उपयोगी कार्यों को परिभाषित करते हैं:

// now () returns current time in milliseconds since start
let now : unit -> int64 =
  let sw = System.Diagnostics.Stopwatch ()
  sw.Start ()
  fun () -> sw.ElapsedMilliseconds

// time estimates the time 'action' repeated a number of times
let time repeat action : int64*'T =
  let v = action ()  // Warm-up and compute value

  let b = now ()
  for i = 1 to repeat do
    action () |> ignore
  let e = now ()

  e - b, v

time एक कार्रवाई चलाता है बार-बार हमें कुछ सौ मिलीसेकंड के लिए परीक्षणों को चलाने की आवश्यकता होती है ताकि विचरण को कम किया जा सके।

फिर हम कुछ कार्यों को परिभाषित करते हैं जो सभी पूर्णांकों को 1..n में अलग-अलग तरीकों से जमा करते हैं।

// Accumulates all integers 1..n using 'List'
let accumulateUsingList n =
  List.init (n + 1) id
  |> List.sum

// Accumulates all integers 1..n using 'Seq'
let accumulateUsingSeq n =
  Seq.init (n + 1) id
  |> Seq.sum

// Accumulates all integers 1..n using 'for-expression'
let accumulateUsingFor n =
  let mutable sum = 0
  for i = 1 to n do
    sum <- sum + i
  sum

// Accumulates all integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach n =
  let mutable sum = 0
  for i in 1..n do
    sum <- sum + i
  sum

// Accumulates all integers 1..n using 'foreach-expression' over list range
let accumulateUsingForEachOverList n =
  let mutable sum = 0
  for i in [1..n] do
    sum <- sum + i
  sum

// Accumulates every second integer 1..n using 'foreach-expression' over range
let accumulateUsingForEachStep2 n =
  let mutable sum = 0
  for i in 1..2..n do
    sum <- sum + i
  sum

// Accumulates all 64 bit integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach64 n =
  let mutable sum = 0L
  for i in 1L..int64 n do
    sum <- sum + i
  sum |> int

// Accumulates all integers n..1 using 'for-expression' in reverse order
let accumulateUsingReverseFor n =
  let mutable sum = 0
  for i = n downto 1 do
    sum <- sum + i
  sum

// Accumulates all 64 integers n..1 using 'tail-recursion' in reverse order
let accumulateUsingReverseTailRecursion n =
  let rec loop sum i =
    if i > 0 then
      loop (sum + i) (i - 1)
    else
      sum
  loop 0 n

हम परिणाम को एक समान मानते हैं ( 2 वेतन वृद्धि का उपयोग करने वाले कार्यों में से एक को छोड़कर) लेकिन प्रदर्शन में अंतर है। इसे मापने के लिए निम्नलिखित फ़ंक्शन को परिभाषित किया गया है:

let testRun (path : string) =
  use testResult = new System.IO.StreamWriter (path)
  let write   (l : string)  = testResult.WriteLine l
  let writef  fmt           = FSharp.Core.Printf.kprintf write fmt

  write "Name\tTotal\tOuter\tInner\tCC0\tCC1\tCC2\tTime\tResult"

  // total is the total number of iterations being executed
  let total   = 10000000
  // outers let us variate the relation between the inner and outer loop
  //  this is often useful when the algorithm allocates different amount of memory
  //  depending on the input size. This can affect cache locality
  let outers  = [| 1000; 10000; 100000 |]
  for outer in outers do
    let inner = total / outer

    // multiplier is used to increase resolution of certain tests that are significantly
    //  faster than the slower ones

    let testCases =
      [|
    //   Name of test                         multiplier    action
        "List"                              , 1           , accumulateUsingList
        "Seq"                               , 1           , accumulateUsingSeq
        "for-expression"                    , 100         , accumulateUsingFor
        "foreach-expression"                , 100         , accumulateUsingForEach
        "foreach-expression over List"      , 1           , accumulateUsingForEachOverList
        "foreach-expression increment of 2" , 1           , accumulateUsingForEachStep2
        "foreach-expression over 64 bit"    , 1           , accumulateUsingForEach64
        "reverse for-expression"            , 100         , accumulateUsingReverseFor
        "reverse tail-recursion"            , 100         , accumulateUsingReverseTailRecursion
      |]
    for name, multiplier, a in testCases do
      System.GC.Collect (2, System.GCCollectionMode.Forced, true)
      let cc g = System.GC.CollectionCount g

      printfn "Accumulate using %s with outer=%d and inner=%d ..." name outer inner

      // Collect collection counters before test run
      let pcc0, pcc1, pcc2 = cc 0, cc 1, cc 2

      let ms, result       = time (outer*multiplier) (fun () -> a inner)
      let ms               = (float ms / float multiplier)

      // Collect collection counters after test run
      let acc0, acc1, acc2 = cc 0, cc 1, cc 2
      let cc0, cc1, cc2    = acc0 - pcc0, acc1 - pcc1, acc1 - pcc1
      printfn "  ... took: %f ms, GC collection count %d,%d,%d and produced %A" ms cc0 cc1 cc2 result

      writef "%s\t%d\t%d\t%d\t%d\t%d\t%d\t%f\t%d" name total outer inner cc0 cc1 cc2 ms result

.NET 4.5.2 x64 पर चलने के दौरान परीक्षा परिणाम:

.NET 4.5.2 x64 पर परीक्षा परिणाम

हम नाटकीय अंतर देखते हैं और कुछ परिणाम अप्रत्याशित रूप से खराब हैं।

आइए बुरे मामलों को देखें:

सूची

// Accumulates all integers 1..n using 'List'
let accumulateUsingList n =
  List.init (n + 1) id
  |> List.sum

यहाँ क्या होता है एक पूर्ण सूची में सभी पूर्णांक 1..n हैं। 1..n एक राशि का उपयोग करके बनाया और घटाया जाता है। यह सीमा से अधिक पुनरावृत्ति और संचय करने की तुलना में अधिक महंगा होना चाहिए, यह लूप के लिए लगभग ~ 42x धीमा लगता है।

इसके अलावा, हम देख सकते हैं कि जीसी टेस्ट रन के दौरान लगभग 100x चला गया क्योंकि कोड ने बहुत सारी वस्तुओं को आवंटित किया था। इससे सीपीयू भी खर्च होता है।

seq

// Accumulates all integers 1..n using 'Seq'
let accumulateUsingSeq n =
  Seq.init (n + 1) id
  |> Seq.sum

Seq संस्करण पूर्ण List आवंटित नहीं करता है, इसलिए यह थोड़ा आश्चर्य की बात है कि लूप के लिए यह ~ 270x धीमा है। इसके अलावा, हम देखते हैं कि जीसी ने 661x निष्पादित किया है।

जब आइटम प्रति काम की मात्रा बहुत छोटा हो (इस मामले में दो पूर्णांकों को जोड़ते हुए), तो Seq अक्षम है।

बात यह है कि कभी भी Seq उपयोग न करें। बात नापने की है।

( manofstick edit: Seq.init इस गंभीर प्रदर्शन के मुद्दे का अपराधी है। यह Seq.init (n+1) id बजाय { 0 .. n } अभिव्यक्ति का उपयोग करने के लिए अधिक प्रभावशाली है। यह अभी भी बहुत अधिक कुशल हो जाएगा। जब इस पीआर विलय कर दिया और जारी की है यहां तक कि रिहाई हालांकि, मूल के बाद। Seq.init ... |> Seq.sum अभी भी धीमी गति से हो जाएगा, लेकिन कुछ हद तक जवाबी intuitively, Seq.init ... |> Seq.map id |> Seq.sum काफी तेज होगा। यह Seq.init कार्यान्वयन के साथ पिछड़ी संगतता बनाए रखने के लिए था, जो Current में Current गणना नहीं करता है, बल्कि उन्हें एक Lazy वस्तु में लपेटता है - हालांकि यह भी थोड़ा बेहतर प्रदर्शन करना चाहिए यह PR । नोट एडिटर को: क्षमा करें, यह एक तरह का रेकिंग नोट्स है, लेकिन मैं नहीं चाहता कि लोग Seq को बंद कर दें जब सुधार सिर्फ कोने के आसपास हो ... जब वह समय आता है तो चार्ट्स को अपडेट करना अच्छा होगा वह इस पृष्ठ पर हैं। )

सूची के लिए फ़ार्च-अभिव्यक्ति

// Accumulates all integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach n =
  let mutable sum = 0
  for i in 1..n do
    sum <- sum + i
  sum

// Accumulates all integers 1..n using 'foreach-expression' over list range
let accumulateUsingForEachOverList n =
  let mutable sum = 0
  for i in [1..n] do
    sum <- sum + i
  sum

इन दो फ़ंक्शन के बीच का अंतर बहुत सूक्ष्म है, लेकिन प्रदर्शन अंतर लगभग ~ 76x नहीं है। क्यों? चलो इंजीनियर को बुरे कोड को उल्टा करते हैं:

public static int accumulateUsingForEach(int n)
{
  int sum = 0;
  int i = 1;
  if (n >= i)
  {
    do
    {
      sum += i;
      i++;
    }
    while (i != n + 1);
  }
  return sum;
}

public static int accumulateUsingForEachOverList(int n)
{
  int sum = 0;
  FSharpList<int> fSharpList = SeqModule.ToList<int>(Operators.CreateSequence<int>(Operators.OperatorIntrinsics.RangeInt32(1, 1, n)));
  for (FSharpList<int> tailOrNull = fSharpList.TailOrNull; tailOrNull != null; tailOrNull = fSharpList.TailOrNull)
  {
    int i = fSharpList.HeadOrDefault;
    sum += i;
    fSharpList = tailOrNull;
  }
  return sum;
}

for i in [1..n] लूप के while एक कुशल के रूप में accumulateUsingForEach किया जाता है, लेकिन for i in [1..n] इसे रूपांतरित किया जाता है:

FSharpList<int> fSharpList =
  SeqModule.ToList<int>(
    Operators.CreateSequence<int>(
      Operators.OperatorIntrinsics.RangeInt32(1, 1, n)));

इसका मतलब है कि पहले हम 1..n से अधिक का एक Seq 1..n और अंत में ToList को कॉल ToList

महंगा।

foreach-अभिव्यक्ति वृद्धि 2

// Accumulates all integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach n =
  let mutable sum = 0
  for i in 1..n do
    sum <- sum + i
  sum

// Accumulates every second integer 1..n using 'foreach-expression' over range
let accumulateUsingForEachStep2 n =
  let mutable sum = 0
  for i in 1..2..n do
    sum <- sum + i
  sum

एक बार फिर इन दोनों कार्यों के बीच अंतर सूक्ष्म है, लेकिन प्रदर्शन अंतर क्रूर है: ~ 25x

एक बार फिर आइए चलते हैं ILSpy :

public static int accumulateUsingForEachStep2(int n)
{
  int sum = 0;
  IEnumerable<int> enumerable = Operators.OperatorIntrinsics.RangeInt32(1, 2, n);
  foreach (int i in enumerable)
  {
    sum += i;
  }
  return sum;
}

एक Seq 1..2..n से अधिक का बनाया जाता है और फिर हम Enumerator का उपयोग करके Seq पर पुनरावृति करते हैं।

हम F# उम्मीद कर रहे थे कि ऐसा कुछ बनाया जाए:

public static int accumulateUsingForEachStep2(int n)
{
  int sum = 0;
  for (int i = 1; i < n; i += 2)
  {
    sum += i;
  }
  return sum;
}

हालाँकि, F# संकलक केवल एक से बढ़ कर int32 पर्वतमाला पर छोरों के लिए कुशल का समर्थन करता है। अन्य सभी मामलों के लिए यह Operators.OperatorIntrinsics.RangeInt32 पर वापस आती है। जो अगले सुपरराइजिंग परिणाम की व्याख्या करेगा

64 बिट से अधिक के लिए फोरचेक-अभिव्यक्ति

// Accumulates all 64 bit integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach64 n =
  let mutable sum = 0L
  for i in 1L..int64 n do
    sum <- sum + i
  sum |> int

यह लूप के लिए 47x धीमी गति से प्रदर्शन करता है, एकमात्र अंतर यह है कि हम 64 बिट पूर्णांक पर पुनरावृति करते हैं। ILSpy हमें दिखाता है क्यों:

public static int accumulateUsingForEach64(int n)
{
  long sum = 0L;
  IEnumerable<long> enumerable = Operators.OperatorIntrinsics.RangeInt64(1L, 1L, (long)n);
  foreach (long i in enumerable)
  {
    sum += i;
  }
  return (int)sum;
}

F# केवल int32 संख्याओं के लिए लूप्स के लिए कुशल का समर्थन करता है, जिसमें इसे फ़ॉलबैक Operators.OperatorIntrinsics.RangeInt64 का उपयोग करना होता है ।peratorIntrinsics.RangeInt64।

अन्य मामले लगभग इसी तरह के प्रदर्शन करते हैं:

.NET 4.5.2 x64 पर परीक्षा परिणाम

कारण बड़ा परीक्षण रन पर प्रदर्शन में खराबी आ रही है कि लागू की भूमि के ऊपर है action हम में और कम से कम के रूप में काम कर रही बढ़ रही है action

0 ओर लूपिंग कभी-कभी प्रदर्शन लाभ दे सकता है क्योंकि यह एक सीपीयू रजिस्टर को बचा सकता है, लेकिन इस मामले में सीपीयू के पास स्पेयर करने के लिए रजिस्टर होता है, इसलिए इससे कोई फर्क नहीं पड़ता है।

निष्कर्ष

माप महत्वपूर्ण है क्योंकि अन्यथा हम सोच सकते हैं कि ये सभी विकल्प समकक्ष हैं लेकिन कुछ विकल्प दूसरों की तुलना में ~ 270x धीमे हैं।

सत्यापन कदम में रिवर्स इंजीनियरिंग शामिल है निष्पादन योग्य हमें यह समझाने में मदद करता है कि हमने प्रदर्शन क्यों किया या हमें अपेक्षित प्रदर्शन नहीं मिला। इसके अलावा, सत्यापन हमें उन मामलों में प्रदर्शन की भविष्यवाणी करने में मदद कर सकता है जो एक उचित माप करना बहुत मुश्किल है।

प्रदर्शन को हमेशा मापना मुश्किल है, हमेशा अपनी प्रदर्शन मान्यताओं को सत्यापित करें।

विभिन्न एफ # डेटा पाइपलाइनों की तुलना

F# डेटा पाइपलाइन बनाने के लिए कई विकल्प हैं, उदाहरण के लिए: List , Seq और Array

मेमोरी उपयोग और प्रदर्शन के दृष्टिकोण से कौन सी डेटा पाइपलाइन बेहतर है?

इसका उत्तर देने के लिए हम विभिन्न पाइपलाइनों का उपयोग करके प्रदर्शन और मेमोरी उपयोग की तुलना करेंगे।

डेटा पाइपलाइन

ओवरहेड को मापने के लिए, हम संसाधित किए गए प्रति आइटम कम सीपीयू-लागत के साथ एक डेटा पाइपलाइन का उपयोग करेंगे:

let seqTest n =
  Seq.init (n + 1) id
  |> Seq.map    int64
  |> Seq.filter (fun v -> v % 2L = 0L)
  |> Seq.map    ((+) 1L)
  |> Seq.sum

हम सभी विकल्पों के लिए समान पाइपलाइन बनाएंगे और उनकी तुलना करेंगे।

हम n के आकार को विभाजित करेंगे लेकिन काम की कुल संख्या को समान होने देंगे।

डेटा पाइपलाइन विकल्प

हम निम्नलिखित विकल्पों की तुलना करेंगे:

  1. इम्पीरियल कोड
  2. ऐरे (गैर-आलसी)
  3. सूची (गैर-आलसी)
  4. LINQ (आलसी पुल धारा)
  5. Seq (आलसी पुल धारा)
  6. नेसोस (आलसी पुल / पुश स्ट्रीम)
  7. पुलस्ट्रीम (सरलीकृत पुल धारा)
  8. पुशस्ट्रीम (सरलीकृत धक्का धारा)

हालांकि एक डेटा पाइपलाइन नहीं है, हम Imperative कोड के खिलाफ तुलना करेंगे क्योंकि सीपीयू कोड को कैसे कार्यान्वित करता है, यह सबसे निकटता से मेल खाता है। डेटा पाइपलाइनों के प्रदर्शन ओवरहेड को मापने के लिए हमें परिणाम की गणना करने के लिए यह सबसे तेज़ संभव तरीका होना चाहिए।

Array और List प्रत्येक चरण में एक पूर्ण Array / List List गणना करती है, इसलिए हम मेमोरी ओवरहेड की अपेक्षा करते हैं।

LINQ और Seq दोनों IEnumerable<'T> आसपास आधारित हैं जो आलसी पुल स्ट्रीम है (पुल का अर्थ है कि उपभोक्ता स्ट्रीम निर्माता स्ट्रीम से डेटा खींच रहा है)। इसलिए हम प्रदर्शन और स्मृति उपयोग के समान होने की उम्मीद करते हैं।

Nessos एक उच्च-प्रदर्शन स्ट्रीम लाइब्रेरी है जो पुश और पुल (जैसे जावा Stream ) दोनों का समर्थन करती है।

PullStream और PushStream Pull & Push धाराओं का सरलीकृत कार्यान्वयन है।

प्रदर्शन परिणाम चालू होने से: F # 4.0 - .NET 4.6.1 - x64

प्रदर्शन परिणाम चालू होने से: F # 4.0 - .NET 4.6.1 - x64

सलाखों को बीता हुआ समय दिखाते हैं, कम बेहतर होता है। सभी परीक्षणों के लिए उपयोगी कार्य की कुल राशि समान है, इसलिए परिणाम तुलनीय हैं। इसका मतलब यह भी है कि कुछ रन बड़े डेटासेट का मतलब है।

हमेशा की तरह जब मापने वाले एक दिलचस्प परिणाम देखते हैं।

  1. List प्रदर्शन खराब बड़े डेटा सेट के लिए अन्य विकल्पों की तुलना में है। यह GC या खराब कैश इलाके की वजह से हो सकता है।
  2. Array प्रदर्शन उम्मीद से बेहतर।
  3. LINQ Seq से बेहतर प्रदर्शन करता है, यह अप्रत्याशित है क्योंकि दोनों IEnumerable<'T> आसपास आधारित हैं। हालांकि, Seq आंतरिक रूप से सभी एल्गोरिदम के लिए एक सामान्य आवेग के आसपास आधारित है, जबकि LINQ विशेष एल्गोरिदम का उपयोग करता है।
  4. Push Pull से बेहतर प्रदर्शन करता है। यह उम्मीद की जाती है क्योंकि पुश डेटा पाइपलाइन की कम जांच होती है
  5. सरलीकृत Push डेटा पाइपलाइन Nessos लिए तुलनीय प्रदर्शन करता है। हालांकि, Nessos पुल और समानता का समर्थन करता है।
  6. छोटे डेटा पाइपलाइनों के लिए Nessos के प्रदर्शन में Nessos संभव है क्योंकि पाइपलाइनों की स्थापना ओवरहेड होती है।
  7. उम्मीद के मुताबिक Imperative कोड ने बेहतरीन प्रदर्शन किया

जीसी कलेक्शन को चालू रखने से गणना करें: F # 4.0 - .NET 4.6.1 - x64

जीसी कलेक्शन को चालू रखने से गणना करें: F # 4.0 - .NET 4.6.1 - x64

बार परीक्षण के दौरान GC संग्रह की कुल संख्या को दिखाता है, कम बेहतर है। यह एक माप है कि डेटा पाइपलाइन द्वारा कितनी वस्तुओं का निर्माण किया जाता है।

हमेशा की तरह जब मापने वाले एक दिलचस्प परिणाम देखते हैं।

  1. List Array से अधिक ऑब्जेक्ट बनाने की उम्मीद कर रही है क्योंकि एक List अनिवार्य रूप से नोड्स की एकल लिंक की गई सूची है। एक सरणी एक निरंतर स्मृति क्षेत्र है।
  2. अंतर्निहित संख्याओं को देखते हुए List और Array 2 पीढ़ी के संग्रह को बल देता है। इस तरह के कलेक्शन महंगे हैं।
  3. Seq संग्रह की एक आश्चर्यजनक राशि को ट्रिगर कर रहा है। यह आश्चर्यजनक रूप से इस संबंध में List से भी बदतर है।
  4. LINQ , Nessos , Push और Pull कुछ रनों के लिए कोई संग्रह नहीं चलाता है। हालांकि, वस्तुओं को आवंटित किया जाता है इसलिए GC अंततः चलना होगा।
  5. जैसा कि उम्मीद थी कि Imperative कोड आवंटित करने के बाद कोई भी GC संग्रह ट्रिगर नहीं किया गया था।

निष्कर्ष

सभी डेटा पाइपलाइन सभी परीक्षण मामलों में समान कार्य करते हैं लेकिन हम विभिन्न पाइपलाइनों के बीच प्रदर्शन और स्मृति उपयोग में महत्वपूर्ण अंतर देखते हैं।

इसके अलावा, हम देखते हैं कि डेटा पाइपलाइनों का ओवरहेड संसाधित किए गए डेटा के आकार के आधार पर भिन्न होता है। उदाहरण के लिए, छोटे आकारों के लिए Array काफी अच्छा प्रदर्शन कर रहा है।

ओवरहेड को मापने के लिए पाइप लाइन में प्रत्येक चरण में किए गए काम की मात्रा को ध्यान में रखना चाहिए। "वास्तविक" स्थितियों में Seq का ओवरहेड मायने नहीं रख सकता क्योंकि वास्तविक कार्य में अधिक समय लगता है।

अधिक चिंता की बात यह है कि मेमोरी उपयोग अंतर है। GC मुक्त नहीं है और यह GC दबाव को कम रखने के लिए लंबे समय तक चलने वाले अनुप्रयोगों के लिए फायदेमंद है।

F# डेवलपर्स के प्रदर्शन और स्मृति उपयोग के बारे में चिंतित यह Nessos स्ट्रीम की जाँच करने के लिए अनुशंसित है।

यदि आपको रणनीतिक रूप से रखे गए शीर्ष-प्रदर्शन की आवश्यकता है तो Imperative कोड विचार करने योग्य है।

अंत में, जब प्रदर्शन की बात आती है तो धारणा नहीं बनाते हैं। उपाय और सत्यापित करें।

पूर्ण स्रोत कोड:

module PushStream =
  type Receiver<'T> = 'T -> bool
  type Stream<'T>   = Receiver<'T> -> unit

  let inline filter (f : 'T -> bool) (s : Stream<'T>) : Stream<'T> =
    fun r -> s (fun v -> if f v then r v else true)

  let inline map (m : 'T -> 'U) (s : Stream<'T>) : Stream<'U> =
    fun r -> s (fun v -> r (m v))

  let inline range b e : Stream<int> =
    fun r ->
      let rec loop i = if i <= e && r i then loop (i + 1)
      loop b

  let inline sum (s : Stream<'T>) : 'T =
    let mutable state = LanguagePrimitives.GenericZero<'T>
    s (fun v -> state <- state + v; true)
    state

module PullStream =

  [<Struct>]
  [<NoComparison>]
  [<NoEqualityAttribute>]
  type Maybe<'T>(v : 'T, hasValue : bool) =
    member    x.Value        = v
    member    x.HasValue     = hasValue
    override  x.ToString ()  =
      if hasValue then
        sprintf "Just %A" v
      else
        "Nothing"

  let Nothing<'T>     = Maybe<'T> (Unchecked.defaultof<'T>, false)
  let inline Just v   = Maybe<'T> (v, true)

  type Iterator<'T> = unit -> Maybe<'T>
  type Stream<'T>   = unit -> Iterator<'T>

  let filter (f : 'T -> bool) (s : Stream<'T>) : Stream<'T> =
    fun () ->
      let i = s ()
      let rec pop () =
        let mv = i ()
        if mv.HasValue then
          let v = mv.Value
          if f v then Just v else pop ()
        else
          Nothing
      pop

  let map (m : 'T -> 'U) (s : Stream<'T>) : Stream<'U> =
    fun () ->
      let i = s ()
      let pop () =
        let mv = i ()
        if mv.HasValue then
          Just (m mv.Value)
        else
          Nothing
      pop

  let range b e : Stream<int> =
    fun () ->
      let mutable i = b
      fun () ->
        if i <= e then
          let p = i
          i <- i + 1
          Just p
        else
          Nothing

  let inline sum (s : Stream<'T>) : 'T =
    let i = s ()
    let rec loop state =
      let mv = i ()
      if mv.HasValue then
        loop (state + mv.Value)
      else
        state
    loop LanguagePrimitives.GenericZero<'T>

module PerfTest =

  open System.Linq
#if USE_NESSOS
  open Nessos.Streams
#endif

  let now =
    let sw = System.Diagnostics.Stopwatch ()
    sw.Start ()
    fun () -> sw.ElapsedMilliseconds

  let time n a =
    let inline cc i       = System.GC.CollectionCount i

    let v                 = a ()

    System.GC.Collect (2, System.GCCollectionMode.Forced, true)

    let bcc0, bcc1, bcc2  = cc 0, cc 1, cc 2
    let b                 = now ()

    for i in 1..n do
      a () |> ignore

    let e = now ()
    let ecc0, ecc1, ecc2  = cc 0, cc 1, cc 2

    v, (e - b), ecc0 - bcc0, ecc1 - bcc1, ecc2 - bcc2

  let arrayTest n =
    Array.init (n + 1) id
    |> Array.map    int64
    |> Array.filter (fun v -> v % 2L = 0L)
    |> Array.map    ((+) 1L)
    |> Array.sum

  let imperativeTest n =
    let rec loop s i =
      if i >= 0L then
        if i % 2L = 0L then
          loop (s + i + 1L) (i - 1L)
        else
          loop s (i - 1L)
      else
        s
    loop 0L (int64 n)

  let linqTest n =
    (((Enumerable.Range(0, n + 1)).Select int64).Where(fun v -> v % 2L = 0L)).Select((+) 1L).Sum()

  let listTest n =
    List.init (n + 1) id
    |> List.map     int64
    |> List.filter  (fun v -> v % 2L = 0L)
    |> List.map     ((+) 1L)
    |> List.sum

#if USE_NESSOS
  let nessosTest n =
    Stream.initInfinite id
    |> Stream.take    (n + 1)
    |> Stream.map     int64
    |> Stream.filter  (fun v -> v % 2L = 0L)
    |> Stream.map     ((+) 1L)
    |> Stream.sum
#endif

  let pullTest n =
    PullStream.range 0 n
    |> PullStream.map     int64
    |> PullStream.filter  (fun v -> v % 2L = 0L)
    |> PullStream.map     ((+) 1L)
    |> PullStream.sum

  let pushTest n =
    PushStream.range 0 n
    |> PushStream.map     int64
    |> PushStream.filter  (fun v -> v % 2L = 0L)
    |> PushStream.map     ((+) 1L)
    |> PushStream.sum

  let seqTest n =
    Seq.init (n + 1) id
    |> Seq.map    int64
    |> Seq.filter (fun v -> v % 2L = 0L)
    |> Seq.map    ((+) 1L)
    |> Seq.sum

  let perfTest (path : string) =
    let testCases =
      [|
        "array"       , arrayTest       
        "imperative"  , imperativeTest  
        "linq"        , linqTest        
        "list"        , listTest        
        "seq"         , seqTest         
#if USE_NESSOS
        "nessos"      , nessosTest      
#endif
        "pull"        , pullTest        
        "push"        , pushTest        
      |]
    use out                   = new System.IO.StreamWriter (path)
    let write (msg : string)  = out.WriteLine msg
    let writef fmt            = FSharp.Core.Printf.kprintf write fmt

    write "Name\tTotal\tOuter\tInner\tElapsed\tCC0\tCC1\tCC2\tResult"

    let total   = 10000000
    let outers = [| 10; 1000; 1000000 |]
    for outer in outers do
      let inner = total / outer
      for name, a in testCases do
        printfn "Running %s with total=%d, outer=%d, inner=%d ..." name total outer inner
        let v, ms, cc0, cc1, cc2 = time outer (fun () -> a inner)
        printfn "  ... %d ms, cc0=%d, cc1=%d, cc2=%d, result=%A" ms cc0 cc1 cc2 v
        writef "%s\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d" name total outer inner ms cc0 cc1 cc2 v

[<EntryPoint>]
let main argv =
  System.Environment.CurrentDirectory <- System.AppDomain.CurrentDomain.BaseDirectory
  PerfTest.perfTest "perf.tsv"
  0


Modified text is an extract of the original Stack Overflow Documentation
के तहत लाइसेंस प्राप्त है CC BY-SA 3.0
से संबद्ध नहीं है Stack Overflow