F#
F # Советы и подсказки производительности
Поиск…
Использование хвостовой рекурсии для эффективной итерации
Исходя из императивных языков, многие разработчики задаются вопросом, как написать for-loop
который выходит раньше, поскольку F#
не поддерживает break
, continue
или return
. Ответ в F#
заключается в использовании хвостовой рекурсии, которая является гибким и идиоматическим способом итерации, при этом обеспечивая отличную производительность.
Предположим, мы хотим реализовать tryFind
для List
. Если F#
поддерживает return
мы бы немного напишем tryFind
следующим образом:
let tryFind predicate vs =
for v in vs do
if predicate v then
return Some v
None
Это не работает в F#
. Вместо этого мы записываем функцию с помощью tail-recursion:
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
мы видим, что это верно для нашего 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;
}
Помимо некоторых ненужных назначений (которые, надеюсь, удаляет JIT-er), это, по сути, эффективный цикл.
Кроме того, хвостовая рекурсия является идиоматической для 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#
но идеи применимы во всех средах
Первое правило при оптимизации производительности - не полагаться на предположение; Всегда измерять и проверять свои предположения.
Поскольку мы не пишем машинный код напрямую, трудно предсказать, как компилятор и JIT: er преобразуют вашу программу в машинный код. Вот почему нам нужно измерить время выполнения, чтобы убедиться, что мы ожидаем повышения производительности, и убедитесь, что фактическая программа не содержит никаких скрытых служебных данных.
Проверка - довольно простой процесс, который включает в себя обратное проектирование исполняемого файла, используя, например, инструменты, такие как ILSpy . JIT: er усложняет проверку, поскольку просмотр фактического машинного кода является сложным, но выполнимым. Однако, как правило, изучение IL-code
дает большие выгоды.
Более сложная проблема - измерение; сложнее, потому что сложно настроить реалистичные ситуации, которые позволяют измерять улучшения в коде. Все еще измерение неоценимо.
Анализ простых функций F #
Рассмотрим некоторые простые функции 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
создается и уменьшается с использованием суммы. Это должно быть более дорогостоящим, чем просто повторение и накопление в диапазоне, похоже, примерно на 42x медленнее, чем цикл for.
Кроме того, мы видим, что GC выполнял около 100x во время тестового прогона, потому что код выделял много объектов. Это также стоит CPU.
Seq
// Accumulates all integers 1..n using 'Seq'
let accumulateUsingSeq n =
Seq.init (n + 1) id
|> Seq.sum
Версия Seq
не выделяет полный List
поэтому немного удивляет, что это ~ 270x медленнее, чем цикл for. Кроме того, мы видим, что GC выполнил 661x.
Seq
неэффективен, когда количество работы на один элемент очень невелико (в этом случае агрегирование двух целых чисел).
Дело не в том, чтобы никогда не использовать Seq
. Дело в том, чтобы измерить.
( manofstick edit: Seq.init
является виновником этой серьезной проблемы с производительностью. Гораздо эффективнее использовать выражение { 0 .. n }
вместо Seq.init (n+1) id
. Это станет намного более эффективным когда этот PR объединен и выпущен. Даже после релиза исходный Seq.init ... |> Seq.sum
все равно будет медленным, но несколько конт-интуитивно, Seq.init ... |> Seq.map id |> Seq.sum
будет довольно быстрым. Это должно было поддерживать обратную совместимость с реализацией Seq.init
, которая не вычисляет Current
изначально, а скорее обертывает их в Lazy
объект, хотя это тоже должно немного улучшиться из-за это PR . Примечание для редактора: извините, это своего рода бессвязные заметки, но я не хочу, чтобы люди были отстранены от Seq, когда улучшение только за углом ... Когда это время придет, было бы хорошо обновить диаграммы которые находятся на этой странице. )
foreach-expression через список
// 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
Разница между этими двумя функциями очень тонкая, но разница в производительности не составляет примерно 76х. Зачем? Давайте перепроектировать плохой код:
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;
}
accumulateUsingForEach
реализован как эффективный в while
цикла , но for i in [1..n]
превращается в:
FSharpList<int> fSharpList =
SeqModule.ToList<int>(
Operators.CreateSequence<int>(
Operators.OperatorIntrinsics.RangeInt32(1, 1, n)));
Это означает, что сначала мы создаем Seq
над 1..n
и, наконец, ToList
.
Дорого.
приращение foreach-expression 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;
}
A Seq
создается над 1..2..n
а затем мы перебираем 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
. Что объяснит следующий сюрпризный результат
foreach-expression более 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
Это выполняет ~ 47 раз медленнее, чем цикл for, единственное отличие состоит в том, что мы перебираем более 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
.
Другие случаи примерно одинаковы:
Причина снижения производительности для более крупных тестовых прогонов заключается в том, что накладные расходы при вызове action
растут, поскольку мы делаем все меньше и меньше работы в action
.
Цикл в направлении 0
может иногда давать преимущества производительности, так как он может сохранить регистр CPU, но в этом случае CPU имеет резервные копии, поэтому он, похоже, не имеет значения.
Заключение
Измерение важно, потому что в противном случае мы могли бы подумать, что все эти альтернативы эквивалентны, но некоторые альтернативы медленнее, чем другие.
Шаг подтверждения включает обратное проектирование, исполняемый файл помогает нам объяснить, почему мы сделали или не достигли ожидаемой производительности. Кроме того, проверка может помочь нам прогнозировать производительность в случаях, когда слишком сложно выполнить правильное измерение.
Трудно прогнозировать производительность всегда. Измерьте, всегда проверяйте свои предположения о производительности.
Сравнение различных конвейеров данных F #
В 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 (Lazy pull stream)
- Seq (Lazy pull stream)
- Нессос (ленивый трюк / толчок)
- PullStream (упрощенный поток тяги)
- PushStream (Упрощенный поток push)
Хотя это не конвейер данных, мы будем сравнивать с Imperative
кодом, так как это наиболее точно соответствует тому, как процессор выполняет код. Это должен быть самый быстрый способ вычислить результат, позволяющий нам измерить накладные расходы на производительность для конвейеров данных.
Array
и List
вычисляют полный Array
/ List
на каждом шаге, поэтому мы ожидаем нехватки памяти.
LINQ
и Seq
основаны на IEnumerable<'T>
который представляет собой ленивый поток pull (pull означает, что потребительский поток вытаскивает данные из потока производителя). Поэтому мы ожидаем, что производительность и использование памяти будут одинаковыми.
Nessos
- это высокопроизводительная потоковая библиотека, которая поддерживает push и pull (например, Java 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
ухудшается, потому что накладные расходы на установку трубопроводов. - Как и ожидалось,
Imperative
код
GC Collection count from running on: 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 Streams .
Если вам нужна первоклассная производительность, стратегически размещенный 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