awesome-everything EN
↑ Обратно к восхождению

Базовый CS с нуля

Зачем нужны языки высокого уровня

Суть Ассемблер читаем, но всё ещё зависит от CPU и многословен. Язык высокого уровня позволяет одному оператору соответствовать многим машинным инструкциям, переносим между разными CPU и куда более продуктивен. Суть — в абстракции.
◷ 18 min

Язык ассемблера сделал жизнь терпимой. Вместо написания сырых битовых паттернов ты пишешь мнемоники вроде LOAD R0, 200 и позволяешь ассемблеру их транслировать. Но у ассемблера есть две глубокие проблемы, которые становятся болезненными, как только программы начинают расти.

Проблема 1: ассемблер зависит от CPU. Программа, написанная для процессора x86-64, не может работать на процессоре ARM без полной переработки на ассемблере ARM. Как только появляется другое железо, вся кодовая база оказывается бесполезной. В 1950-х годах, когда IBM выпускала новую модель компьютера, каждую программу приходилось переписывать с нуля. Наука и бизнес порождали огромные объёмы вычислений — модели погоды, платёжные ведомости, траектории ракет — и переписывать одну и ту же программу каждый раз при появлении новой машины было невозможно.

Проблема 2: ассемблер многословен и близок к железу. Сложение двух чисел на ассемблере выглядит так (для типичного CPU): загрузить первое число в регистр, загрузить второе в другой регистр, сложить регистры, сохранить результат. Четыре инструкции. В математике всё это предложение записывается как c = a + b. Для программы, решающей систему дифференциальных уравнений, разрыв между математическим замыслом и кодом ассемблера составляет тысячи строк. Поддерживать и проверять такой код крайне сложно.

Ответом стал новый вид языка — разработанный для людей, а не для CPU — называемый языком высокого уровня.

Цель

После этого урока ты сможешь объяснить две главные проблемы ассемблера (зависимость от CPU и многословность), описать, как язык высокого уровня решает обе проблемы, определить, что означает абстракция в этом контексте, и объяснить, зачем нужен транслятор для преодоления разрыва между языком высокого уровня и машинным кодом.

1

Что делает язык «высокоуровневым»? Слово высокий относится к уровню абстракции от железа. Чем выше уровень, тем дальше ты от реальных двоичных операций CPU и тем ближе к тому, как люди естественным образом думают о задачах.

Представь уровни как ступени лестницы:

  • Машинный код (нижний уровень): сырые битовые паттерны, которые CPU исполняет напрямую. Никакой человекочитаемости вообще.
  • Язык ассемблера: одна мнемоника на машинную инструкцию. Человекочитаемые имена для битовых паттернов, но по-прежнему одна инструкция за раз, по-прежнему зависит от CPU.
  • Язык высокого уровня (выше): один оператор может соответствовать многим машинным инструкциям. Переносим между разными CPU. Разработан вокруг человеческих концепций, а не операций CPU.

Примеры языков высокого уровня: C, C++, Java, Python, TypeScript, Rust, Go — по сути, все языки, о которых ты слышал, кроме ассемблера. Когда кто-то говорит «язык программирования», он почти всегда имеет в виду язык высокого уровня.

2

Один оператор — многие машинные инструкции. В языке высокого уровня одна строка исходного кода может соответствовать десяткам — или сотням — машинных инструкций после трансляции. Язык берёт на себя всю тяжёлую работу по выбору нужных инструкций.

Рассмотрим вычисление площади круга, area = 3.14159 * radius * radius, на языке высокого уровня в сравнении с ассемблером. На языке высокого уровня ты пишешь именно это выражение. Компилятор (программа-транслятор) порождает машинные инструкции для: загрузки константы 3.14159 в регистр с плавающей точкой, загрузки radius из его адреса в памяти в другой регистр, выдачи инструкции умножения с плавающей точкой, повторного умножения, сохранения результата по адресу area. Точная последовательность зависит от CPU, и тебе никогда не нужно об этом думать. Ты выразил математический замысел; компилятор создал механизм на уровне машины.

Это сжатие «многие к одному» — главный выигрыш в производительности. Команда Джона Бэкуса в IBM сообщала, что переход с ассемблера на FORTRAN повысил производительность программистов примерно в 20 раз для научных программ — та же научная работа, на которую уходили недели на ассемблере, выполнялась на FORTRAN за дни.

3

Переносимость: один исходный файл — многие CPU. Поскольку язык высокого уровня не привязан к системе команд какого-либо конкретного CPU, один и тот же исходный код может быть транслирован разными трансляторами для разного железа. Ты пишешь area = 3.14159 * radius * radius один раз; транслятор для x86-64 превращает это в машинные инструкции x86-64, а транслятор для ARM превращает тот же источник в машинные инструкции ARM. Программист не меняет исходный код.

Это и есть переносимость: свойство программы, написанной в одном месте, работать на разном железе без переписывания. Переносимость — вот почему один и тот же код TypeScript, который ты пишешь на ноутбуке с macOS (процессор ARM), можно развернуть на Linux-сервере (процессор x86-64) без каких-либо изменений в исходном коде — какой-то транслятор в цепочке инструментов обрабатывает разницу.

