Является ли git реализация алгоритма ограничения терпения корректным?

qaru.site/info/34621/..., казалось, был хорошим кандидатом для применения алгоритма ограничения терпения. Однако, проверяя мой потенциальный ответ, я обнаружил, что git diff --patience не оправдывает моих ожиданий (и в этом случае ничем не отличается от стандартного diff алгоритм):

$ cat a
/**
 * Function foo description.
 */
function foo() {}

/**
 * Function bar description.
 */
function bar() {}

$ cat b
/**
 * Function bar description.
 */
function bar() {}

$ git diff --no-index --patience a b
diff --git a/a b/b
index 3064e15..a93bad0 100644
--- a/a
+++ b/b
@@ -1,9 +1,4 @@
 /**
- * Function foo description.
- */
-function foo() {}
-
-/**
  * Function bar description.
  */
 function bar() {}

Я бы ожидал, что diff будет:

diff --git a/a b/b
index 3064e15..a93bad0 100644
--- a/a
+++ b/b
@@ -1,8 +1,3 @@
-/**
- * Function foo description.
- */
-function foo() {}
-
 /**
  * Function bar description.
  */

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

Ответ 1

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

Алгоритмы LCS (самая длинная общая подпоследовательность) сводятся к сокращению времени, затрачиваемого на поиск минимального разрешения на дистанционное редактирование. Стандартное (динамическое программирование) решение - O (MN), где M - количество символов в исходной строке, а N - количество символов в целевой строке. В нашем случае "символы" - это строки, а "строка" - это набор строк, а не строки с символами (где символы будут, например, кодами ASCII). Мы просто заполняем матрицу M x N "затрат на редактирование"; когда мы закончили, мы создаем фактическое редактирование, отслеживая минимальный путь назад через результирующую матрицу. См. https://jlordiales.me/2014/03/01/dynamic -programming-edit-distance/ для примера. (Веб-страница, найденная с помощью поиска Google: это не то, с чем я имел какое-либо отношение, кроме сканирования на высокой скорости для правильности сейчас. Кажется правильным.:-))

Фактически вычисление этой матрицы довольно дорого для больших файлов, так как M и N - это количество строк исходного кода (обычно приблизительно равно): результаты файла строки размером ~ 4k в ~ 16M записей в матрице, которые должны быть заполнены полностью, прежде чем мы сможем проследить минимальный путь назад. Более того, сравнение "символов" уже не так тривиально, как сравнение символов, поскольку каждый "символ" является полной линией. (Обычная трюк заключается в хэш-строке каждой строки и вместо хэширования вместо генерации матрицы, а затем повторно проверять во время трассировки, заменяя "сохранить неизменный символ" на "удалить оригинал и вставить новый", если хэш ввел нас в заблуждение. в присутствии хеш-коллизий: мы можем получить очень немного субоптимальную последовательность редактирования, но она практически никогда не будет ужасной.)

LCS изменяет матричный расчет, наблюдая, что сохранение длинных общих подпоследовательностей ( "сохранить все эти строки" ) почти всегда приводит к большой победе. Найдя несколько хороших LCS-es, мы разбиваем проблему на "редактирование не общего префикса, сохранение общей последовательности и редактирование не общего суффикса": теперь мы вычисляем две матрицы динамического программирования, но для небольших проблем, поэтому он идет быстрее. (И, конечно же, мы можем рефинансировать префикс и суффикс. Если бы у нас был файл размером ~ 4k, и мы обнаружили, что ~ 2k полностью без изменений, в общих строках около середины, оставляя строки ~ 0.5k вверху и ~ 1.5k внизу, мы можем проверить, что длинные общие подпоследовательности в вершинах размером ~ 0.5k "имеют разницу", а затем в ~ 1.5k "дно имеет разницу".)

LCS плохо работает и, следовательно, приводит к ужасным различиям, когда "общие подпоследовательности" представляют собой тривиальные строки, такие как    }, которые имеют множество совпадений, но не очень важно. Вариант терпения diff просто отбрасывает эти строки из начального вычисления LCS, так что они не являются частью "общей подпоследовательности". Это делает оставшиеся матрицы большими, поэтому вы должны быть терпеливыми.: -)

В результате терпение diff не помогает здесь, потому что наша проблема не имеет ничего общего с общими подпоследовательностями. На самом деле, даже если мы полностью отбросили LCS и просто сделали одну большую матрицу, мы все равно получили бы нежелательный результат. Наша проблема заключается в том, что стоимость удаления:

  - * Функция foo description.
- */
-функция foo() {}
-
-/**
Код>

(и ничего не вставлять) является тот же как стоимость удаления:

<Предварительно > <код > -/** - * Описание функции foo. - */ -функция foo() {} - Код >

Стоимость одного из них - это просто "удалить 5 символов". Даже если мы взвешиваем каждый символ - делаем непустые строки "более дорогими" для удаления, чем пустые строки - стоимость остается той же: мы удаляем пять строк в конце.

Вместо этого нам нужен способ взвешивания строк на основе "визуальной кластеризации": короткие строки на краю дешевле удалить, чем короткие строки в середине, Эвристика уплотнения добавила к Git 2.9 попытка сделать это после факта. Это, по-видимому, по крайней мере немного ошибочно (только пустые строки подсчитываются, и они должны фактически существовать, а не просто подразумеваться путем достижения края). Возможно, было бы лучше сделать взвешивание во время заполнения матрицы (если предположить, что осталось после устранения LCS, действительно идет полная матрица динамического программирования). Однако это нетривиально.

Ответ 2

Я нашел новую запись Брэма Коэна, описание алгоритма терпения diff, который поддерживает наблюдаемый вывод git diff:

... как работает Patience Diff -

  • Сопоставьте первые строки обоих, если они идентичны, затем сопоставьте второй, третий и т.д., пока пара не будет соответствовать.
  • Сопоставьте последние строки обоих, если они идентичны, а затем совпадают с последними, вторыми и последними и т.д., пока пара не будет соответствовать.
  • Найти все строки, которые происходят ровно один раз с обеих сторон, затем выполнить самую длинную общую подпоследовательность на этих линиях, сопоставляя их.
  • Сделайте шаги 1-2 на каждом разделе между согласованными строками

Таким образом, акцент алгоритма на уникальные строки подрывается шагами 1 и 2., которые обнаруживают общий префикс и суффикс, даже если они сделаны из шумных строк.

Такая формулировка немного отличается от того, что я видел до этого, и Брэм признает, что он немного изменил ее:

Я ранее описывал его с порядком немного другого...

Мой вопрос фактически повторил озабоченность, выраженную в этот комментарий в сообщение Bram.