Почему стандартные библиотеки не являются примитивами языка программирования? [закрыто]

30

Я думал о том, почему существуют (во всех изученных мною языках программирования, таких как C ++, Java, Python) стандартные библиотеки, такие как stdlib, вместо того, чтобы иметь подобные «функции», являющиеся примитивом самого языка.

Симона Бройли
источник
4
Что вы имеете в виду «почему компилятор не может просто перевести вызов функции в набор инструкций»? Это примерно то, что делает компилятор, стандартная библиотека или нет (хорошо, Python только наполовину и Java в JVM байт-код; похожая концепция). Стандартные библиотеки на самом деле не имеют ничего общего с компиляцией кода -> инструкциями.
Делиот
25
@ Delioth Я думаю, что Симона спрашивает, почему не все в стандартной библиотеке языка $ LANG вместо этого является примитивной конструкцией / функцией этого языка. Я бы сказал, что это разумный вопрос для тех, кто
Андрес Ф.
33
Стандартная библиотека, как правило, заполняет пробел между рабочим языком программирования и полезным, который люди будут использовать.
Теластин
6
Значительная часть стандартной библиотеки Python фактически написана на C и уже скомпилирована.
ElmoVanKielmo
1
В отличие от этого, в большинстве реализаций BASIC все является частью языка, и в них вообще нет ни библиотек, ни их поддержки (за исключением нескольких реализаций для возможности вызова подпрограмм машинного языка).
Евро Мицелли

Ответы:

32

Позвольте мне немного подробнее рассказать о хорошем ответе @ Vincent (+1) :

Почему компилятор не может просто перевести вызов функции в набор инструкций?

Это можно сделать через два механизма:

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

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

Тем не менее, несмотря на это, иногда лучшим вариантом для компилятора является перевод вызова функции на исходном языке в вызов функции в машинном коде. Рекурсия, виртуальные методы и размер - некоторые причины, по которым встраивание не всегда возможно / практично. (Другая причина - намерение сборки, например, отдельная компиляция (объектные модули), отдельные модули загрузки (например, DLL)).

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

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

Эрик Эйдт
источник
3
Хороший ответ. Следует добавить несколько слов, почему было принято это решение в C: если я правильно помню, главная причина была в том, что это облегчало создание компиляторов C для многих различных аппаратных архитектур.
Док Браун
10
@DocBrown К 1975 году было достаточно примеров в области разработки языка программирования (ALGOL-68, кто-нибудь?), Которые показали, что попытки втирать все в язык напрямую приводили к значительным замедлениям как в окончании языковых спецификаций, так и в в производстве языковых реализаций.
Joker_vD
5
Подобный пример - то, что сделал Python print: в 2.x это был оператор со своей собственной специальной грамматикой, но в 3.x он стал просто еще одним вызовом функции. См. PEP 3105 для официального объяснения.
Ден04
1
@DocBrown, мобильность почти наверняка не была причиной. Когда Unix и C были созданы, они были спроектированы и построены для одной машины, запасного PDP-7, поскольку Кен Томпсон поинтересовался, какие концепции могут быть спасены от неудачного проекта Multics. C также был создан по одной причине: иметь язык высокого уровня, чтобы (пере) реализовывать Unix. Они - это в основном эксперимент в разработке программного обеспечения, а не серьезная попытка создания коммерческой мультиплатформенной ОС и языка. См. Например, bell-labs.com/usr/dmr/www/chist.html .
Евро Мицелли
@EuroMicelli: я не вижу там противоречия. И ваша ссылка содержит много деталей о том, когда переносимость стала важной, это было на самом деле в первые годы разработки на C и Unix. Я могу только догадываться здесь, но если бы изобретатели C не сохранили язык преднамеренно маленьким, я думаю, было бы весьма маловероятно, что они могли бы перенести его так быстро и успешно на множество различных архитектур.
Док Браун
70

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

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

