Поиск…


Использование хвостовой рекурсии для эффективной итерации

Исходя из императивных языков, многие разработчики задаются вопросом, как написать 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:

Результаты тестов на .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 .

Другие случаи примерно одинаковы:

Результаты тестов на .NET 4.5.2 x64

Причина снижения производительности для более крупных тестовых прогонов заключается в том, что накладные расходы при вызове 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 но пусть общее число работ будет одинаковым.

Альтернативные источники данных

Мы сравним следующие альтернативы:

  1. Императивный код
  2. Массив (нелогичный)
  3. Список (не ленивый)
  4. LINQ (Lazy pull stream)
  5. Seq (Lazy pull stream)
  6. Нессос (ленивый трюк / толчок)
  7. PullStream (упрощенный поток тяги)
  8. 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

Производительность Результаты работы: 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 ухудшается, потому что накладные расходы на установку трубопроводов.
  7. Как и ожидалось, Imperative код

GC Collection count from running on: F # 4.0 - .NET 4.6.1 - x64

GC Collection count from running on: 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 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


Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow