Реализует ли 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
и обнаружить, что единственная ссылка на него - возвращенная, может сохранить эту информацию о функции. Когда он встречает вызов той функции, он знает, что безопасно изменить возвращаемое значение на месте.
Это возможно сделать только при межмодульных вызовах из-за возможности замены кода.
Может быть, однажды у нас будет это.