Винсент Рамдани
источник
3
Не всегда. PHPв качестве примера вряд ли можно различить его обширные языковые функции и сам язык.
Вахид Амири
15
Я не взял бы PHP в качестве примера простого языка
DrBreakalot
3
@DrBreakalot PHP - чрезвычайно простой язык. Это не значит, что он имеет последовательный дизайн, но это другая проблема.
Легкость гонки с Моникой
19
@LightnessRacesinOrbit Я бы даже не назвал PHP «простым»: он имеет объектную систему на основе классов, отдельный набор «примитивных значений», автономные функции, первоклассные замыкания, построенные на объектной системе, механизм пространства имен, различные понятия называемых «статический», высказывание, а также выражения, include, requireи require_once, если / для / While (структурного программирования), исключение, отдельная система «значений ошибок», осложненная слабых правила типизации, осложненное правило оператора старшинства, и так далее , Сравните это с простотой, скажем, Smalltalk, Scheme, Prolog, Forth и т.
Д .
3
Основная причина, на которую намекают, но которая явно не указана в этом ответе, заключается в том, что, сохраняя язык настолько простым, насколько это возможно, его гораздо проще реализовать на других платформах. Поскольку стандартные библиотеки обычно пишутся на самом языке , их можно легко перенести.
BlueRaja - Дэнни Пфлюгофт
34

В дополнение к тому, что уже сказано в других ответах, введение стандартных функций в библиотеку - это разделение задач :

  • Работа компилятора - анализировать язык и генерировать для него код. Задача компилятора - не содержать ничего, что уже может быть написано на этом языке и предоставлено в виде библиотеки.

  • Это стандартная библиотека (которая всегда доступна неявно) для обеспечения основной функциональности, которая нужна практически всем программам. Это не работа стандартной библиотеки - содержать все функции, которые могут быть полезны.

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

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

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

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

  • Библиотека потоков бесполезна на одноядерном встроенном контроллере. Разрешение языковой реализации для этого встроенного контроллера просто не включать pthreadбиблиотеку - это просто правильная вещь.

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

  • Даже базовая стандартная библиотека ничего не стоит, когда вы программируете ядро. Вы не можете реализовать write()без системного вызова в ядре, и вы не можете реализовать printf()без write(). Как программист ядра, ваша задача - предоставить write()системный вызов, вы не можете просто ожидать, что он будет там.

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

Конечно, языки высокого уровня, такие как python и java, могут сделать много предположений относительно своей среды. И они имеют тенденцию включать много, много вещей в свои стандартные библиотеки. Языки более низкого уровня, такие как C, предоставляют гораздо меньше в своих стандартных библиотеках и значительно уменьшают базовую стандартную библиотеку. Вот почему вы найдете работающий компилятор C практически для любой архитектуры, но, возможно, не сможете запустить на нем никаких сценариев Python.

cmaster
источник
16

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

Эта модульность дает вам ряд преимуществ:

  • Это сводит к минимуму объем работы, необходимый для поддержки новой аппаратной платформы, поскольку большая часть кода стандартной библиотеки не зависит от аппаратного обеспечения и может использоваться повторно.
  • Реализация стандартной библиотеки может быть оптимизирована различными способами (по скорости, по пространству, по использованию ресурсов и т. Д.). Во многих ранних вычислительных системах был доступен только один компилятор, а наличие отдельной стандартной библиотеки означало, что разработчики могли менять реализации в соответствии со своими потребностями.
  • Функциональность стандартной библиотеки даже не должна существовать. Например, при написании чистого C-кода у вас есть полнофункциональный компилятор, но большая часть стандартных функций библиотеки отсутствует, а некоторые вещи, такие как файловый ввод-вывод, даже невозможны. Если для реализации этой функциональности требовался компилятор, то на некоторых платформах, где он вам нужен больше всего, не было бы компилятора C, соответствующего стандартам.
  • В ранних системах компиляторы часто разрабатывались компанией, которая проектировала аппаратное обеспечение. Стандартные библиотеки часто предоставлялись поставщиком ОС, поскольку им часто требовался доступ к функциям (например, к системным вызовам), специфичным для этой программной платформы. Для автора компилятора было непрактично поддерживать все разные комбинации аппаратного и программного обеспечения (раньше было гораздо больше разнообразия как в аппаратной архитектуре, так и в программной платформе).
  • На языках высокого уровня стандартная библиотека может быть реализована в виде динамически загружаемой библиотеки. Одна реализация стандартной библиотеки может затем использоваться несколькими компиляторами и / или языками программирования.

