Не так давно издательство O’Reilly выпустило книгу «21st Century C: C Tips from the New School», которая собрала на Amazon.com довольно противоречивые отзывы. Поэтому я решил написать исключительно оптимистичный материал о «древнем языке» C и предлагаю обсудить его настоящее (а если желаете — прошлое и будущее).
Много лет я всеми правдами и неправдами пытался расстаться с языком C. Слишком прост по составу, требуется управлять множеством деталей, слишком старый и неработоспособный, на первый взгляд — слишком низкоуровневый. Я крутил бурные и страстные интрижки с Java, C++ и Erlang. На всех этих языках у меня есть выполненные проекты, которыми я горжусь, но все эти языки в свое время разбили мне сердце. Они сулили что-то и не сдерживали обещаний, создавали культуры, в которых ты сосредотачиваешься не на том, на чем следует, а также требовали идти на катастрофические компромиссы, которые не оборачивались ничем, кроме горестных разочарований. И я на коленях приползал обратно к C.
Язык C универсален. Это единственный язык, который может одновременно похвастаться и высокой продуктивностью и исключительной скоростью, превосходным инструментарием для любых областей, огромным сообществом разработчиков, высокопрофессиональной культурой. Да, не без компромиссов — но этот язык их не стыдится.
На других языках вы, возможно, быстрее напишете работоспособное решение, но в долгосрочной перспективе, когда речь зайдет о производительности и надежности, именно C избавит вас от массы головных болей. Я усвоил этот урок на собственном горьком опыте.
Простота и выразительность
С — поразительно высокоуровневый язык. Да, я не оговорился, и повторю еще раз: C — фантастически высокоуровневый язык. Конечно, не такой высокоуровневый, как Java или C#, и далеко не такой, как Erlang, Python или JavaScript. Но он не менее высокоуровневый, чем C++, зато гораздо, гораздо проще. Разумеется, C++ предлагает больше абстракций, но эти абстракции недалеко ушли от C. Работая с C++, вы все равно должны в совершенстве знать C, а также еще кое-какую смешную дребедень.
Если кто-то вам скажет: «Мне нужен язык программирования, на котором мне всего лишь достаточно сказать, чего я хочу — и все будет готово», дайте ему карамельку».
Алан Дж. Перлис
Сложно вспомнить какие-нибудь низкоуровневые языки, которые могли бы заменить C на практике. Это объясняется тем, что C так чертовски успешно абстрагирует свойства той машины, на которой работает, и делает это на таком высоком уровне, что остальные низкоуровневые языки просто не нужны, если есть C. Язык C действительно хорош в том, для чего предназначается.
Синтаксис и семантика C удивительно сильны и выразительны. На этом языке удобно судить и о высокоуровневых алгоритмах, и о низкоуровневом оборудовании. Семантика C настолько проста, а синтаксис так удобен, что значительно упрощается чисто когнитивная сторона работы. Программист может сосредоточиться на том, что действительно важно.
Язык C задал планку для по-настоящему хорошего низкоуровневого языка и избавился от всего, что этой планке не соответствует. Таким образом, С полностью перевернул представление о том, что такое «качественный низкоуровневый язык». Черт возьми, это впечатляет!
Более простой код, более простые типы
C — статически типизированный язык со слабым контролем типов, его система типов довольно проста. В отличие от C++ или Java, здесь нет классов, в которых требуется определять всевозможные новые варианты поведения типов, действующие во время исполнения. Вы работаете практически только со структурами и объединениями, и все вызывающие элементы должны предельно точно описывать, как они используют типы.
«Даром» вызывающая сторона не получает почти никакой информации.
«Вы хотели банан, но оказались наедине с гориллой, которая держит этот банан. А вокруг вас — джунгли».
Джо Армстронг
Иногда кажущаяся слабость оказывается преимуществом. Именно такова ситуация с API языка C: их «поверхностная область» кажется очень маленькой и простой. Вместо массивных фреймворков здесь делается ставка на хорошо прослеживаемые тенденции и культуру создания компактных библиотек, которые представляют собой легковесные абстракции простых типов.
Сравните это с объектно-ориентированными языками, где в базах кода зачастую развиваются огромные взаимозависимые интерфейсы из сложных типов, где аргументы и возвращаемые типы оказываются сложнее. Такая сложность становится фрактальной: каждый тип является классом, определенным в контексте методов с аргументами и возвращаемыми типами, которые, в свою очередь, также могут быть сложными возвращаемыми типами.
Конечно, объектно-ориентированные системы типов не провоцируют такую фрактальную сложность сами по себе, но они повышают вероятность ее возникновения. В объектно-ориентированной среде проще пойти по неверному пути. C также не исключает возможности свернуть на кривую дорожку, но сделать это сложнее. В C обычно создаются более простые и неглубокие типы с меньшим количеством зависимостей. Они проще для понимания и для отладки.
Самый быстрый
С — самый быстрый из современных языков: как на микроуровне, так и в масштабах целого стека. И это не просто самый быстрый язык на уровне среды времени исполнения, он последовательно демонстрирует максимальную эффективность в потреблении памяти и в скорости запуска. Вам просто нужно выбрать, что для вас важнее — пространство или время. C не скрывает от вас никаких деталей, здесь просто судить и о первом, и о втором.
«Если вы пытаетесь перехитрить компилятор, то практически сводите на нет его пользу».
Керниган и Плоджер, The Elements of Programming Style
Всякий раз, когда вам рассказывают о том, что на высокоуровневом языке, например, Java или Haskell, удалось достичь производительности «почти как на C» — изучите детали. В лучшем случае это окажется плоской шуткой. Таким «обгонятелям» приходится вытворять неуклюжие обратные сальто, пользоваться специальными знаниями об «умных» компиляторах и внутренних механизмах виртуальной машины, чтобы достичь сравнимой скорости. Обычно такие эксперименты приводят к тому, что простая и выразительная природа языка тонет в мудреных оптимизациях, специфичных для конкретных версий. Выигрыш в скорости обычно получается минимальным.
Если вы пишете на C программу, которая должна работать быстро, вы знаете, почему она получится быстрой. Такая программа не будет существенно замедляться при использовании различных компиляторов и сред исполнения, как это происходит при использовании виртуальной машины, экспериментах с настройками сборщика мусора (они могут радикально влиять на производительность и длительность пауз). Не случится и такого, что срабатывание какого-то фрагмента кода в приложении коренным образом изменит профиль сборки мусора во всей остальной программе.
Способы оптимизации в C обычно просты и незатейливы. Конечно, встречаются сравнительно сложные случаи, но в таких ситуациях в вашем распоряжении будет масса профилировочных инструментов, которые помогут разобраться с проблемой без необходимости изучать подноготную виртуальной машины или «достаточно умный компилятор». При работе с профилировочными инструментами для процессора, памяти и ввода-вывода язык C подойдет идеально, так как вы будете четко видеть, что именно происходит. C сохраняет лидирующие позиции в скорости как на микроуровне, так и во всем стеке.
Ускоренные циклы сборки-запуска-отладки
Эффективность и продуктивность труда разработчика исключительно сильно зависят от цикла «сборка-запуск-отладка». Чем быстрее этот цикл, тем более интерактивна сама разработка и тем дольше вы остаетесь в движении, тратя время именно на решение задачи. C обладает наивысшей интерактивностью разработки среди всех широко распространенных статически типизированных языков.
«Оптимизм — это профессиональная болезнь всего программирования. Обратная связь — это лекарство от этой болезни».
Кент Бек
Поскольку цикл сборки, запуска и отладки не является чертой, присущей самому языку, а больше зависит от сопутствующего инструментария, именно этот цикл зачастую остается без внимания. Тем не менее, влияние данного цикла на продуктивность сложно переоценить. К сожалению, эта тема не затрагивается в большинстве дискуссий о языках программирования, речь идет о самих строках кода, а также о записываемости/читаемости исходного кода. Но реальность такова, что и по инструментальному обеспечению, и по интерактивности этот цикл в языке C гораздо быстрее, чем в любом сравнимом языке.
Вездесущие отладчики и удобные аварийные дампы
Практически в любой системе, куда вам может потребоваться портировать программу, найдутся готовые отладчики C и инструменты для аварийных дампов. Это бесценные вещи, если нужно быстро найти источник проблем. А проблемы будут.
«Ошибка, отсутствует клавиатура — для продолжения нажмите F1».
При работе с любым другим языком под рукой может не оказаться удобного отладчика, а иногда — хорошего инструмента для аварийного дампирования. Более чем вероятно, что вам когда-либо придется программировать те или иные сложные процессы, код для которых стыкуется с кодом на C. В таком случае, вам может потребоваться отладить интерфейс между кодом на C и другим языком. Контекст при этом зачастую почти отсутствует, работа становится тягостной, чреватой ошибками, а результат бывает совершенно неприменим на практике.
Если же код написан только на C, то вы сможете вызывать стеки, переменные, аргументы, локальные поточные переменные и глобальные переменные — практически все, что встречается в памяти. Это поразительно полезно, особенно если приходится решать проблему, которая усугублялась уже несколько дней в долгоживущем серверном процессе, а как-либо иначе воспроизвести ее невозможно. Если вы потеряете такой контекст в высокоуровневом языке — я вам не завидую.
Вызывается отовсюду
В языке C есть стандартизированный бинарный интерфейс приложений (ABI), поддерживаемый всеми существующими операционными системами, языками и платформами. Он не требует ни среды времени исполнения, ни каких-либо иных неизбежных издержек. Таким образом, код, написанный вами на C, полезен не только для вызывающих элементов из C, но и для любой мыслимой библиотеки, языка или среды, какие только существуют в природе.
«Ключ к портируемости — немногочисленные концепции и полное описание».
Дж. Пальме
Код на C можно использовать в автономных исполняемых файлах, скриптовых языках, коде ядра, коде прошивок, в качестве DLL и даже вызывать из SQL. Это лингва франка системного программирования и подключаемых библиотек. Если вы хотите написать какой-то код только однажды и обеспечить его используемость в большинстве сред и ситуаций, то C — единственный разумный выбор.
Да, у него есть и недостатки
В C много «недостатков». В нем нет контроля границ, в памяти легко что-нибудь повредить, встречаются висящие указатели и утечки памяти/ресурсов, поддержка параллелизма прикручена кое-как, нет модулей и пространств имен. Обработка ошибок может быть страшно затруднительной и пространной. Легко допустить целый класс ошибок, о которые разбивается стек вызовов, а ваш процесс захватывается зловредным вводом. Замыкания? Смешно!
«Если ничего не получается, прочтите инструкцию».
Л. Ласельо
Недостатки C очень хорошо известны, и это радует. Во всех языках и реализациях есть свои сбои и проблемы. Язык C просто их не скрывает. Существует масса статических и динамических инструментов, позволяющих справляться с наиболее распространенными и опасными ошибками в этом языке. Тот факт, что некоторые наиболее востребованные и надежные программы в мире написаны именно на C, доказывает, что недостатки языка преувеличены, ошибки легко отслеживать и устранять.
Как-то раз в Couchbase нам довелось потратить более двух человеко-месяцев работы, чтобы справиться со сбоем в виртуальной машине Erlang. У нас ушла уйма времени на то, чтобы отследить одну проблему, которая коренилась в самом ядре реализации Erlang. Сначала мы вообще не понимали, что происходит и почему, думали, что ошибка в нашем подключаемом коде на C, безуспешно пытались ее найти и исправить. А оказалось, что в ядре Erlang есть баг, вызывающий условия гонки. Такая фундаментальная проблема возможна в любом языке.
Сначала, из соображений производительности, мы стали все активнее переписывать код Couchbase на C, именно на этом языке реализовали несколько новых фич. Но когда мы столкнулись с проблемами, оказалось, что их совсем несложно отлаживать и исправлять. Итак, в долгосрочной перспективе язык C более продуктивен.
Где-то в подсознании я желаю сделать язык C немного лучше. Просто сгладить некоторые острые края и исправить кое-какие вопиющие проблемы. Но, наверное, не удастся исправить все сверху донизу — синтаксис, семантику, инструментарий. Возможно, это и не стоит усилий. На сегодняшний день язык C непостижимо эффективен, и ситуация вряд ли изменится в обозримом будущем.
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.