Почему использование последовательности намного медленнее, чем использование списка в этом примере

Предыстория: у меня есть последовательность непрерывных данных с отметками времени. В последовательности данных есть дыры, некоторые большие, другие - только одно пропущенное значение.
Когда дыра представляет собой всего лишь одно пропущенное значение, я хочу исправить дыры, используя фиктивное значение (большие дыры будут проигнорированы) .

Я хотел бы использовать ленивую генерацию исправленной последовательности, поэтому я использую Seq.unfold.

Я сделал две версии метода, чтобы залатать дыры в данных.

Первый использует последовательность данных с дырками в ней и создает исправленную последовательность. Это то, что я хочу, но методы работают ужасно медленно, когда количество элементов во входной последовательности превышает 1000, и становится все хуже, чем больше элементов содержит входная последовательность.

Второй метод использует список данных с дырками и создает исправленную последовательность, и он выполняется быстро. Однако это не то, что я хочу, поскольку это приводит к созданию экземпляра всего списка ввода в памяти.

Я хотел бы использовать метод (последовательность -> последовательность), а не метод (список -> последовательность), чтобы избежать одновременного хранения всего списка ввода в памяти.

Вопросы:

1) Почему первый метод такой медленный (становится все хуже с большими входными списками) (я подозреваю, что это связано с многократным созданием новых последовательностей с помощью Seq.skip 1, но я не уверен)

2) Как я могу быстро исправить дыры в данных, используя входную последовательность, а не входной список?

Код:

open System

// Method 1 (Slow)
let insertDummyValuesWhereASingleValueIsMissing1 (timeBetweenContiguousValues : TimeSpan) (values : seq<(DateTime * float)>) =
    let sizeOfHolesToPatch = timeBetweenContiguousValues.Add timeBetweenContiguousValues // Only insert dummy-values when the gap is twice the normal
    (None, values) |> Seq.unfold (fun (prevValue, restOfValues) ->  
        if restOfValues |> Seq.isEmpty then
            None // Reached the end of the input seq
        else
            let currentValue = Seq.hd restOfValues
            if prevValue.IsNone then
                Some(currentValue, (Some(currentValue), Seq.skip 1 restOfValues  )) // Only happens to the first item in the seq
            else
                let currentTime = fst currentValue
                let prevTime = fst prevValue.Value
                let timeDiffBetweenPrevAndCurrentValue = currentTime.Subtract(prevTime)
                if timeDiffBetweenPrevAndCurrentValue = sizeOfHolesToPatch then
                    let dummyValue = (prevTime.Add timeBetweenContiguousValues, 42.0) // 42 is chosen here for obvious reasons, making this comment superfluous
                    Some(dummyValue, (Some(dummyValue), restOfValues))
                else
                    Some(currentValue, (Some(currentValue), Seq.skip 1 restOfValues))) // Either the two values were contiguous, or the gap between them was too large to patch

// Method 2 (Fast)
let insertDummyValuesWhereASingleValueIsMissing2 (timeBetweenContiguousValues : TimeSpan) (values : (DateTime * float) list) =
    let sizeOfHolesToPatch = timeBetweenContiguousValues.Add timeBetweenContiguousValues // Only insert dummy-values when the gap is twice the normal
    (None, values) |> Seq.unfold (fun (prevValue, restOfValues) ->  
        match restOfValues with
        | [] -> None // Reached the end of the input list
        | currentValue::restOfValues -> 
            if prevValue.IsNone then
                Some(currentValue, (Some(currentValue), restOfValues  )) // Only happens to the first item in the list
            else
                let currentTime = fst currentValue
                let prevTime = fst prevValue.Value
                let timeDiffBetweenPrevAndCurrentValue = currentTime.Subtract(prevTime)
                if timeDiffBetweenPrevAndCurrentValue = sizeOfHolesToPatch then
                    let dummyValue = (prevTime.Add timeBetweenContiguousValues, 42.0) 
                    Some(dummyValue, (Some(dummyValue), currentValue::restOfValues))
                else
                    Some(currentValue, (Some(currentValue), restOfValues))) // Either the two values were contiguous, or the gap between them was too large to patch

// Test data
let numbers = {1.0..10000.0}
let contiguousTimeStamps = seq { for n in numbers -> DateTime.Now.AddMinutes(n)}

let dataWithOccationalHoles = Seq.zip contiguousTimeStamps numbers |> Seq.filter (fun (dateTime, num) -> num % 77.0 <> 0.0) // Has a gap in the data every 77 items

let timeBetweenContiguousValues = (new TimeSpan(0,1,0))

// The fast sequence-patching (method 2)
dataWithOccationalHoles |> List.of_seq |> insertDummyValuesWhereASingleValueIsMissing2 timeBetweenContiguousValues |> Seq.iter (fun pair -> printfn "%f %s" (snd pair) ((fst pair).ToString()))