Исторически говоря (по крайней мере, с точки зрения C), исходные версии языка до стандартизации вообще не имели стандартной библиотеки. Поставщики ОС и третьи стороны часто предоставляют библиотеки, полные обычно используемых функций, но разные реализации включают разные вещи, и они в значительной степени несовместимы друг с другом. Когда C был стандартизирован, они определили «стандартную библиотеку» в попытке согласовать эти несопоставимые реализации и улучшить переносимость. Стандартная библиотека C разработана отдельно от языка, как библиотеки Boost для C ++, но позже была интегрирована в спецификацию языка.

ВТА
источник
6

Дополнительный ответ: «Интеллектуальная собственность»

Примечательным примером является реализация Math.Pow (double, double) в .NET Framework, который был приобретен Microsoft у Intel и остается нераскрытым, даже если среда стала открытой. (Если быть точным, в приведенном выше случае это внутренний вызов, а не библиотека, но идея верна). Библиотека, отделенная от самого языка (теоретически также подмножество стандартных библиотек), может дать сторонникам языка больше гибкости при рисовании грань между тем, что должно быть прозрачным, и тем, что должно оставаться нераскрытым (из-за их договоров с третьими сторонами или по другим причинам, связанным с ИС)

miroxlav
источник
Это смущает. На странице, на которую вы ссылаетесь Math.Pow, не упоминается ни о покупке, ни о Intel, и рассказывается о людях, читающих исходный код реализации функции.
Легкость гонки с Моникой
@LightnessRacesinOrbit - хм, я все еще могу видеть это там (при поиске «intel»). Вы также можете найти ссылку на недавний исходный код (в самых последних комментариях), а также альтернативную реализацию (во втором ответе), которая является общедоступной, но ее сложность и неэффективность с комментариями дают подсказку, почему оригинальная реализация до сих пор не раскрыта. Для действительно эффективной реализации может потребоваться глубокое знание многих деталей на уровне ЦП, которые не обязательно доступны в открытом доступе.
miroxlav
5

Ошибки и отладка.

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

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

Питер Б
источник
5

Это отличный вопрос!

Уровень развития

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

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

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

Зачем?

Давайте рассмотрим идею переноса некоторой части функциональности из стандартной библиотеки в компилятор.

Преимущества:

  • Лучшая диагностика: внутренние характеристики могут быть специальными.
  • Лучшая производительность: встроенные функции могут быть в специальном корпусе.

Недостатки:

  • Увеличение массы компилятора: каждый особый случай добавляет сложности компилятору; Сложность увеличивает затраты на техническое обслуживание и вероятность ошибок.
  • Более медленная итерация: изменение реализации функциональности требует изменения самого компилятора, что усложняет создание небольшой библиотеки (вне std) для экспериментов.
  • Более высокий барьер для входа: чем дороже / сложнее что-то изменить, тем меньше людей могут прыгнуть.

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

Матье М.
источник
5

Как сам языковой дизайнер, я хотел бы повторить некоторые другие ответы здесь, но предоставить их глазами кого-то, кто создает язык.

API не закончен, когда вы закончите добавлять все, что можете в него. API заканчивается, когда вы берете все, что можете из него.

Язык программирования должен быть указан с использованием некоторого языка. Вы должны быть в состоянии передать значение любой программы, написанной на вашем языке. На этом языке очень трудно писать, и еще труднее писать хорошо. В целом, это, как правило, очень точная и хорошо структурированная форма английского языка, используемая для передачи значения не компьютеру, а другим разработчикам, особенно тем разработчикам, которые пишут компиляторы или интерпретаторы для вашего языка. Вот пример из спецификации C ++ 11, [intro.multithread / 14]:

Видимая последовательность побочных эффектов на атомарном объекте M в отношении вычисления значения B для M представляет собой максимальную непрерывную подпоследовательность побочных эффектов в порядке модификации M, где первый побочный эффект виден относительно B и для каждого побочного эффекта, это не тот случай, когда B происходит до него. Значение атомарного объекта M, как определено оценкой B, должно быть значением, сохраненным некоторой операцией в видимой последовательности M относительно B. [Примечание: можно показать, что видимая последовательность побочных эффектов значения вычисления являются уникальными, учитывая требования согласованности ниже. —Конечная записка]

Блек! Любой, кто попробовал понять, как C ++ 11 обрабатывает многопоточность, может понять, почему формулировка должна быть настолько непрозрачной, но это не прощает того факта, что она ... ну ... такая непрозрачная!

