Самая быстрая структура данных неизменяемого списка для большого количества конкатенаций и одной итерации - PullRequest
9 голосов
/ 13 декабря 2011

Я работаю с Haskell.Стандартное объединение списков наивно и медленно.Моя ситуация такова: у меня есть алгоритм, который создает конкатенацию одного списка (порядок не имеет значения, поэтому его можно либо добавить, либо добавить, либо комбинировать) много раз, а затем возвращает его.Результат будет использован только один раз.Высокая производительность очень важна.

Итак, это довольно простая ситуация.Я слышал о списках различий и что это помогает в этой ситуации.Но разве это лучший вариант?

Списки могут вырасти до больших: 100 000 записей.

Ответы [ 5 ]

15 голосов
/ 14 декабря 2011

Это эмпирический вопрос, и на него следует ответить эмпирически.Разумные альтернативы включают

  • Стандартный список с минусами (в вашем вопросе называется prepend)

  • Список различий (список Джона Хьюза) с константойдобавление по времени

  • Алгебраический тип данных, поддерживающий добавление с постоянным временем:

    data Alist a = ANil | ASingle a | AAppend (Alist a) (Alist a)
    
  • Список списков с окончательным concat.

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

И, пожалуйста, сообщите нам результаты.

12 голосов
/ 13 декабря 2011

Если порядок не имеет значения, просто используйте обычный список. Предшествующий (consing) - это O (1), а весь список - O (n), который хорош для операций, которые вас интересуют.

Список различий полезен, если вы на самом деле заботитесь о добавлении, а не о добавлении, поскольку при обычном добавлении добавление выполняется быстро, добавление равно O (n). Списки различий позволяют добавлять O (1). Помимо простоты добавления, список различий в каждом случае медленнее или медленнее, чем обычный список.

6 голосов
/ 13 декабря 2011

Если вы можете добавлять элементы один за другим, простой список в порядке.

Если вы можете добавлять только чанки, тогда лучше использовать список списков, потому что добавление нового чанка становится O (1) вместо O (N), где N - размер чанка.

Два фактора помогают спискам быть быстрыми:

  • Лень
  • Список фьюжн

Оба будут работать только в том случае, если вы создадите список списков хорошего производителя и используете его только одним хорошим потребителем. Таким образом, если ваш производитель и потребитель хороши , и вы используете список однопоточным способом, то GHC будет генерировать просто циклы и никаких промежуточных списков вообще из-за объединения списков. Существуют две разные реализации объединения списков: так называемые build / foldr и потоковое объединение. Смотри также http://www.haskell.org/haskellwiki/Correctness_of_short_cut_fusion

Если производитель и потребитель хороши, но объединение списков не включает (потому что вы не использовали флаги оптимизации, потому что GHC не поддерживает определенную оптимизацию объединения, или если вы используете компилятор, отличный от GHC без поддержки объединения), вы будете по-прежнему получить разумную производительность из-за лени. В этом случае промежуточные списки будут созданы, но немедленно собраны сборщиком мусора.

4 голосов
/ 14 декабря 2011

Если под добавлением вы подразумеваете «добавить один элемент в конец списка», и вы реализуете это с помощью xs ++ [x], то да, это ужасно медленно для огромных списков, потому что каждый ++ равен O (n), что делаетвсего O (n ^ 2).

В этом случае вы можете ускорить это, просто используя cons, чтобы добавить элемент в начало списка вместо конца.Это делает весь процесс построения списка O (n).Затем вы можете использовать reverse, чтобы изменить его, что также является O (n), но вы должны сделать это только один раз, так что вы все равно O (n).

Если ваша обработка либо не является 'В зависимости от порядка или может быть сделано в обратном порядке с небольшими изменениями, вы можете в любом случае исключить reverse.И в этом случае вы также можете использовать лень для создания элементов только по мере их обработки, что означает, что вам не нужен весь список в памяти, что потенциально может также немного ускорить ваш код в зависимости от поведения памяти вашего кода;если каждый элемент списка помещается в кэш-память ЦП, вы можете получить большую скорость таким образом.

Если вы добавляете, вы имеете в виду «объединить список в конец другого списка», вы можете сделать то же самое, используякакая-то операция «обратного препендинга», когда вы помещаете элементы из нового списка в начало списка целей по одному элементу за раз;это дает вам конкатенацию списков, которая является линейной по размеру каждого нового списка, а не списка, который вы строите, так что в общем количестве обрабатываемых элементов она составляет O (n), а не O (n ^ 2).

В качестве альтернативы вы можете создать список списков в обратном порядке, используя cons, а затем обработать его с помощью некоторой операции обратного выравнивания, которая также должна быть O (n).

Это все ещеТруднее понять, как полностью избежать реверсирования в этом случае (многоэлементное добавление), если только ваша окончательная обработка полностью не зависит от порядка.

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

2 голосов
/ 13 декабря 2011

Рассмотрим список списков, если сегменты имеют разную длину.И concat.Лень должна справиться с этим.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...