Хабрахабр

И всё же C — низкоуровневый язык

Какие-то из них используются до сих пор, другие — повлияли на следующие поколения языков, популярность третьих тихо сошла на нет. За прошедшие с момента появления языка C десятилетия было создано множество интереснейших языков программирования. Между тем архаичный, противоречивый, примитивный, сделанный в худших традициях своего поколения языков C (и его наследники) живее всех живых.

Она звучит то громче, то тише, но в последнее время буквально оглушает. Критика C — классический для нашей индустрии эпистолярный жанр. Про C можно говорить разное, в дизайне языка действительно много неприятных ошибок, но отказывать C в «низкоуровневости» — это уже слишком! Пример — перевод статьи Дэвида Чизнэлла «C — не низкоуровневый язык», опубликованный в нашем блоге некоторое время назад.

Так получилась эта статья. Чтобы не терпеть такую несправедливость, я собрался с духом и постарался определиться с тем, что есть язык программирования низкого уровня и чего хотят от него практики, после чего перебрал аргументы критиков C.

Вот некоторые аргументы критиков С, перечисленные в том числе и в статье Дэвида Чизнэлла:

  1. Абстрактная машина языка C слишком похожа на устаревшую архитектуру PDP-11, которая давно уже не соответствует устройству популярных современных процессоров.
  2. Несоответствие абстрактной машины C устройству реальных машин усложняет разработку оптимизирующих компиляторов языка.
  3. Неполнота и сложность стандарта языка ведут к разночтениям в реализациях стандарта.
  4. Доминирование C-подобных языков не позволяет исследовать альтернативные архитектуры процессоров.

Давайте для начала определимся с требованиями к низкоуровневому языку, после чего вернёмся к приведённым аргументам.

Но перед обсуждением спорных моментов желательно иметь хоть какие-то исходные требования к предмету спора. Общепризнанного определения языка низкого уровня не существует.

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

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

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

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

А если есть контракт, то возникает необходимость в независимом от конкретной реализации стандарта языке. Заметьте, что для такого языка не так важны высокоуровневые абстракции, — для него важнее служить контрактом между платформой, компилятором и разработчиком.

Наше первое требование — общие для целевых платформ черты — выражается в абстрактной машине языка, поэтому с неё мы и начнём обсуждение C.

В её основе лежит традиционная архитектура фон Неймана, в которой программы выполняются последовательно центральным процессором, а память представляет собой плоскую ленту, где хранятся и данные, и сами программы. Платформа, на которой появился язык C, — PDP-11. Такая архитектура легко реализуется в железе, и со временем все компьютеры общего назначения стали использовать именно её.

