Реализует ли erlang копирование и изменение записей каким-либо умным способом?

Дано:

-record(foo, {a, b, c}).

Я делаю что-то вроде этого:

Thing = #foo{a={1,2}, b={3,4}, c={5,6}},
Thing1 = Thing#foo{a={7,8}}.

С семантической точки зрения Thing и Thing1 являются уникальными объектами. Однако с точки зрения языковой реализации создание полной копии Thing для генерации Thing1 было бы крайне расточительным. Например, если запись была размером в мегабайт, а я сделал тысячу "копий", каждый из которых изменял пару байтов, я просто сжег гигабайт. Если внутренняя структура отслеживала представление родительской структуры, и каждая производная размечала этого родителя таким образом, который указывал на его собственное изменение, но сохранял версии всех остальных, производные могли быть созданы с минимальными накладными расходами памяти.

Мой вопрос таков: эрланг делает что-то умное - внутренне - чтобы сохранить накладные расходы на обычную писанину эрланга;

Thing = #ridiculously_large_record,
Thing1 = make_modified_copy(Thing),
Thing2 = make_modified_copy(Thing1),
Thing3 = make_modified_copy(Thing2),
Thing4 = make_modified_copy(Thing3),
Thing5 = make_modified_copy(Thing4)

... до минимума?

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

4 ответа

Решение

Точная работа сборки мусора и выделения памяти известна лишь немногим. К счастью, они очень рады поделиться своими знаниями, и следующее основано на том, что я узнал из списка рассылки erlang-questions и обсуждения с разработчиками OTP.

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

При выполнении кода в одном процессе обновляются только части. Давайте проанализируем кортежи, так как это пример, который вы предоставили.

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

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

Как всегда, Erlang дает вам несколько инструментов, чтобы точно понять, что происходит. Руководство по эффективности подробно, как использовать erts_debug:size/1 а также erts_debug:flat_size/1 понять размер структуры данных при внутреннем использовании в процессе и при копировании. Инструменты трассировки также позволяют понять, когда, что и сколько было собрано мусора.

Запись foo имеет четвёртую строчку (содержит четыре слова), но вся структура имеет размер 14 слов. Любые немедленные (pids, порты, маленькие целые числа, атомы, catch и nil) могут быть сохранены непосредственно в массиве кортежей. Любой другой термин, который не может вписаться в слово, например другие кортежи, не хранится напрямую, а на него ссылаются указатели в штучной упаковке (коробочный указатель - это термин erlang с адресом переадресации к реальному элементу... только внутренним компонентам).

В вашем случае новый кортеж той же арности создан и атом foo и все указатели копируются из предыдущего кортежа, за исключением индекса два, a, который указывает на новый кортеж {7,8} который составляет 3 слова. Во всех 5 + 3 новые слова создаются в куче, и только 3 слова копируются из старого кортежа, остальные 9 слов не затрагиваются.

Чрезмерно большие кортежи не рекомендуются. При обновлении кортежа весь кортеж, т. Е. Массив, а не глубокий контент, необходимо скопировать, а затем обновить другим, чтобы сохранить постоянную структуру данных. Это также приведет к увеличению объема мусора, заставляя сборщик мусора нагреваться, что также снижает производительность. dict а также array По этой причине модули избегают использовать большие кортежи и вместо этого имеют мелкое дерево кортежей.

Я могу определенно проверить, что люди уже указали:

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

Это работает только потому, что у нас есть неизменные данные. Так что в вашем примере каждый раз, когда вы обновляете значение в #foo запись ни одна из данных в элементах не копируется, и создается только новый 4-элементный кортеж (5 слов). Эрланг никогда не будет делать глубокое копирование в операциях этого типа или при передаче аргументов в вызовах функций.

В заключение:

Thing = #foo{a={1,2}, b={3,4}, c={5,6}},
Thing1 = Thing#foo{a={7,8}}.

Вот если Thing не используется снова, вероятно, он будет обновлен на месте, а копирование кортежа будет исключено, как говорится в Руководстве по эффективности. (Я думаю, что синтаксис кортежей и записей соответствует чему-то вроде setelement)

Thing = #ridiculously_large_record,
Thing1 = make_modified_copy(Thing),
Thing2 = make_modified_copy(Thing1),
...

Здесь кортежи фактически копируются каждый раз.

Я думаю, что это было бы теоретически возможно сделать интересную оптимизацию для этого. Если компилятор может выполнить escape-анализ возвращаемого значения make_modified_copy и обнаружить, что единственная ссылка на него - возвращенная, может сохранить эту информацию о функции. Когда он встречает вызов той функции, он знает, что безопасно изменить возвращаемое значение на месте.

Это возможно сделать только при межмодульных вызовах из-за возможности замены кода.

Может быть, однажды у нас будет это.

Другие вопросы по тегам