Поздно пришёл к этому Q & A с уже отличными ответами, но я хотел вмешаться, поскольку иностранец привык смотреть на вещи с низкоуровневой точки зрения битов и байтов в памяти.
Я очень взволнован неизменными проектами, даже с точки зрения C, и с точки зрения поиска новых способов эффективного программирования этого ужасного оборудования, которое у нас есть в наши дни.
Медленнее / Faster
Что касается вопроса о том, замедляет ли это дело, то роботизированный ответ будет yes
. На этом весьма технически концептуальном уровне неизменность может только замедлять процесс. Аппаратное обеспечение работает лучше, когда оно не время от времени выделяет память и может просто изменить существующую память (почему у нас есть такие понятия, как временная локальность).
Тем не менее, практический ответ maybe
. Производительность по-прежнему в значительной степени является метрикой производительности в любой нетривиальной кодовой базе. Как правило, мы не находим ужасающие в обслуживании кодовые базы, срабатывающие в условиях гонки, как наиболее эффективные, даже если мы игнорируем ошибки. Эффективность часто является функцией элегантности и простоты. Пик микрооптимизаций может несколько конфликтовать, но они обычно зарезервированы для самых маленьких и наиболее важных участков кода.
Преобразование неизменяемых битов и байтов
Исходя из низкоуровневой точки зрения, если мы используем такие рентгеновские концепции, как objects
и strings
т. Д., То в основе этого лежат биты и байты в различных формах памяти с различными характеристиками скорости / размера (скорость и размер аппаратного обеспечения памяти обычно составляют взаимоисключающий).
Иерархии памяти компьютера нравится, когда мы неоднократно обращаемся к одному и тому же фрагменту памяти, как показано на приведенной выше диаграмме, поскольку он будет сохранять этот часто доступный фрагмент памяти в самой быстрой форме памяти (кэш L1, например, который почти так же быстро, как регистр). Мы можем многократно обращаться к одной и той же памяти (многократно использовать ее многократно) или многократно обращаться к различным разделам фрагмента (например: циклически проходить по элементам в смежном фрагменте, который многократно обращается к различным разделам этого фрагмента памяти).
Мы заканчиваем тем, что бросаем гаечный ключ в этом процессе, если модификация этой памяти заканчивается желанием создать целый новый блок памяти на стороне, например так:
... в этом случае доступ к новому блоку памяти может потребовать принудительных сбоев страниц и пропусков кэша, чтобы переместить его обратно в самые быстрые формы памяти (вплоть до регистра). Это может быть настоящим убийцей производительности.
Есть способы смягчить это, однако, используя резервный пул предварительно выделенной памяти, уже затронутой.
Большие агрегаты
Другая концептуальная проблема, которая возникает из представления более высокого уровня, - это просто создание ненужных копий действительно больших агрегатов оптом.
Чтобы избежать слишком сложной диаграммы, давайте представим, что этот простой блок памяти был каким-то дорогим (возможно, символы UTF-32 на невероятно ограниченном оборудовании).
В этом случае, если мы захотим заменить «HELP» на «KILL» и этот блок памяти будет неизменным, нам придется создать целый новый блок целиком, чтобы создать уникальный новый объект, даже если изменились только его части :
Немного напрягая наше воображение, такого рода глубокая копия всего остального только для того, чтобы сделать одну маленькую деталь уникальной, может быть довольно дорогой (в реальных случаях этот блок памяти будет намного, намного больше, чтобы создать проблему).
Однако, несмотря на такие затраты, этот тип конструкции будет гораздо менее подвержен человеческим ошибкам. Любой, кто работал на функциональном языке с чистыми функциями, может, вероятно, оценить это, особенно в многопоточных случаях, когда мы можем многопоточность такого кода без заботы в мире. В целом, программисты-люди склонны спотыкаться об изменениях состояния, особенно тех, которые вызывают внешние побочные эффекты для состояний, выходящих за рамки текущей функции. Даже восстановление после внешней ошибки (исключения) в таком случае может быть невероятно трудным с изменяемыми изменениями внешнего состояния в миксе.
Одним из способов смягчения этой избыточной работы копирования является превращение этих блоков памяти в набор указателей (или ссылок) на символы, например, так:
Извините, я не смог понять, что нам не нужно делать L
уникальные при создании диаграммы.
Синий указывает на мелкие скопированные данные.
... к сожалению, было бы невероятно дорого заплатить указатель / справочную стоимость за символ. Кроме того, мы можем разбросать содержимое символов по всему адресному пространству и в конечном итоге заплатить за него в виде множества ошибок страниц и кеша, что может сделать это решение еще хуже, чем копировать всю вещь целиком.
Даже если бы мы были осторожны в распределении этих символов подряд, скажем, машина может загрузить 8 символов и 8 указателей на символ в строку кэша. В итоге мы загружаем память следующим образом:
В этом случае нам понадобится загрузить 7 разных строк кеша непрерывной памяти для прохождения этой строки, когда в идеале нам нужно только 3.
Разделите данные
Чтобы смягчить проблему, описанную выше, мы можем применить ту же базовую стратегию, но на более грубом уровне из 8 символов, например
Результат требует загрузки данных в 4 строки кэша (1 для 3 указателей и 3 для символов) для прохождения этой строки, что лишь на 1 меньше теоретического оптимума.
Так что это совсем не плохо. Есть некоторая потеря памяти, но памяти много, и использование большего количества не замедляет работу, если дополнительная память просто будет иметь холодные данные, к которым не часто обращаются. Это только для горячих, непрерывных данных, где уменьшенное использование памяти и скорость часто идут рука об руку, где мы хотим поместить больше памяти в одну страницу или строку кэша и получить доступ ко всем этим до выселения. Это представление довольно дружественно к кешу.
скорость
Таким образом, использование представлений, подобных приведенному выше, может дать довольно приличный баланс производительности. Вероятно, наиболее критично для производительности использование неизменяемых структур данных возьмет на себя такую модификацию фрагментов данных и сделает их уникальными в процессе, при этом копируя неизмененные фрагменты. Это также подразумевает некоторые накладные расходы на атомарные операции для безопасной ссылки на мелкие скопированные фрагменты в многопоточном контексте (возможно, при некотором атомарном подсчете ссылок).
Тем не менее, пока эти массивные фрагменты данных представлены на достаточно грубом уровне, большая часть этих издержек уменьшается и, возможно, даже упрощается, в то же время давая нам безопасность и простоту кодирования и многопоточности в большем количестве функций в чистом виде без внешней стороны. последствия.
Хранение новых и старых данных
Я считаю, что неизменность потенциально наиболее полезна с точки зрения производительности (в практическом смысле), когда у нас может возникнуть соблазн сделать целые копии больших данных, чтобы сделать их уникальными в изменчивом контексте, целью которого является создание чего-то нового из что-то, что уже существует таким образом, что мы хотим сохранить как новое, так и старое, когда мы могли бы просто сделать небольшие кусочки этого уникального с осторожным неизменным дизайном.
Пример: отменить систему
Примером этого является система отмены. Мы можем изменить небольшую часть структуры данных и захотим сохранить как исходную форму, к которой мы можем отменить, так и новую форму. При таком неизменном дизайне, который делает уникальными только небольшие измененные разделы структуры данных, мы можем просто сохранить копию старых данных в записи отмены, оплачивая только стоимость памяти добавленных данных уникальных частей. Это обеспечивает очень эффективный баланс производительности (что делает реализацию системы отмены простым куском) и производительности.
Интерфейсы высокого уровня
Тем не менее, что-то неловкое возникает с вышеупомянутым случаем. В контексте локального вида функций изменяемые данные часто проще и проще всего изменить. В конце концов, самый простой способ изменить массив - это просто пройти через него и изменять по одному элементу за раз. Мы можем в конечном итоге увеличить интеллектуальные издержки, если у нас будет большое количество высокоуровневых алгоритмов на выбор для преобразования массива, и нам нужно было выбрать подходящий алгоритм, чтобы гарантировать, что все эти короткие мелкие копии создаются, пока изменяемые части сделано уникальным.
Вероятно, самый простой способ в этих случаях - это использовать изменяемые буферы локально внутри контекста функции (где они обычно не сбивают нас с толку), которая атомарно фиксирует изменения в структуре данных, чтобы получить новую неизменяемую копию (я полагаю, что некоторые языки требуют эти "переходные процессы") ...
... или мы можем просто смоделировать высокоуровневые и высокоуровневые функции преобразования над данными, чтобы мы могли скрыть процесс изменения изменяемого буфера и фиксации его в структуре без использования изменяемой логики. В любом случае, это еще не очень широко изученная территория, и нам придется сократить нашу работу, если мы будем больше использовать неизменяемые конструкции, чтобы придумать содержательные интерфейсы для преобразования этих структур данных.
Структуры данных
Другая вещь, которая возникает здесь, заключается в том, что неизменность, используемая в контексте, критичном к производительности, вероятно, приведет к тому, что структуры данных будут разбиты на объемные данные, где фрагменты не слишком малы по размеру, но и не слишком велики.
Связанные списки могут хотеть измениться немного, чтобы приспособиться к этому и превратиться в развернутые списки. Большие непрерывные массивы могут превратиться в массив указателей в непрерывные блоки с модульным индексированием для произвольного доступа.
Это потенциально меняет то, как мы смотрим на структуры данных, интересным образом, в то же время подталкивая модифицирующие функции этих структур данных к более объемному характеру, чтобы скрыть дополнительную сложность мелкого копирования некоторых битов здесь и придания другим битам уникальности там.
Представление
Во всяком случае, это мой маленький взгляд на эту тему. Теоретически неизменность может иметь стоимость от очень большой до меньшей. Но очень теоретический подход не всегда заставляет приложения работать быстро. Это может сделать их масштабируемыми, но в реальном мире скорость часто требует более практичного мышления.
С практической точки зрения такие качества, как производительность, ремонтопригодность и безопасность, как правило, превращаются в одно большое размытие, особенно для очень большой кодовой базы. Хотя производительность в некотором абсолютном смысле ухудшается из-за неизменности, трудно утверждать, что она имеет преимущества для производительности и безопасности (включая безопасность потоков). Увеличение этих показателей часто может привести к увеличению практической производительности, хотя бы потому, что разработчики имеют больше времени для настройки и оптимизации своего кода без ошибок.
Так что я думаю , что из этого практического смысла, неизменные структуры данных фактически могут помочь производительности в большинстве случаев, как бы странно это звучит. Идеальный мир может искать смесь этих двух: неизменяемых структур данных и изменчивых, причем изменяемые структуры обычно очень безопасно использовать в очень локальном контексте (например, локально для функции), тогда как неизменяемые могут избегать внешней стороны выполняет все сразу и превращает все изменения в структуру данных в элементарную операцию, создавая новую версию без риска возникновения гонок.