von Neuman bottleneck). Современные усовершенствования архитектуры фон Неймана направлены на устранение её главного узкого места — задержек при обмене данными между процессором и памятью (англ. Разница в производительности памяти и центрального процессора привела к появлению кеширующих подсистем процессоров (одноуровневых и позже — многоуровневых).

Современные процессоры стали суперскалярными (англ. Но даже кешей в наши дни уже недостаточно. Задержки при получении инструкциями данных из памяти частично компенсируются внеочередным выполнением (англ. superscalar). branch predictor). instruction-level parallelism) инструкций вкупе с предсказателем ветвлений (англ.

К нему относятся архитектуры, построенные вокруг процессоров с единственным ядром: настольные и серверные x86, мобильные ARM, сходящие со сцены Sun/Oracle SPARC и IBM POWER. Последовательная абстрактная машина C (и многих других языков) имитирует работу не столько конкретно PDP-11, сколько любых компьютеров, устроенных по принципу архитектуры фон Неймана.

Архитектура фон Неймана, таким образом, была масштабирована на несколько ядер. Со временем в один процессор стали интегрировать несколько вычислительных ядер, в результате чего появилась необходимость поддерживать когерентность кешей каждого ядра и потребовались протоколы межъядерного взаимодействия.

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

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

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

Стандартизированное и переносимое воплощение архитектуры фон Неймана — абстрактная машина C — удобно реализуется на всех основных платформах и поэтому пользуется своей популярностью как портативного ассемблера вполне заслуженно.

В случае C это непосредственная работа с памятью и объектами в ней как с массивом байтов, возможность напрямую работать с адресами байтов и развитая арифметика указателей. Наше второе требование к языку низкого уровня — доступ к низкоуровневым деталям реализации каждой из популярной платформ.

Вместе с указателями и примитивными механизмами циклов это усложняет работу оптимизатора. Критики C указывают на то, что в стандарте языка даётся слишком много гарантий касательно, например, расположения отдельных полей в структурах и объединениях.

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

В рамках C возможно поработать компилятором, выбрав, к примеру, инструкции SIMD и правильно разместив данные в памяти. Позиция разработчиков C в данном случае такова: низкоуровневый язык должен позволять работать на уровне, достаточно низком для самостоятельного решения программистом задач оптимизации.

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

Но практикам бывают нужны именно низкоуровневые инструменты, иначе язык не получится использовать для разработки операционных систем и других низкоуровневых программ, то есть он не будет удовлетворять второму из наших требований. Интересно, что Чизнэлл в статье под названием «C — не низкоуровневый язык» парадоксально утверждает, что C — слишком низкоуровневый, указывая на отсутствие в нём высокоуровневых инструментов.

У функциональных языков тоже хватает эффективных компиляторов: MLTon, OCaml и другие. Отвлекаясь от описания проблем оптимизации именно C, хочу заметить, что в настоящий момент в оптимизирующие компиляторы высокоуровневых языков (тех же C# и Java) вложено не меньше усилий, чем в GCC или Clang. Но разработчики того же OCaml пока могут похвастаться производительностью в лучшем случае в половину скорости кода на C…

Чизнэлл приводит в своей статье ссылку на результаты опроса, проведённого в 2015 году: многие программисты допускали ошибки в решении задач на понимание стандартов C.

У меня версия C99 есть в бумажном виде, страниц эдак на 900. Полагаю, что кто-то из читателей имел дело со стандартом C. Удовольствие от работы со стандартом C не получает никто: ни разработчики компиляторов, ни разработчики документа, ни программисты. Это не лаконичная спецификация Scheme объёмом меньше 100 страниц и не вылизанный Standard ML, состоящий из 300.

Авторы ANSI C проделали огромную работу, обобщив существующие реализации и прикрыв бесчисленными «костылями» неортогональности в дизайне языка. Но надо понимать, что стандарт C разрабатывался постфактум, уже после появления множества «почти-еле-только местами» совместимых диалектов.

Но C был реализовал множеством компиляторов. Может показаться странным, что такой документ вообще кто-то взялся реализовывать. Но, очевидно, стандарт был всем в индустрии действительно нужен. Я не буду пересказывать чужие байки о зоопарке мира UNIX конца 80-х, тем более что сам в то время считал не слишком уверенно и только до пяти.

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

Более-менее опытный программист способен разработать неоптимизирующий компилятор C в разумные сроки, что подтверждается существованием множества полулюбительских реализаций (тех же TCC, LCC и 8cc). На самом деле современный стандарт C не так уж и плох.

Наличие общепринятого стандарта означает, что C удовлетворяет последнему из наших требований к языку низкого уровня: этот язык строится от спецификации, а не конкретной реализации.

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

Но каждый из этих подходов имеет ограниченную применимость при использовании в центральном процессоре. Действительно, существует множество альтернатив традиционному подходу с последовательным исполнением программ: модели SIMD в стиле GPU, модели в стиле абстрактной машины Erlang и другие.

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

Модель независимых акторов лучше использовать на более высоком уровне, в большом кластере, где каждый узел — всё та же высокопроизводительная машина с традиционным процессором. Erlang прекрасно работает в кластере, но эффективную quick sort или быструю хеш-таблицу на нём сделать трудно.

Не сомневаюсь, что любые достаточно общие подходы к вычислениям будут включены в популярные процессоры. Между тем, современные x86-совместимые процессоры давно уже включают в себя наборы векторных инструкций, схожие с GPU по назначению и принципам работы, но сохраняющие общую схему процессора в стиле фон Неймана в целом.

Под такие неординарные железки действительно имеет смысл разрабатывать языки с особой семантикой. Есть такое авторитетное мнение: будущее за специализированными программируемыми ускорителями. Но компьютер общего назначения был и остаётся похожим на ту самую PDP-11, для которой так хорошо подходят C-подобные императивные языки.

Он пишет, что для обеспечения скорости программ на C процессоры имитируют абстрактную машину C (и давно забытую PDP-11), после чего указывает на ограниченность такой машины. В статье Чизнэлла есть фундаментальное противоречие. Но я не понимаю, почему это означает, что «C — не низкоуровневый язык».

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

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

Но, судя по всему, кое-что создатели языка всё же сделали правильно. При всём при этом у C хватает недостатков: архаичная библиотека функций, запутанный и противоречивый стандарт, грубые ошибки в дизайне.

И пускай C устарел, но, видимо, любому его преемнику всё равно придётся отталкиваться от тех же самых принципов. Так или иначе, нам по-прежнему нужен язык низкого уровня, причём построенный именно для популярных фоннеймановских компьютеров.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть