F#
एफ # प्रदर्शन युक्तियाँ और चालें
खोज…
कुशल पुनरावृत्ति के लिए पूंछ-पुनरावृत्ति का उपयोग करना
अनिवार्य भाषाओं से आने वाले कई डेवलपर्स आश्चर्यचकित करते हैं कि एक फॉरेस्ट 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 पर चलने के दौरान परीक्षा परिणाम:
हम नाटकीय अंतर देखते हैं और कुछ परिणाम अप्रत्याशित रूप से खराब हैं।
आइए बुरे मामलों को देखें:
सूची
// 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।
अन्य मामले लगभग इसी तरह के प्रदर्शन करते हैं:
कारण बड़ा परीक्षण रन पर प्रदर्शन में खराबी आ रही है कि लागू की भूमि के ऊपर है 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
के आकार को विभाजित करेंगे लेकिन काम की कुल संख्या को समान होने देंगे।
डेटा पाइपलाइन विकल्प
हम निम्नलिखित विकल्पों की तुलना करेंगे:
- इम्पीरियल कोड
- ऐरे (गैर-आलसी)
- सूची (गैर-आलसी)
- LINQ (आलसी पुल धारा)
- Seq (आलसी पुल धारा)
- नेसोस (आलसी पुल / पुश स्ट्रीम)
- पुलस्ट्रीम (सरलीकृत पुल धारा)
- पुशस्ट्रीम (सरलीकृत धक्का धारा)
हालांकि एक डेटा पाइपलाइन नहीं है, हम Imperative
कोड के खिलाफ तुलना करेंगे क्योंकि सीपीयू कोड को कैसे कार्यान्वित करता है, यह सबसे निकटता से मेल खाता है। डेटा पाइपलाइनों के प्रदर्शन ओवरहेड को मापने के लिए हमें परिणाम की गणना करने के लिए यह सबसे तेज़ संभव तरीका होना चाहिए।
Array
और List
प्रत्येक चरण में एक पूर्ण Array
/ List
List
गणना करती है, इसलिए हम मेमोरी ओवरहेड की अपेक्षा करते हैं।
LINQ
और Seq
दोनों IEnumerable<'T>
आसपास आधारित हैं जो आलसी पुल स्ट्रीम है (पुल का अर्थ है कि उपभोक्ता स्ट्रीम निर्माता स्ट्रीम से डेटा खींच रहा है)। इसलिए हम प्रदर्शन और स्मृति उपयोग के समान होने की उम्मीद करते हैं।
Nessos
एक उच्च-प्रदर्शन स्ट्रीम लाइब्रेरी है जो पुश और पुल (जैसे जावा Stream
) दोनों का समर्थन करती है।
PullStream और PushStream Pull
& Push
धाराओं का सरलीकृत कार्यान्वयन है।
प्रदर्शन परिणाम चालू होने से: F # 4.0 - .NET 4.6.1 - x64
सलाखों को बीता हुआ समय दिखाते हैं, कम बेहतर होता है। सभी परीक्षणों के लिए उपयोगी कार्य की कुल राशि समान है, इसलिए परिणाम तुलनीय हैं। इसका मतलब यह भी है कि कुछ रन बड़े डेटासेट का मतलब है।
हमेशा की तरह जब मापने वाले एक दिलचस्प परिणाम देखते हैं।
-
List
प्रदर्शन खराब बड़े डेटा सेट के लिए अन्य विकल्पों की तुलना में है। यहGC
या खराब कैश इलाके की वजह से हो सकता है। -
Array
प्रदर्शन उम्मीद से बेहतर। -
LINQ
Seq
से बेहतर प्रदर्शन करता है, यह अप्रत्याशित है क्योंकि दोनोंIEnumerable<'T>
आसपास आधारित हैं। हालांकि,Seq
आंतरिक रूप से सभी एल्गोरिदम के लिए एक सामान्य आवेग के आसपास आधारित है, जबकिLINQ
विशेष एल्गोरिदम का उपयोग करता है। -
Push
Pull
से बेहतर प्रदर्शन करता है। यह उम्मीद की जाती है क्योंकि पुश डेटा पाइपलाइन की कम जांच होती है - सरलीकृत
Push
डेटा पाइपलाइनNessos
लिए तुलनीय प्रदर्शन करता है। हालांकि,Nessos
पुल और समानता का समर्थन करता है। - छोटे डेटा पाइपलाइनों के लिए
Nessos
के प्रदर्शन मेंNessos
संभव है क्योंकि पाइपलाइनों की स्थापना ओवरहेड होती है। - उम्मीद के मुताबिक
Imperative
कोड ने बेहतरीन प्रदर्शन किया
जीसी कलेक्शन को चालू रखने से गणना करें: F # 4.0 - .NET 4.6.1 - x64
बार परीक्षण के दौरान GC
संग्रह की कुल संख्या को दिखाता है, कम बेहतर है। यह एक माप है कि डेटा पाइपलाइन द्वारा कितनी वस्तुओं का निर्माण किया जाता है।
हमेशा की तरह जब मापने वाले एक दिलचस्प परिणाम देखते हैं।
-
List
Array
से अधिक ऑब्जेक्ट बनाने की उम्मीद कर रही है क्योंकि एकList
अनिवार्य रूप से नोड्स की एकल लिंक की गई सूची है। एक सरणी एक निरंतर स्मृति क्षेत्र है। - अंतर्निहित संख्याओं को देखते हुए
List
औरArray
2 पीढ़ी के संग्रह को बल देता है। इस तरह के कलेक्शन महंगे हैं। -
Seq
संग्रह की एक आश्चर्यजनक राशि को ट्रिगर कर रहा है। यह आश्चर्यजनक रूप से इस संबंध मेंList
से भी बदतर है। -
LINQ
,Nessos
,Push
औरPull
कुछ रनों के लिए कोई संग्रह नहीं चलाता है। हालांकि, वस्तुओं को आवंटित किया जाता है इसलिएGC
अंततः चलना होगा। - जैसा कि उम्मीद थी कि
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