Mathematica "связанные списки" и производительность

В Mathematica я создаю похожие списки:

toLinkedList[x_List] := Fold[pair[#2, #1] &, pair[], Reverse[x]];

fromLinkedList[ll_pair] := List @@ Flatten[ll];

emptyQ[pair[]] := True;
emptyQ[_pair] := False;    

Использование символа pair для ячеек cons имеет то преимущество, что Flatten работает безопасно, даже если списки содержат стиль Mathematica List s и позволяет вам определять пользовательские нотации с помощью MakeExpression/MakeBoxes, что делает все гораздо приятнее. Чтобы избежать необходимости обманывать $IterationLimit, я написал функции для работы с этими списками, используя либо циклы While, либо NestWhile вместо использования рекурсии. Естественно, я хотел посмотреть, какой подход будет быстрее, поэтому я написал двух кандидатов, чтобы я мог наблюдать за их борьбой:

nestLength[ll_pair] := 
 With[{step = {#[[1, -1]], #[[-1]] + 1} &},
  [email protected][step, {ll, 0}, ! [email protected]@# &]];

whileLength[ll_pair] := 
 Module[{result = 0, current = ll},
  While[! [email protected],
   current = current[[2]];
   ++result];
  result];

Результаты были очень странными. Я тестировал функции на связанных списках длиной 10000, а whileLength был обычно примерно на 50% быстрее, примерно в 0,035 секунды до nestLength 0,055 секунд. Однако время от времени whileLength заняло около 4 секунд. Я думал, что может быть какое-то поведение кэширования, поэтому я начал генерировать свежие случайные списки для проверки, а whileLength не обязательно будет медленным при первом запуске с новым списком; это может занять десятки раз, чтобы увидеть замедление, но тогда оно не будет повторяться (по крайней мере, не для 200 запусков, которые я пытался с каждым списком).

Что может быть?

Для справки, функция, которую я использовал для тестирования, такова:

getTimes[f_, n_] :=
 With[{ll = [email protected][100, 10000]},
  Table[Timing[[email protected]], {n}][[All, 1]]]

EDIT: Я забыл упомянуть версию раньше; Я получил эти результаты с помощью Mathematica 8.

ИЗМЕНИТЬ второе: Когда я прочитал ответ Даниэля Лихтблау, я понял, что мои времена для "типичных" прогонов опущены ведущим 0. Это было исправлено.

РЕДАКТИРОВАТЬ третий: Я думаю, Леонид Шифрин правилен, чтобы связать проблему с Module; Я могу получить такое же поведение из версии на основе NestWhile, заменив With на Module:

nestModuleLength[ll_pair] := 
  Module[{step = {#[[1, -1]], #[[-1]] + 1} &}, 
   [email protected][step, {ll, 0}, ! [email protected]@# &]];

In[15]:= Select[getTimes[nestModuleLength, 100], # > 3 &]
Out[15]= {3.797}

Ответ 1

Приведенные ниже примеры дают типичные результаты.

Один медленный пример в пробеге длиной 20.

In[18]:= getTimes[whileLength, 20]

Out[18]= {0.031, 0.032, 0.031, 0.031, 0.031, 0.032, 0.031, 0.031, \
0.031, 0.047, 0.032, 0.031, 0.031, 3.547, 0.047, 0.031, 0.031, 0.032, \
0.031, 0.031}

Я отмечаю попутно, что тайминги ~ 10 раз быстрее, чем в исходной записи, за исключением медленных случаев, которые сопоставимы. Не уверен, что объясняет эту разницу в коэффициентах.

Нет медленных примеров.

In[17]:= getTimes[nestLength, 20]

Out[17]= {0.047, 0.047, 0.062, 0.047, 0.047, 0.062, 0.047, 0.047, \
0.047, 0.063, 0.046, 0.047, 0.047, 0.063, 0.047, 0.046, 0.047, 0.063, \
0.047, 0.047}

Один медленный пример в пробеге 100.

In[19]:= getTimes[whileLength, 100]

Out[19]= {0.031, 0.031, 0.031, 0.032, 0.031, 3.594, 0.047, 0.031, \
0.031, 0.031, 0.032, 0.031, 0.031, 0.031, 0.032, 0.031, 0.047, 0.031, \
0.031, 0.031, 0.032, 0.031, 0.031, 0.031, 0.032, 0.047, 0.031, 0.031, \
0.031, 0.032, 0.031, 0.031, 0.031, 0.032, 0.031, 0.031, 0.047, 0.031, \
0.031, 0.032, 0.031, 0.031, 0.031, 0.032, 0.031, 0.031, 0.047, 0.031, \
0.032, 0.031, 0.031, 0.031, 0.032, 0.031, 0.031, 0.047, 0.031, 0.031, \
0.032, 0.031, 0.031, 0.031, 0.032, 0.031, 0.047, 0.031, 0.031, 0.032, \
0.031, 0.031, 0.031, 0.032, 0.031, 0.031, 0.031, 0.032, 0.046, 0.032, \
0.031, 0.031, 0.031, 0.032, 0.031, 0.031, 0.047, 0.031, 0.032, 0.031, \
0.031, 0.031, 0.032, 0.031, 0.047, 0.031, 0.031, 0.031, 0.032, 0.031, \
0.031, 0.031}

Mathematica реализует, несовершенно, так называемую "бесконечную оценку". То есть выражение переоценивается до тех пор, пока оно не перестанет меняться. Чтобы сделать это достаточно быстро, существуют различные оптимизации, которые пытаются по возможности коротко закоротить процесс.

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

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

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

Как заметил Леонид Шифрин, символы с атрибутом HoldAllComplete невосприимчивы к этой проблеме. Поэтому использование этого атрибута может быть полезным для этого типа кода.

Даниэль Лихтблау Wolfram Research

Ответ 2

Отказ от ответственности: следующее - спекуляция. Это похоже на поиск UpValues. Похоже, что это было оптимизировано для глобальных переменных (так что система пропускает этот шаг, когда может определить, что он может это сделать), но не для Module - генерируемых локальных переменных. Чтобы проверить это, присвойте HoldAllComplete атрибуту pair, и эффект исчезнет (с тех пор UpValues не проверяется на current):

SetAttributes[pair, HoldAllComplete];

In[17]:= ll = [email protected][100, 10000];
Max[Table[Timing[whileLength[ll]], {1000}][[All, 1]]]

Out[18]= 0.047

НТН

Ответ 3

Кажется, он связан с управлением памятью локальных символов модуля.

Я покажу серию времен от ряда прогонов. Каждый пробег дает, конечно, уникальный сюжет, но я проверил "последовательность" среди прогонов. Посмотрите:

whileLength[l2_pair] := 
  Module[{result = 0}, current = l2; 
   While[! [email protected], current = current[[2]];
    ++result];
   result];  

дает следующий ряд временных рядов:

введите описание изображения здесь

При использовании только глобальных символов:

whileLength[l2_pair] := 
  Module[{}, result = 0; current = l2; 
   While[! [email protected], current = current[[2]];
    ++result];
   result];

дает:

введите описание изображения здесь