Проблема с реализацией структуры данных веревки в C ++

Я пытаюсь создать структуру данных rope. Это тип двоичного дерева, то есть рекурсивной структуры данных.

Назначение веревки состоит в том, чтобы разделение и конкатенация происходило быстро, что означает, что вы избегаете копирования целых цепочек.
Так, например, пользователь должен иметь возможность произнесите rope1 + rope2 и ожидайте результата в ~ логарифмическом времени.

Однако это создает проблему:
Если веревка модифицируется, ее родительские элементы также изменяются косвенно.
Поскольку моя цель - сделать rope заменой для string, это неприемлемо.

Мое решение: всякий раз, когда происходит какое-либо изменение в rope, я бы создавал новый узел с небольшим изменением, оставляя старые неизмененными.

В теории, я думаю, это сработает неплохо.

Однако на практике это включает выделение кучи для (почти?) Каждой модификации строк.
Даже изменение одного символа приведет к созданию нового объекта кучи, что не только медленно сам по себе, но это также значительно снижает локальность памяти, что очень негативно сказывается на производительности.

Как мне подойти к решению этой проблемы?


person user541686    schedule 05.09.2012    source источник
comment
Итак, вы хотите, чтобы разделение и конкатенация были быстрыми, а все остальное - быстрым? Я думаю, это тот случай, когда вы не можете съесть свой торт и съесть его.   -  person R. Martinho Fernandes    schedule 05.09.2012
comment
Если у вас есть GCC, проверьте заголовок <ext/rope> для примера реализации.   -  person Kerrek SB    schedule 05.09.2012
comment
Поскольку моя цель - сделать веревку заменой для веревки, Вы не можете сделать rope заменой для string. По определению, это две разные структуры данных с двумя разными наборами операций. string в C ++ 11 должен быть непрерывным, что не является вашей цепочкой.   -  person Nicol Bolas    schedule 05.09.2012
comment
Вам нужно только выделить / скопировать кучу, если на нее также ссылается другая строка. Вы быстро окажетесь в ситуации, когда все эти копии появятся в конце концов, чуть позже и с дополнительными накладными расходами.   -  person Mooing Duck    schedule 05.09.2012
comment
@ R.MartinhoFernandes: Да, я этого боялся ...   -  person user541686    schedule 05.09.2012
comment
@NicolBolas: Ну, я имею в виду, если не брать адрес. :) Я не имел в виду, что он будет на 100% совместим, просто его можно использовать в целом.   -  person user541686    schedule 05.09.2012
comment
@MooingDuck: Хм ... так ты предлагаешь мне сосчитать ссылки?   -  person user541686    schedule 05.09.2012
comment
@Mehrdad: Вы действительно планировали делать копирование при записи без подсчета ссылок или GC? Я даже не уверен, возможно ли это. В любом случае, я хотел предложить rope1 + rope2 сделать глубокую копию. Но теперь, когда я думаю об этом подробнее, подсчет ссылок / COW может быть конкурентоспособным с точки зрения скорости. Я не уверен.   -  person Mooing Duck    schedule 06.09.2012
comment
@MooingDuck: Ну, я жонглировал несколькими мыслями, и одним из них было отсутствие подсчета ссылок - я не был уверен, поэтому и спросил. Я предполагаю, что скорость будет довольно конкурентоспособной, судя по характеру веревки - она ​​может быть немного медленнее, чем веревка, но не намного, поэтому я думаю, что, вероятно, пойду по этому маршруту. (Другая альтернатива, на мой взгляд, заключалась в том, что пользователь класса мог использовать счетчик ссылок вместо самого класса, но это, похоже, не сработало.)   -  person user541686    schedule 06.09.2012


Ответы (2)


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

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

Практическая проблема с этим - полезно отделить мутацию от операций без мутации:

    char& rope::operator[] (std::string::pos)

может изменить указанный char, но нет тривиального способа принудительно выбрать перегрузку const, когда это фактически не будет. Это означает, что вы должны либо предположить, что произойдет мутация и, возможно, вызвать ненужную копию, либо вместо этого вернуть какой-то прокси, который перегружает преобразование и присваивание символов.

Этот подход был опробован для ранних реализаций std::string (где строка эквивалентна одноузловой веревке) iirc, и он потерял популярность; частично из-за проблемы мутации, а частично из-за того, что COW и требуемый реф-счет становятся все более дорогостоящими, если вам приходится беспокоиться о многопоточности.


Как вы говорите, у веревки возникает дополнительная проблема, если ваш узел содержит два независимых типа состояния: свою собственную строку и ссылки на его дочерние узлы (поскольку это приводит к тому, что мутации ссылки refcount / child будут распространяться вверх по дереву).

Если вместо этого символы хранятся только в конечных узлах, и вы делаете полную копию нелистовых узлов (так что каждая веревка имеет свою собственную «структуру каталогов»), вы все равно избегаете копирования символов, а пересчитанное общее состояние намного проще.

Получится ли это конкатенация логарифмического времени, которую вы хотите? Возможно нет:

  • вам нужно скопировать все узлы, не являющиеся листами (и добавить новый корень), и их количество - это количество листьев
  • вы также должны увеличивать количество ссылок на листах, что является линейным

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

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

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

person Useless    schedule 05.09.2012
comment
+1 за рефсчет, кажется разумным. (Все еще читаю остальное - пока все выглядит отлично, спасибо за информацию.) - person user541686; 05.09.2012

Однако на практике это включает выделение кучи (почти?) Для каждой модификации строк.

Если вы хотите избежать частых проблем с производительностью выделения кучи, я бы предложил использовать пул памяти для вашего класса, который выделяет фрагмент памяти и должен запрашивать новое выделение из ОС только при его заполнении, что должно происходить довольно редко. . Затем вы можете оптимизировать доступ к пулу памяти для выделения типов данных малых блоков, таких как char и т. Д.

У Андрея Александреску есть отличный пример распределителя памяти малых блоков в своей книге «Современный дизайн C ++».

person Jason    schedule 05.09.2012