Сравнение парадигм программирования - Comparison of programming paradigms

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

Основные парадигмальные подходы

Есть два основных подхода к программированию:

Следующие примеры широко считаются основными парадигмами программирования, как это видно при измерении популярности языков программирования :

Ниже приведены распространенные типы программирования, которые могут быть реализованы с использованием различных парадигм:

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

Парадигма Описание Основные черты Связанные парадигмы Критика Примеры
Императив Программы как операторы, которые напрямую изменяют вычисленное состояние ( поля данных ) Прямые назначения , общие структуры данных , глобальные переменные Эдсгер В. Дейкстра , Майкл А. Джексон C , C ++ , Java , Kotlin , PHP , Python , Ruby
Структурированный Стиль императивного программирования с более логичной структурой программы Structograms , отступы , отсутствие или ограниченное использование GOTO заявления Императив C , C ++ , Java , Kotlin , Паскаль , PHP , Python
Процедурный На основе структурного программирования, основанного на концепции модульного программирования или вызова процедуры. Локальные переменные , последовательность, выбор, итерация и модуляризация Структурированный, императивный C , C ++ , Lisp , PHP , Python
Функциональный Рассматривает вычисления как оценку математических функций, избегая состояния и изменяемых данных. Лямбда-исчисление , композиционность , формула , рекурсия , ссылочная прозрачность , без побочных эффектов Декларативная C ++ , C # , Clojure , CoffeeScript , Elixir , Erlang , F # , Haskell , Java (начиная с версии 8), Kotlin , Lisp , Python , R , Ruby , Scala , SequenceL , Standard ML , JavaScript , Elm
Управляемый событиями, в том числе управляемый временем Поток управления определяется в основном событиями , такими как щелчки мыши или прерывания, включая таймер Основной цикл , обработчики событий, асинхронные процессы Процедурные, информационный поток JavaScript , ActionScript , Visual Basic , Elm
Объектно-ориентированный Лечит поля данных в качестве объектов манипулируют с помощью предопределенных методов только Объекты , методы, передача сообщений , скрытие информации , абстракция данных , инкапсуляция , полиморфизм , наследование , сериализация -маршаллинг Процедурный Википедия , другие Common Lisp , C ++ , C # , Eiffel , Java , Kotlin , PHP , Python , Ruby , Scala , JavaScript
Декларативная Определяет логику программы, но не подробный поток управления Языки четвертого поколения , электронные таблицы , генераторы программ отчетов SQL , регулярные выражения , Prolog , OWL , SPARQL , Datalog , XSLT
Автоматное программирование Рассматривает программы как модель конечного автомата или любого другого формального автомата. Государственное перечисление , управляющая переменная , состояние изменяется, изоморфизм , таблица состояний перехода Императивный, событийный Абстрактный язык конечных автоматов

Различия в терминологии

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

Языковая поддержка

Синтаксический сахар - это улучшение функциональности программы за счет введения языковых функций, облегчающих конкретное использование, даже если конечный результат может быть достигнут без них. Одним из примеров синтаксического сахара могут быть классы, используемые в объектно-ориентированных языках программирования . Императивный язык C может поддерживать объектно-ориентированное программирование с помощью своих средств указателей на функции , приведения типов и структур. Однако такие языки, как C ++, стремятся сделать объектно-ориентированное программирование более удобным путем введения синтаксиса, специфичного для этого стиля кодирования. Более того, специализированный синтаксис подчеркивает объектно-ориентированный подход. Точно так же функции и синтаксис циклов в C (и других процедурных и структурированных языках программирования) можно рассматривать как синтаксический сахар. Язык ассемблера может поддерживать процедурное или структурированное программирование с помощью своих средств изменения значений регистров и выполнения ветвления в зависимости от состояния программы. Однако в таких языках, как C, введен синтаксис, специфичный для этих стилей кодирования, чтобы сделать процедурное и структурированное программирование более удобным. Возможности языка C # (C Sharp), такие как свойства и интерфейсы, аналогичным образом не позволяют использовать новые функции, но предназначены для того, чтобы сделать хорошие методы программирования более заметными и естественными.

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

Расширением этого является синтаксический сахарин или бесплатный синтаксис, который не упрощает программирование.

Сравнение производительности

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

Парадигмы, которые широко используют подпрограммы (включая функциональные, процедурные и объектно-ориентированные) и не используют также значительного встроенного расширения (встраивание через оптимизацию компилятора ), следовательно, будут использовать большую часть общих ресурсов на связях подпрограмм. Объектно-ориентированные программы, которые намеренно не изменяют состояние программы напрямую, а вместо этого используют методы мутатора (или сеттеры ) для инкапсуляции этих изменений состояния, как прямое следствие, будут иметь больше накладных расходов. Это связано с тем, что передача сообщений - это, по сути, вызов подпрограммы, но с тремя дополнительными накладными расходами: динамическое выделение памяти , копирование параметров и динамическая отправка . Получение памяти из кучи и копирование параметров для передачи сообщений может потребовать значительных ресурсов, которые намного превышают ресурсы, необходимые для изменения состояния. Аксессоры (или геттеры ), которые просто возвращают значения частных переменных-членов, также зависят от аналогичных подпрограмм передачи сообщений вместо использования более прямого присваивания (или сравнения), увеличивающего общую длину пути.