Переносимость требует, чтобы слой трансляции — компилятор или интерпретатор — был написан отдельно для каждого целевого CPU. Но эта работа делается один раз разработчиками языка и используется всеми пользователями языка.

Почему это работает

Почему люди не использовали языки высокого уровня с самого начала? Написать первый компилятор было само по себе трудно. В начале 1950-х годов многие эксперты всерьёз считали, что ни одна программа автоматической трансляции не сможет породить машинный код такой же эффективный, как код опытного программиста на ассемблере. Джон Бэкус и его команда в IBM доказали обратное с FORTRAN в 1957 году — компилятором, производящим код почти такой же быстрый, как написанный вручную ассемблер. Сопротивление было столь же психологическим, сколь и техническим: доверить машине трансляцию своей программы казалось рискованным. Как только FORTRAN продемонстрировал, что сгенерированный код достаточно хорош, принятие произошло быстро.

4

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

Когда ты пишешь area = radius * radius * 3.14159, ты работаешь на уровне математических концепций. Ты не думаешь о том, в каком регистре хранится radius, использует ли умножение целочисленный или блок с плавающей точкой, и сколько тактов занимает инструкция умножения. Эти детали существуют — CPU с ними разберётся — но язык высокого уровня скрывает их от тебя, чтобы ты мог думать о том, что вычисляется, а не о том, как машина это выполняет.

Это не единственное использование абстракции в вычислительной технике — ты встретишь её снова и снова при изучении функций, структур данных, операционных систем и сетей. В каждом случае паттерн одинаков: скрой механизм, обнажи концепцию.

5

Цена абстракции: нужен транслятор. Язык высокого уровня — это не то, что понимает CPU. CPU по-прежнему выполняет только машинный код. Это означает, что каждая программа на языке высокого уровня должна быть транслирована — так или иначе, в какой-то момент — в машинный код целевого CPU, прежде чем она сможет работать.

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

Частая ошибка

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

area = r * r * π
высок. уровень
компилятор
FLOAD Fr0, r
asm 1
FMUL Fr0, Fr0
asm 2
FLOAD Fr1, π
asm 3
FMUL Fr0, Fr1
asm 4
FSTORE area, Fr0
asm 5
Один оператор высокого уровня (выделено синим) разворачивается в пять инструкций уровня ассемблера (выделено зелёным) после трансляции. CPU выполняет пять машинных инструкций; программист написал только одну строку.
Разбор примера

Подсчёт машинных инструкций за одним оператором высокого уровня.

Рассмотрим эту строку TypeScript:

const total = price * quantity + shipping;

Предположим, что price, quantity и shipping — 64-битные числа с плавающей точкой, уже находящиеся в памяти. Упрощённая трансляция в инструкции нашего игрушечного стиля:

FLOAD  Fr0, addr(price)     ; загрузить price в регистр с плавающей точкой Fr0
FLOAD  Fr1, addr(quantity)  ; загрузить quantity в Fr1
FMUL   Fr0, Fr1             ; Fr0 = price * quantity
FLOAD  Fr1, addr(shipping)  ; загрузить shipping в Fr1 (повторное использование)
FADD   Fr0, Fr1             ; Fr0 = (price * quantity) + shipping
FSTORE addr(total), Fr0     ; сохранить результат в памяти по адресу 'total'

Шесть машинных инструкций из одной строки TypeScript. На реальном CPU x86-64 количество и точная форма будут другими, но соотношение строк высокого уровня к машинным инструкциям обычно составляет от 1:5 до 1:100 в зависимости от того, что делает строка.

Обрати внимание: ты как программист TypeScript не выбирал ни одну из этих инструкций, ни одно имя регистра, ни один адрес в памяти. Со всем этим справился компилятор. Язык высокого уровня позволил выразить математический замысел; компилятор создал механизм.

Практика 0 / 5

Язык ассемблера зависит от CPU. Если ты написал программу на ассемблере для x86-64 и хочешь запустить её на CPU ARM, что нужно сделать? Введи 1 (переписать на ARM-ассемблере) или 2 (запустить напрямую — всё заработает).

Оператор языка высокого уровня 'area = r * r * pi' соответствует скольким машинным инструкциям — больше чем одной или ровно одной? Введи 1 (больше одной) или 0 (ровно одна).

Переносимость означает, что один и тот же исходный код может работать на разных CPU без переписывания. Какой слой должен быть написан отдельно для каждого целевого CPU, чтобы это стало возможным? Введи 1 (исходный код) или 2 (компилятор или интерпретатор).

Абстракция в языке высокого уровня означает сокрытие деталей о регистрах, адресах и выборе инструкций от программиста. Продолжает ли CPU выполнять эти детали? Введи 1 (да) или 0 (нет).

FORTRAN (1957) — один из первых языков высокого уровня. По данным команды IBM, переход с ассемблера на FORTRAN повысил производительность программистов примерно во сколько раз для научных программ? Введи приближённый множитель (целое число от 10 до 30).

Проверь себя
Викторина

Каковы два главных преимущества языка высокого уровня перед языком ассемблера?

Итог

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

Продолжить восхождение ↑Компиляция против интерпретации
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.