Сравните это с определением std::shared_ptr<T>::resetв разделе библиотеки стандарта:

template <class Y> void reset(Y* p);

Эффекты: эквивалентноshared_ptr(p).swap(*this)

Так в чем же разница? В части определения языка авторы не могут предполагать, что читатель понимает языковые примитивы. Все должно быть тщательно указано в английской прозе. Как только мы дойдем до части определения библиотеки, мы можем использовать язык для определения поведения. Это часто намного проще!

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

И мы действительно видим некоторые размытые линии:

  • В Java, java.lang.ref.Reference<T>может только быть подклассы по стандартной библиотеки классов java.lang.ref.WeakReference<T> java.lang.ref.SoftReference<T>и java.lang.ref.PhantomReference<T>потому , что поведение Referenceтак глубоко переплетены со спецификацией языка Java , что им нужно , чтобы поставить некоторые ограничения в части этого процесса реализуется как «стандартная библиотека» классов.
  • В C # есть класс System.Delegate, который инкапсулирует концепцию делегатов. Несмотря на название, он не является делегатом. Это также абстрактный класс (не может быть создан), из которого нельзя создавать производные классы. Только система может сделать это с помощью функций, записанных в спецификации языка.
Корт Аммон - Восстановить Монику
источник
2

Это подразумевается как дополнение к существующим ответам (и это слишком долго для комментария).

Есть как минимум две другие причины для стандартной библиотеки:

Барьер для входа

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

Если вы идете по этому пути, вы отсекаете себя от множества потенциальных участников.

Горячая загрузка кода

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

Джаред Смит
источник
3
«Любой уважающий себя компилятор должен быть самодостаточным», - вовсе нет. Было бы бессмысленно иметь версии, скажем, LLVM, написанные на C, C ++, Objective-C, Swift, Fortran и так далее, для компиляции всех этих языков.
gnasher729
@ gnasher729 - это не особый случай (наряду с другими мультиязычными целями, такими как CLR)?
Джаред Смит
@JaredSmith Я бы сказал, что сейчас это общий случай, а не особый. Никто больше не пишет «компилятор» как монолитное приложение. Вместо этого они производят системы компиляции . Большая часть функциональности полного компилятора полностью не зависит от конкретного языка, который компилируется, и большая часть зависящей от языка части может быть выполнена путем предоставления различных данных, определяющих грамматику языка, а не путем написания разного кода для каждого языка, который вы хочу скомпилировать.
alephzero
2

Это интересный вопрос, но уже дано много хороших ответов, поэтому я не буду пытаться дать полный.

Тем не менее, две вещи, которые я не думаю, получили достаточно внимания:

Во-первых, все это не супер четко. Это что-то вроде спектра, потому что есть причины действовать по-другому. Например, компиляторы часто знают о стандартных библиотеках и их функциях. Пример примера: функция «Hello World» на C - printf - лучшая из тех, что я могу себе представить. Это библиотечная функция, вроде как, так как она очень зависит от платформы. Но это поведение (определяемое реализацией) должно быть известно компилятору, чтобы предупредить программиста о неправильных вызовах. Это не особенно опрятно, но было замечено как хороший компромисс. Кстати, это реальный ответ на большинство вопросов «почему этот дизайн»: много компромиссов и «в то время казалось хорошей идеей». Не всегда "это был ясный способ сделать это" или "

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

Со стандартной моделью библиотеки (и поддержкой потоков в ней) это можно сделать аккуратно: компилятор почти такой же, вы можете повторно использовать биты применяемых библиотек и все, что не можете удалить. Если это запекается в компиляторе, вещи начинают запутываться.

Например:

  • Вы не можете быть совместимым компилятором.

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

Аналогичные проблемы возникают, если вы хотите настроить или расширить функциональность «библиотеки». Это гораздо чаще, чем вы думаете. Просто придерживайтесь многопоточности: Windows, Linux и некоторые экзотические сетевые процессоры все делают потоки совершенно по-разному. Хотя биты linux / windows могут быть довольно статичными и могут использовать идентичный API, материал NPU будет меняться в зависимости от дня недели и API с ним. Компиляторы быстро отклонятся, когда люди решат, какие биты они должны поддерживать / могли бы делать с ними достаточно быстро, если бы не было способа разделить подобные вещи.

drjpizzle
источник