Управляемый код

Для программ, выполняемых в среде управляемого кода , такой как .NET Framework , многие проблемы влияют на производительность, на которые существенно влияет парадигма языка программирования и различные используемые языковые функции.

Примеры псевдокода, сравнивающие различные парадигмы

Псевдокод сравнение императива, процедурный и объектно - ориентированных подходы , используемые для вычисления площади круга (πr²), предполагая отсутствие подпрограммы встраивания , нет макро препроцессоров , зарегистрировать арифметические и взвешивание каждой «шага» команд , как только одна инструкция - как грубая мера длины пути команд - представлена ​​ниже. Шаг инструкции, который концептуально выполняет изменение состояния, в каждом случае выделяется жирным шрифтом. Арифметические операции, используемые для вычисления площади круга, одинаковы во всех трех парадигмах, с той разницей, что процедурная и объектно-ориентированная парадигмы заключают эти операции в вызов подпрограммы, что делает вычисление универсальным и многократно используемым. Тот же эффект может быть достигнут в чисто императивной программе с использованием препроцессора макросов только за счет увеличения размера программы (только на каждом сайте вызова макроса) без соответствующей пропорциональной стоимости времени выполнения (пропорциональной n вызовам, которые могут быть расположены в пределах внутренний цикл, например). И наоборот, встраивание подпрограмм компилятором может уменьшить процедурные программы до чего-то похожего по размеру на чисто императивный код. Однако для объектно-ориентированных программ, даже со встраиванием, сообщения все равно должны быть построены (из копий аргументов) для обработки объектно-ориентированными методами. Накладные расходы на вызовы, виртуальные или другие, не зависят от изменения потока управления, а зависят от затрат на сопутствующие соглашения о вызовах , таких как код пролога и эпилога , настройка стека и передача аргументов (см. Здесь более реалистичную длину пути инструкций, стек и другие расходы, связанные со звонками на платформе x86 ). См. Также здесь слайд-презентацию Эрика С. Робертса («Распределение памяти по переменным», глава 7), иллюстрирующую использование стека и кучи памяти при суммировании трех рациональных чисел в объектно-ориентированном языке Java .

Императив Процедурный Объектно-ориентированный
 load r;                      1
 r2 = r * r;                  2
 result = r2 * "3.142";       3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.... storage .............
result variable
constant "3.142"
area proc(r2,res):
   push stack                                 5
   load r2;                                   6
   r3 = r2 * r2;                              7
   res = r3 * "3.142";                        8
   pop stack                                  9
   return;                                   10
...............................................
main proc:
   load r;                                    1
   call area(r,result);
    +load p = address of parameter list;      2
    +load v = address of subroutine 'area';   3
    +goto v with return;                      4
.
.
.
.
.... storage .............
result variable
constant "3.142"
parameter list variable
function pointer (==>area)
stack storage
circle.area method(r2):
   push stack                                 7
   load r2;                                   8
   r3 = r2 * r2;                              9
   res = r3 * "3.142";                       10
   pop stack                                 11
   return(res);                           12,13
...............................................
main proc:
   load r;                                    1
   result = circle.area(r);
      +allocate heap storage;                 2
      +copy r to message;                     3
      +load p = address of message;           4
      +load v = addr. of method 'circle.area' 5
      +goto v with return;                    6
.
.
.... storage .............
result variable (assumed pre-allocated)
immutable variable "3.142" (final)
(heap) message variable for circle method call
vtable(==>area)
stack storage

Преимущества процедурной абстракции и объектно-ориентированного полиморфизма плохо иллюстрируются небольшим примером, подобным приведенному выше. Этот пример предназначен в основном для иллюстрации некоторых внутренних различий в производительности, а не для абстракции или повторного использования кода.

Подпрограмма, накладные расходы на вызов метода

Присутствие (вызываемой) подпрограммы в программе не вносит ничего лишнего в функциональность программы независимо от парадигмы, но может в значительной степени способствовать структурированию и универсальности программы, что значительно упрощает ее написание, изменение и расширение. Степень, в которой различные парадигмы используют подпрограммы (и связанные с ними требования к памяти), влияет на общую производительность всего алгоритма, хотя, как указал Гай Стил в статье 1977 года, хорошо спроектированная реализация языка программирования может иметь очень низкие накладные расходы на процедурную абстракцию. (но сожалеет, что в большинстве реализаций они редко достигают этого на практике - будучи «довольно легкомысленными или небрежными в этом отношении»). В той же статье Стил также рассматривает аргументы в пользу программирования на основе автоматов (с использованием вызовов процедур с хвостовой рекурсией ) и приходит к выводу, что «мы должны проявлять здоровое уважение к вызовам процедур» (потому что они мощные), но предлагал «использовать их экономно. "

