Сложность Concat () - PullRequest
       19

Сложность Concat ()

1 голос
/ 21 ноября 2010

В Haskell, при многократном использовании : для создания последовательности, перебор результата занимает время O ( N ).

1:2:3:4:[]

Если я пытаюсь сделать что-то подобноев LINQ я могу использовать Concat(), но для итерации по результату требуется время O ( N ²), потому что когда я вызываю MoveNext() в полученном перечислителе для n -говремя, это должно пройти через n слоев Concat() с.

new[] { 1 }.Concat(new[] { 2 }).Concat(new[] { 3 }).Concat(new[] { 4 })

или

new[] { 1 }.Concat(new[] { 2 }.Concat(new[] { 3 }.Concat(new[] { 4 })))

Как я могу сделать это так, чтобы это было быстро(то есть линейный) и функционально «чистый» (т.е. без использования List<T>)?Может быть, написав IConcatenable<T> с собственной перегрузкой Concat()?Или я не прав, что использование Concat() таким образом является квадратичным?

Ответы [ 2 ]

3 голосов
/ 21 ноября 2010

Операция LINQ Concat() ни в коей мере не эквивалентна функциональной операции cons.Они очень разные.Для каждой операции «против» создается «новый» список.Это на самом деле быстро, так как структуры данных в игре разработаны специально для этого использования.Для каждой операции Concat создается новый итератор, который не предназначен для такого использования.Чтобы проиллюстрировать, что делается каждый раз, рассмотрим следующий более короткий пример:

1:2:3:[]

Функциональная операция состоит из нескольких шагов для оценки.3:[] приводит к временному списку [3], затем 2:[3] приводит к спискам [2,3] и 1:[2,3].Это 3 простых шагов только для создания списка.Чтобы «перебрать» его, потребуется некоторая рекурсия и сопоставление (или любой другой эквивалент Хаскелла).Это займет где-то около 3 или 4 дополнительных сложных шагов (по одному для каждого сегмента списка).

new[] { 1 }.Concat(new[] { 2 }).Concat(new[] { 3 });

Операция LINQ также состоит из нескольких шагов для оценки.new[] { 1 }.Concat(new[] { 2 }) выдает новый итератор, повторяющий последовательность [[1]+[2]], а последний выдает новый итератор, повторяющий последовательность [[[1]+[2]]+[3]].Для создания итератора потребовалось два простых шага, но вы должны заметить, насколько сложна последовательность на самом деле.Здесь используются 5 итераторов, по одному для каждого массива (3) и по одному для каждой объединенной пары (2).Я не буду проходить все этапы итерации, но при этом выполняется гораздо больше вызовов функций и доступа к свойствам (как того требует каждый итератор).

new[] { 1 }.Concat(new[] { 2 }.Concat(new[] { 3 }))

Операция LINQ, которая выглядит более структурно эквивалентной, снова принимаеттакое же количество шагов для создания, получая [[1]+[[2]+[3]]] с тем же количеством итераторов и столь же сложными итерациями, хотя это и так.Ну, это не потому, что так делается на функциональных языках, и они оптимизированы для этого использования.У них есть преимущество использования неизменяемых последовательностей, которые могут быть повторно использованы в других составных списках.Генерация сложных последовательностей с использованием LINQ таким образом не совсем то, для чего она была разработана.По языку нет оптимизаций (некоторые по JIT, но это все), и это явно не то, как вы хотите перебирать последовательность.ИМХО прямо в голову, потому что это сложно.


Я думаю, что лучший подход в попытке повысить производительность - создать связанные списки для представления объединенной последовательности.Вы можете использовать класс LinkedList<T>, создать свои собственные структуры данных, напоминающие функциональный список, или, что еще лучше, использовать F # FSharpList<T>.Затем добавьте методы расширения и другие вспомогательные классы.

1 голос
/ 21 ноября 2010

Вы можете попробовать .AsParallel, но я сомневаюсь, что распараллеливание того стоит, если только ваши последовательности не очень большие, также работают только с индексируемыми данными

...