// The SLOOOOOOW sequence-patching (method 1)
dataWithOccationalHoles |> insertDummyValuesWhereASingleValueIsMissing1 timeBetweenContiguousValues |> Seq.iter (fun pair -> printfn "%f %s" (snd pair) ((fst pair).ToString()))

person Treefrog    schedule 20.08.2009    source источник


Ответы (2)


Seq.skip создает новую последовательность. Я думаю, поэтому ваш оригинальный подход медленный.

Моя первая склонность - использовать выражение последовательности и Seq.pairwise. Это быстро и легко читать.

let insertDummyValuesWhereASingleValueIsMissingSeq (timeBetweenContiguousValues : TimeSpan) (values : seq<(DateTime * float)>) =
  let sizeOfHolesToPatch = timeBetweenContiguousValues.Add timeBetweenContiguousValues // Only insert dummy-values when the gap is twice the normal
  seq {
    yield Seq.hd values
    for ((prevTime, _), ((currentTime, _) as next)) in Seq.pairwise values do
      let timeDiffBetweenPrevAndCurrentValue = currentTime.Subtract(prevTime)
      if timeDiffBetweenPrevAndCurrentValue = sizeOfHolesToPatch then
        let dummyValue = (prevTime.Add timeBetweenContiguousValues, 42.0) // 42 is chosen here for obvious reasons, making this comment superfluous
        yield dummyValue
      yield next
  }
person Jason    schedule 20.08.2009
comment
+1: Когда я изучал F #, я попал в канавку функционального программирования, устранив все императивные конструкции. Я наблюдал, как читаемость моего кода резко упала с использованием Seq.unfold, а не бесконечно более простого подхода loop и ref. - person Juliet; 20.08.2009
comment
Джейсон, это решение, которое я искал. Моей первоначальной склонностью при написании метода было использовать yield (я работаю с C #), но, поскольку у меня нет книги F # (ожидая декабрьского выпуска Дона Сайма), я не мог понять, как F # использует yield, поэтому я пошел с Seq.unfold. - person Treefrog; 20.08.2009
comment
@Древесная лягушка. даже лучше, у f # есть yield!, который yield foreach, который я хотел бы добавить в C # - person ShuggyCoUk; 20.08.2009
comment
@ShuggyCoUk. Спасибо, что сообщил мне, Шагги, я изучу yield! - person Treefrog; 20.08.2009
comment
@Jason: Seq.skip создает новую последовательность - если посмотреть на реализацию (через Reflector), похоже, что это не так. Итератор, который возвращает Seq.skip, инкапсулирует переданный итератор, хотя несколько элементов были повторены. Копирование последовательности не происходит. - person Richard; 24.08.2009

Каждый раз, когда вы разбиваете последовательность, используя Seq.hd и Seq.skip 1, вы почти наверняка попадаете в ловушку перехода на O (N ^ 2). IEnumerable<T> - ужасный тип для рекурсивных алгоритмов (включая, например, Seq.unfold), поскольку эти алгоритмы почти всегда имеют структуру «первого элемента» и «остатка элементов», и нет эффективного способа создать новый IEnumerable, который представляет собой «остаток». элементов ». (IEnumerator<T> работает, но с его моделью программирования API не так весело / легко работать.)

Если вам нужны исходные данные, чтобы «оставаться ленивым», вам следует использовать LazyList (в F # PowerPack). Если вам не нужна лень, тогда вам следует использовать конкретный тип данных, например, «список», в который вы можете «уйти» в O (1).

(Вы также должны проверить Как избежать переполнения стека (с бесконечными последовательностями F # последовательностей) как к вашему сведению, хотя это применимо лишь косвенно к этой проблеме.)

person Brian    schedule 20.08.2009
comment
Брайан, правильно ли я понимаю, что процесс создания новой последовательности из существующей (например, let seq2 = Seq.skip 1 seq1) - дорогостоящая операция? (Я бы предположил, что это O (1)) Если это дорого, то почему? (У меня создалось впечатление, что последовательности оцениваются только по мере необходимости?) - person Treefrog; 20.08.2009
comment
Что ж, создание на самом деле быстрое: O (1). Но затем оценка его первого элемента означает создание перечислителя для исходной последовательности, вычисление его первого значения, его отбрасывание, вычисление следующего значения и затем получение его. Таким образом, два Seq.skip 1 дают последовательность, которая при оценке будет: создать перечислитель над внутренним, который создает перечислитель над исходным, вычисляет первое значение, отбрасывает, возвращает следующее значение к внутреннему, которое затем отбрасывает внутреннее, вычисляет еще одно значение, и дает это. Каждый вложенный Seq.skip 1 добавляет еще больше работы, в результате чего требуется O (N) времени и пространства. - person Brian; 20.08.2009
comment
Спасибо, что нашли время ответить, Брайан! - person Treefrog; 20.08.2009
comment
Это объясняет, почему F # предоставляет функцию Seq.head, но не Seq.tail. - person Stephen Hosking; 01.10.2010