По частоте вызовов подпрограмм:

  • Для процедурного программирования степень детализации кода в значительной степени определяется количеством дискретных процедур или модулей .
  • Для функционального программирования распространены частые вызовы библиотечных подпрограмм, но они часто могут быть встроены оптимизирующим компилятором.
  • Для объектно-ориентированного программирования количество вызываемых вызовов методов также частично определяется степенью детализации структур данных и, таким образом, может включать в себя множество доступов только для чтения к объектам низкого уровня, которые инкапсулированы и, следовательно, недоступны ни в каком другом, более прямом виде, способ. Поскольку повышенная степень детализации является предпосылкой для большего повторного использования кода , наблюдается тенденция к детализации структур данных и соответствующему увеличению количества дискретных объектов (и их методов) и, следовательно, вызовов подпрограмм. Активно не поощряется создание божественных объектов . Конструкторы также добавляют к счетчику, поскольку они также являются вызовами подпрограмм (если они не встроены). Проблемы с производительностью, вызванные чрезмерной детализацией, могут не проявиться до тех пор, пока масштабируемость не станет проблемой.
  • Для других парадигм, где может использоваться сочетание вышеперечисленных парадигм, использование подпрограмм менее предсказуемо.

Выделение динамической памяти для хранения сообщений и объектов

Уникально, что объектно-ориентированная парадигма включает динамическое выделение памяти из кучи как для создания объектов, так и для передачи сообщений. Тест 1994 года - «Затраты на выделение памяти в больших программах на C и C ++», проведенный Digital Equipment Corporation для разнообразного программного обеспечения с использованием инструмента профилирования на уровне инструкций, позволил измерить, сколько инструкций требовалось для распределения динамической памяти. Результаты показали, что наименьшее абсолютное количество выполненных инструкций составляло в среднем около 50, но другие достигали 611. См. Также «Куча: Удовольствия и боли» Мурали Р. Кришнана, в котором говорится: «Реализации кучи имеют тенденцию оставаться общими для всех платформ, и следовательно, имеют большие накладные расходы ". В статье IBM 1996 года «Масштабируемость алгоритмов динамического распределения памяти» Аруна Айенгара из IBM демонстрируются различные алгоритмы динамической памяти и их соответствующее количество инструкций. Даже рекомендуемый алгоритм MFLF I (HS Stone, RC 9674) показывает количество инструкций в диапазоне от 200 до 400. Приведенный выше пример псевдокода не включает реалистичную оценку этой длины пути выделения памяти или задействованных накладных расходов префикса памяти и связанного с этим мусора. накладные расходы по сбору. Настоятельно заявляя, что выделение кучи является нетривиальной задачей, один микрокаллокатор программного обеспечения с открытым исходным кодом , разработанный разработчиком игр Джоном В. Рэтклиффом , состоит из почти 1000 строк кода.

Динамически отправляемые вызовы сообщений v. Накладные расходы на прямые вызовы процедур

В своей аннотации « Оптимизация объектно-ориентированных программ с использованием статического анализа иерархии классов » Джеффри Дин, Дэвид Гроув и Крейг Чемберс с факультета компьютерных наук и инженерии Вашингтонского университета утверждают, что «интенсивное использование наследования и динамическое -связанные сообщения, вероятно, сделают код более расширяемым и многоразовым, но это также накладывает значительные накладные расходы на производительность по сравнению с эквивалентной, но нерасширяемой программой, написанной не объектно-ориентированным способом. В некоторых областях, таких как пакеты структурированной графики стоимость производительности за счет дополнительной гибкости, обеспечиваемой использованием сильно объектно-ориентированного стиля, является приемлемой. Однако в других областях, таких как библиотеки базовых структур данных, пакеты численных вычислений, библиотеки рендеринга и фреймворки моделирования на основе трассировки, стоимость передача сообщений может быть слишком большой, что вынуждает программиста избегать объектно-ориентированного программирования в «горячих точках» своего приложения ».

Сериализация объектов

Сериализация накладывает большие накладные расходы при передаче объектов из одной системы в другую, особенно когда передача осуществляется в удобочитаемых форматах, таких как Extensible Markup Language ( XML ) и JavaScript Object Notation ( JSON ). Это контрастирует с компактными двоичными форматами для не объектно-ориентированных данных. Как кодирование, так и декодирование значения данных объекта и его атрибутов участвуют в процессе сериализации, который также включает понимание сложных проблем, таких как наследование, инкапсуляция и скрытие данных.

Параллельные вычисления

Профессор Университета Карнеги-Меллона Роберт Харпер в марте 2011 года написал: «В этом семестре мы с Дэном Ликатой совместно преподаем новый курс функционального программирования для будущих студентов первого года обучения CS ... Объектно-ориентированное программирование полностью исключено из вводной программы. , потому что он одновременно антимодульный и антипараллельный по самой своей природе и, следовательно, не подходит для современной учебной программы CS. Предлагаемый новый курс по методологии объектно-ориентированного проектирования будет предложен на втором курсе для тех студентов, которые хотят изучать Эта тема."

Смотрите также

использованная литература

дальнейшее чтение

внешние ссылки