Введение
Свойство алгоритма
В информатике алгоритмическая эффективность — это свойство алгоритма, характеризующее количество используемых им вычислительных ресурсов. Алгоритмическую эффективность можно рассматривать как аналог инженерной производительности для повторяющегося или непрерывного процесса. Для достижения максимальной эффективности желательно минимизировать потребление ресурсов. Однако различные ресурсы, такие как временная и пространственная сложность, нельзя сравнивать напрямую, поэтому оценка эффективности двух алгоритмов часто зависит от того, какая мера эффективности считается наиболее важной. Например, сортировка пузырьком и сортировка TimSort — это оба алгоритма для сортировки списка элементов от наименьшего к наибольшему. Сортировка пузырьком выполняется за время, пропорциональное квадрату количества элементов (см. нотацию «Большое О»), но требует лишь небольшого объема дополнительной памяти, который постоянен относительно длины списка. TimSort сортирует список за время, линейно-логарифмическое по длине списка (пропорциональное величине, умноженной на логарифм этой величины), но требует объема памяти, линейного относительно длины списка. Если для конкретного приложения необходимо быстро сортировать большие списки, TimSort будет лучшим выбором; однако, если важнее минимизировать объем используемой памяти, сортировка пузырьком будет предпочтительнее.
Обзор
Алгоритм считается эффективным, если его потребление ресурсов, также известное как вычислительная стоимость, находится на приемлемом уровне или ниже. Грубо говоря, "приемлемый" означает, что он выполнится за разумное время и с использованием разумного объема памяти на доступном компьютере, как правило, в зависимости от размера входных данных. С 1950-х годов компьютеры значительно увеличили как вычислительную мощность, так и объем доступной памяти, поэтому современные приемлемые уровни были бы неприемлемы даже десять лет назад. Фактически, благодаря приблизительному удвоению вычислительной мощности каждые два года, задачи, которые являются приемлемо эффективными на современных смартфонах и встраиваемых системах, могли быть неприемлемо неэффективными для промышленных серверов десять лет назад. Производители компьютеров часто выпускают новые модели, зачастую с более высокой производительностью. Стоимость программного обеспечения может быть значительной, поэтому в некоторых случаях самым простым и дешевым способом повышения производительности может быть просто покупка более быстрого компьютера, при условии его совместимости с существующим оборудованием. Существует множество способов измерения ресурсов, используемых алгоритмом: наиболее распространенными являются скорость и использование памяти; другие показатели могут включать скорость передачи данных, временное использование диска, долгосрочное использование диска, энергопотребление, общую стоимость владения, время отклика на внешние воздействия и т. д. Многие из этих показателей зависят от размера входных данных, то есть от объема обрабатываемой информации. Они также могут зависеть от организации данных; например, некоторые алгоритмы сортировки показывают плохие результаты на уже отсортированных или отсортированных в обратном порядке данных. На практике на эффективность алгоритма могут влиять и другие факторы, такие как требования к точности и/или надежности. Как подробно описано ниже, способ реализации алгоритма также может существенно влиять на фактическую эффективность, хотя многие аспекты этого связаны с вопросами оптимизации.
Проблемы с реализацией
Проблемы реализации также могут влиять на эффективность, например, выбор языка программирования, способ фактической реализации алгоритма в коде, выбор компилятора для конкретного языка, используемые опции компиляции или даже используемая операционная система. Во многих случаях язык, реализованный интерпретатором, может быть значительно медленнее, чем язык, реализованный компилятором. См. статьи о JIT-компиляции и интерпретируемых языках. Существуют и другие факторы, которые могут влиять на производительность по времени или объему памяти, но которые могут быть вне контроля программиста: выравнивание данных, гранулярность данных, локальность кэша, когерентность кэша, сборка мусора, параллелизм на уровне инструкций, многопоточность (на аппаратном или программном уровне), одновременная многозадачность и вызовы подпрограмм. Некоторые процессоры поддерживают векторную обработку, позволяющую одной инструкции оперировать несколькими операндами; использование этих возможностей может быть как простым, так и сложным для программиста или компилятора. Алгоритмы, разработанные для последовательной обработки, могут потребовать полной переработки для использования параллельной обработки, либо их можно легко переконфигурировать. По мере роста важности параллельных и распределенных вычислений в конце 2010-х годов увеличиваются инвестиции в эффективные высокоуровневые API для параллельных и распределенных вычислительных систем, такие как CUDA, TensorFlow, Hadoop, OpenMP и MPI. Другая проблема, возникающая при программировании, заключается в том, что процессоры, совместимые с одним и тем же набором инструкций (например, x86-64 или ARM), могут реализовывать одну и ту же инструкцию по-разному, в результате чего инструкции, относительно быстрые на одних моделях, могут быть относительно медленными на других. Это часто создает трудности для оптимизирующих компиляторов, которым необходимо обладать обширными знаниями о конкретном процессоре и другом доступном оборудовании на целевой платформе компиляции, чтобы наилучшим образом оптимизировать программу для достижения максимальной производительности. В крайнем случае компилятор может быть вынужден эмулировать инструкции, не поддерживаемые на целевой платформе компиляции, что заставит его генерировать код или подключать вызов внешней библиотеки для получения результата, который иначе невозможно вычислить на этой платформе, даже если он поддерживается аппаратно и более эффективен на других платформах. Это часто встречается во встраиваемых системах в отношении арифметики с плавающей точкой, где малогабаритные и маломощные микроконтроллеры часто не имеют аппаратной поддержки для арифметики с плавающей точкой и, следовательно, требуют ресурсоемких программных подпрограмм для выполнения вычислений с плавающей точкой.
Теория
Анализируйте алгоритм, как правило, с помощью анализа временной сложности, чтобы получить оценку времени выполнения как функции от размера входных данных. Результат обычно представляется в нотации «Большое О». Это полезно для сравнения алгоритмов, особенно при обработке больших объемов данных. Для сравнения производительности алгоритмов требуются более детальные оценки, когда объем данных невелик, хотя это, вероятно, менее важно. Алгоритмы, включающие параллельную обработку, могут быть сложнее анализировать.
Практика
Используйте эталонный тест для измерения времени работы алгоритма. Многие языки программирования предоставляют функцию, которая позволяет определить время, затраченное процессором. Для алгоритмов, работающих длительное время, также может быть полезно измерять общее время выполнения. Результаты обычно следует усреднять по нескольким прогонам. Профилирование во время выполнения может быть очень чувствительным к конфигурации оборудования и возможности одновременной работы других программ или задач в многопроцессорной и многозадачной среде. Такой тип тестирования также сильно зависит от выбора конкретного языка программирования, компилятора и его опций, поэтому алгоритмы, которые сравниваются, должны быть реализованы в одинаковых условиях.
Хранилище иерархия памяти
Современные компьютеры могут иметь относительно большой объем памяти (возможно, гигабайты), поэтому необходимость сжимать алгоритм в ограниченный объем памяти является гораздо меньшей проблемой, чем раньше. Однако наличие четырех различных категорий памяти может быть существенным:
Процессорные регистры – самая быстрая из технологий компьютерной памяти с наименьшим объемом хранения. Большинство прямых вычислений на современных компьютерах выполняются с исходными и целевыми операндами в регистрах, прежде чем они будут обновлены в кэше, основной памяти и виртуальной памяти, если это необходимо. На ядре процессора обычно доступно порядка сотен байт или меньше регистров, хотя файл регистров может содержать больше физических регистров, чем архитектурные регистры, определенные в архитектуре набора команд. Кэш-память – вторая по скорости и вторая по объему память в иерархии памяти. Кэши присутствуют в процессорах, графических процессорах, жестких дисках и внешних периферийных устройствах и обычно реализованы на статической оперативной памяти (SRAM). Кэши памяти многоуровневые: более низкие уровни больше, медленнее и обычно совместно используются ядрами процессора в многоядерных процессорах. Для обработки операндов в кэш-памяти процессор должен извлечь данные из кэша, выполнить операцию в регистрах и записать данные обратно в кэш. Это происходит со скоростью, сопоставимой (примерно в 2-10 раз медленнее) со скоростью арифметико-логического устройства (ALU) или блока операций с плавающей запятой (FPU) процессора или графического процессора, если данные находятся в кэше L1. Скорость снижается примерно в 10 раз при промахе кэша L1, когда данные необходимо извлечь из кэша L2 и записать в него, и еще в 10 раз при промахе кэша L2, когда данные необходимо извлечь из кэша L3 (если он присутствует). Основная физическая память чаще всего реализована на динамической оперативной памяти (DRAM). Основная память значительно больше (обычно гигабайты по сравнению с ≈8 мегабайтами), чем кэш L3 процессора, а задержки чтения и записи обычно в 10-100 раз выше. По состоянию на 2018 год оперативная память все чаще интегрируется в чип процессора в виде памяти процессора или графического процессора. Виртуальная память чаще всего реализована на основе вторичной памяти, такой как жесткий диск, и является расширением иерархии памяти, которое обеспечивает гораздо больший объем хранения, но значительно большую задержку, обычно в 1000 раз больше, чем промах кэша для значения в оперативной памяти. Хотя изначально виртуальная память была создана для создания иллюзии большего объема памяти, чем было доступно на самом деле, в современном использовании она более важна благодаря компромиссу между временем и пространством и возможности использования виртуальных машин. Промахи кэша из основной памяти называются ошибками страниц и приводят к значительным потерям производительности программ. Алгоритм, чьи потребности в памяти помещаются в кэш-память, будет работать намного быстрее, чем алгоритм, который помещается в основную память, который, в свою очередь, будет намного быстрее, чем алгоритм, которому приходится обращаться к виртуальной памяти. Поэтому политики замены кэша чрезвычайно важны для высокопроизводительных вычислений, как и программирование с учетом кэша и выравнивание данных. Чтобы еще больше усложнить ситуацию, некоторые системы имеют до трех уровней кэш-памяти с различными эффективными скоростями. Различные системы будут иметь разный объем этих различных типов памяти, поэтому влияние потребностей алгоритма в памяти может сильно варьироваться от одной системы к другой. В первые дни электронных вычислений, если алгоритм и его данные не помещались в основную память, алгоритм не мог быть использован. Сегодня использование виртуальной памяти, по-видимому, обеспечивает большой объем памяти, но ценой производительности. Если алгоритм и его данные помещаются в кэш-память, можно достичь очень высокой скорости; в этом случае минимизация объема памяти также поможет минимизировать время. Это называется принципом локальности, который можно разделить на локальность ссылок, пространственную локальность и временную локальность. Алгоритм, который не полностью помещается в кэш-память, но демонстрирует локальность ссылок, может работать достаточно хорошо.