Механика XS: Архитектура

Механика XS

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

Данная статья состоит из пяти частей:

Ноябрь Введение мотивация, определения, примеры
Декабрь Архитектура интерпретатор Perl, соглашения вызывов, представление данных
Январь Инструменты h2xs, xsubpp, DynaLoader
Февраль Модули Math::Ackermann, Set::Bit
Март Align::NW глобальное оптимальное выравнивание последовательностей Нидлмана-Вунша

Концепции

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

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

  • управление должно перейти от Perl к C (и обратно)
  • данные должны перейти от Perl к C (и обратно)

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

  • представление данных в C
  • представление данных в Perl
  • выполнение программы на C
  • выполнение программы на Perl

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

Представление данных

Мы начнем с описания представления данных в C и Perl.

Представление данных в C

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

Тип данных Формат
символ (character) один байт
целое число (integer) 32-бита с дополнением до 2-х
вещественное число (double) 80 бит, с выделением под знак, экспоненту и мантиссу
указатель на функцию адрес точки входа функции

Сложные типы данных представляют собой соединение из простых типов. Например, тип данных на C

int x[2];

содержит 8 байт в памяти: 4 байта для x[0], и следующие сразу за ним еще 4 байта для x[1]. Аналогично, тип данных C

struct S
{
  int     a;
  char    b[4];  
}

содержит 8 байт в памяти: 4 байта для a, и следующие сразу за ним еще 4 байта для 4 элементов b.

Данный вид представления данных имеет свои преимущества и недостатки.

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

Самое важное преимущество это эффективность. Данные занимают наименьшее необходимое место в памяти: 8-битный символ занимает 8 бит; массив из 2 32-битных целых чисел занимает 64 бита.

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

Другие преимущества представления данных на C включают простоту и прозрачность.

Недостатки

Недостатком представления данных на C является то, что данные не описывают сами себя: они не содержат мета-данных.

Например, выделенные [выше] 8 байт в памяти, нет возможности сказать, кроме от самих байтов, где они содержат образец int x[2], а где struct S, или другой тип данных, или не содержат вообще никакой полезной информации.

Вся информация о данных скрыта внутри исполняемого кода программы. Причиной тому, что делает 8 байт в памяти в виде struct S является факт, что программа считает именно так: тот факт, что программа помещает int в первые 4 байта, и по 1 символу в каждые последующие 4 байта.

Два специфичных недостатка представления данных на C:

  • оно стремится возложить вопросы по низко-уровневому управлению данными непосредственно на приложение
  • оно усложняет отладку

Представление данных в Perl

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

Каждый объект данных в программе на Perl представляет внутри структуру на C. (Вспомните, интерпретатор Perl это программа на C.) Например, скаляр выглядит примерно таким образом

typedef enum
{
    IOK = 0x01,      /* имеет верное значение целого числа    */
    POK = 0x02,      /* имеет верное значение строки          */
} Flags;

struct Scalar
{
    int   refcnt;    /* сколько отсылок к нам                 */
    Flags flags;     /* что мы из себя представляем           */
    char *pv;        /* указатель на выделенную malloc строку */
    int   cur;       /* длина pv в виде строки на C           */
    int   len;       /* выделенный размер памяти для pv       */
    int   iv;        /* значение целого числа                 */
};

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

my $x = 42;

Когда интерпретатор выполняет данное выражение, он

  • выделяет память для структуры Struct
  • устанавливает поле refcnt равным 1
  • устанавливает поле iv равным 42
  • устанавливает флаг IOK
  • очищает флаг POK

Если мы позднее напишем

print "$x";

интерпретатор

  • выделит место для pv
  • вызовет sprintf(pv, "%d", iv), чтобы сконвертировать iv в строку
  • установит флаг POK

refcnt начинаются с 1 потому что $x относится к скаляру. Когда ссылка на скаляр создана, интерпретатор увеличивает refcnt; когда ссылка на скаляр пропадает, интерпретатор уменьшает refcnt. Когда последняя ссылка на скаляр (включая $x) пропадает, refcnt принимает значение нуля и интерпретатор освобождает структуру Struct.

Массивы представлены структурой (объявлены в av.h). Хэши представлены структурой (объявлены в hv.h). Ссылки на процедуры и функции представлены структурой (объявлены в cv.h). Каждый объект данных в Perl представлен в виде структуры того или иного вида.

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

Данный вид представления данных имеет некоторые важные преимущества для программиста.

Это освобождает программиста от заботы о типах данных. Интерпретатор знает каждый тип данных; он конвертирует из одного в другой при необходимости.

Это также освобождает программиста от заботы выделения места под данных. Интерпретатор знает размер и расположение каждого объекта данных. Он выделяет, изменяет размер и освобождает их при необходимости. Никаких утечек памяти, никаких одичалых или висячих указателей. Никакой болезни C-программиста.

Недостатки

Этот вид представления данных имеет один большой недостаток: эффективность. Мета-данные потребляют память, интерпретатор использует циклы ЦП для навигации по структурам, проверке флагов и конвертирования типов данных на лету. Каждая программа отличается, но не является необычным для данных Perl требовать на порядок больше ОЗУ и ЦП, чем сходные данные на C. Это одна из главных мотиваций для написания XS модулей.

PerlGuts

Код XS требует доступа к объектам данных Perl, но он не манипулирует непосредственной структурой на C напрямую. Напротив, основную часть работы выполняет интерпретатор Perl. Взамен, доступ к объектам данных Perl предоставлен широкой коллекцией макросов и подпрограмм. Они объявлены в различных .h файлах в /usr/local/lib/perl/*version*/*architecture*/CORE, и описаны в perlguts. Они составляют Интерфейс Программирования Приложений на языке С в Perl (Perl’s C Application Programming Interface, далее в тексте просто Perl C API – прим. пер.).

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

Выполнение программ

Теперь мы посмотрим как C и Perl выполняют код.

Программа на C

Точно также как и представление данных на C, выполнение программы на C простое, маленькое, и эффективное. Компилятор C транслирует текст программы на C в последовательность нативных машинных инструкций. ЦП выполняет эти инструкции на аппаратном уровне.

Выделенный регистр ЦП, называемый счетчиком программы (program counter, PC), хранит адрес следующей инструкции для выполнения. В нормальном потоке управления, ЦП увеличивает значение PC как только инструкция выполнена. Это приводит к тому, что ЦП выполняет инструкции в том же порядке в каком они хранятся в памяти.

Управляющие структуры C, такие как for, while, и if () then; else;, требуют ЦП выполнять прыжки: от конца цикла обратно к его вершине; между выражениями then и else, которые не учитываются. Инструкция прыжка содержит адрес следующей инструкции на выполнение: это называется целью (target) прыжка. Когда ЦП выполняет прыжок, он загружает адрес цели прямо в PC, и выполнение продолжается из этой точки.

Вызовы подпрограмм

Вызовы подпрограмм это специальная разновидность прыжка. Большинство процессоров предоставляют определенный вид аппаратной поддержки для вызовов подпрограмм, включая

  • инструкцию call
  • инструкцию return
  • стек в памяти, называемый стек процессора
  • выделенный регистр ЦП для указателя стека (stack pointer, SP)

Первая инструкция подпрограммы называется точкой входа (entry point). Инструкция call содержит адрес точки входа.

Чтобы выполнить инструкцию call, ЦП первым делом увеличивает PC, чтобы получить адрес следующей инструкции. Он называется адрес возрата (return address). Однако, он не выполняет следующую инструкцию. Вместо этого, он кладет адрес возврата на стек процессора. Потом, он загружает точку входа из инструкции call в PC и начинает выполнение подпрограммы.

В конце подпрограммы находится инструкция return. Когда ЦП выполняет инструкцию return, он изымает адрес возврата из стека, загружает его в PC, и возобновляет выполнение с той инструкции, которая следует за оригинальной инструкцией call.

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

Передача параметров

В дополнении к вызову и возврату из подпрограмм, программы хотят передавать параметры в подпрограммы, и получать значения возврата обратно.

Параметры передаются на том же стеке, что и адрес возврата. Компилятор генерирует код, который учитывает каждый параметр и кладет значение параметра на стек. После того, как все параметры расположены на стеке, запрашивается вызов инструкции call.

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

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

Программа на Perl

Выполнение программы на C делится явно на две фазы:

  1. компилятор транслирует исходный код в машинный код
  2. ЦП выполняет машинный код

Выполнение программы на Perl также делится на две фазы:

  1. интерпретатор транслирует исходный код в синтаксическое дерево
  2. интерпретатор выполняет синтаксическое дерево

Однако, разделение между двумя фазами не такое явное

  • блок BEGIN выполняет код в фазе трансляции
  • eval и s///e транслируют код в фазе выполнения

Свойство пересекать черту между трансляцией и выполнением это мощное средство Perl.

Синтаксическое дерево

Интерпретатор Perl транслирует исходный код в синтаксическое дерево (syntax tree). Узлы дерева называются опкодами (opcodes). Не следует путать с опкодами набора инструкций ЦП. Скорее, они структуры C, которые представляют примитивные операции в языке программирования Perl. Имеется более 300 опкодов, представляющих

  • все встроенные функции, такие как cos, open и sort
  • все операторы, такие как cmp, +, и <<
  • управляющие структуры, такие как циклы и условия
  • вызовы подпрограмм

Окончания узла представляют из себя операнды: условие выражения if; числа для прибавления; аргументы для подпрограммы.

Стек-машина

После того, как интерпретатор создаст синтаксическое дерево, он выполняет программу, обходя узлы дерева. Это называется хождение по дереву (walking the tree).

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

Хождение по дереву обычно выдает значение: по этой причине, мы также говорим об оценивании (evaluating) узла. Интерпретатор хранит эти значения на стеке. Мы зовем его стек Perl, чтобы отличать от стека процессора, описанного ранее.

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

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

Пример

Предположим мы имеем следующее выражение на Perl

$x = $y + 3;

Оно разбирается на части в виде синтаксического дерева, например такого

   =
  / \
$x   +
    / \
  $y   3

Это дерево имеет 5 узлов. Таблица ниже показывает их порядок оценки, а также содержание стека после того, как каждый узел оценен.

Шаг 1 2 3 4 5
Узел $x $y 3 + =
Стек \$x 42 3 45
\$x 42 \$x
\$x

Вот, что происходит на каждом шагу.

  1. Т.к. узел = присвоит значение $x, интерпретатор кладет ссылку на $x, а не его значение.
  2. Значение $y оказывается равным 42.
  3. 3 оценивается как 3.
  4. Узел + извлекает значения 3 и 42, складывает их, и кладет сумму на стек.
  5. = извлекает значение 45 и ссылку на $x, и производит присваивание. Теперь стек пуст.

Вызовы подпрограмм

Теперь, когда мы понимает кое-что о данных Perl и выполнении, мы можем взглянуть подробно на то, как Perl делает вызов подпрограмм.

Подпрограммы Perl

Интерпретатор Perl создает отдельное синтаксическое дерево для каждой подпрограммы в программе. Каждое синтаксическое дерево управляется ссылкой на код (code reference). Ссылка на код это вещь, которую вы получаете, написав

my $coderef = sub { ... }

или

sub foo { ... }
my $coderef = \&foo;

в Perl. Внутри себя, ссылка на код представлена в виде (чем же еще?) структурой на C. Не смотря на другие вещи, ссылка на код имеет указатель на корень синтаксического дерева для подпрограммы. Мы зовем его корневой указатель (root pointer).

Вызов подпрограммы в исходнике программы представлен в виде синтаксического дерева напоминающий фрагмент ниже

    entersub
       |
  +----+--...---+
  |    |        |
arg1 arg2 ... argN

Опкод entersub передает управление в вызываемую подпрограмму. Её окончания оценивают аргументы вызова.

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

Опкод entersub хранит указатель ссылки на код. Когда интерпретатор обходит entersub, он следует за этим указателем ссылки на код, и далее следует к корневому указателю в ссылке на код синтаксического дерева этой подпрограммы. Затем он выполняет саму подпрограмму.

Как было сказано ранее, вещи в стеке Perl не являются структурами на C, которые представляют аргументы подпрограммы, скорее, указатели на эти структуры. В этом состоит смысл утверждения из perlsub, что

Массив @_ является локальным массивом, но его элементы это псевдонимы для настоящих скалярных параметров.

Другими словами, Perl передает аргументы по ссылке, как FORTRAN, и не как C.

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

[Все это может показаться очень запутанным, однако, программисты C смогут провести аналогию синтаксического дерева с примерами для связанного списка, калькулятора обратной польской нотации и бинарного дерева из книги “Язык программирования C”, Б. Кернигана и Д. Ритчи. – прим. пер.]

внешние подпрограммы (eXternal Subroutines)

Одна из вещей, которую ссылка на код имеет это указатель на функцию C. Другими словами, ссылка на код имеет поле, которое содержит адрес точки входа скомпилированной подпрограммы на C. Мы зовем его указатель xsub (xsub pointer). Сама подпрограмма C называется xsub.

Когда интерпретатор выполняет опкод entersub, он сначала проверяет указатель xsub в ссылке на код. Если указатель xsub равен null, он следует к корневому указателю в синтаксическом дереве подпрограммы и обходит его, как описано выше.

Если указатель xsub не равен null, то интерпретатор игнорирует корневой указатель. Вместо него, он получает адрес xsub из указателя xsub, и вызывает xsub. Это вызов внешней подпрограммы (eXternal Subroutine call). Вот как передается управление от Perl к C. Это XS.

Загрузка, Линковка, и Установка

Мы рассмотрели как Perl может вызывать подпрограммы C с помощью указателя xsub в ссылке на код. Теперь мы должны понять как Perl овладевает подпрограммой C в самом начале.

Чтобы подпрограмма C стала xsub, должно произойти три вещи

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

Установка

Установка самая простая. Perl C API включает в себя функцию

newXS(char *name, void (*fp)())

Используя имя подпрограммы Perl в name, и адрес точки входа подпрограммы C в fp, newXS установит fp как указатель xsub в ссылке на код для name. Как только это произойдет, любой код на Perl, который вызывает name() будет вызывать подпрограмму C.

name может быть любым пакетом. Чтобы установить подпрограмму с именем new() в пакете Align::NW, мы передаем строку “Align::NW::new” для name. Если Align::NW реализует класс, это приведет к тому, что подпрограмма на C будет являться методом класса.

Загрузка и линковка

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

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

  • исполняемый код для каждой подпрограммы
  • таблицу символов, которая соотносит имя каждой подпрограммы с ее смещением точки входа в библиотеке

Статическая против Динамической

На данный момент, у нас есть два пути: статическая линковка и динамическая линковка.

Статическая линковка требует от нас пересборки интерпретатора Perl. Мы компилируем исходные коды Perl в объектные файлы, а после этого линкуем объектные файлы Perl с нашей библиотекой, чтобы создать новую версию исполняемого файла Perl.

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

Динамическая линковка позволяет нам использовать интерпретатор Perl без [его] модификации. Если программа Perl нуждается в xsub, она загружает библиотеку во время выполнения и ищет точку входа подпрограммы в таблице символов. Различные программы могут устанавливать различные xsub’ы.

Мы обычно предпочитаем динамическую линковку, более конкретно, потому что она позволяет нам писать новые XS модули без необходимости пересборки интерпретатора Perl. Однако, XS модули могут быть слинкованы статически. Это иногда делается ради эффективности. Некоторые модули (такие как DynaLoader) требуют статической линковки. До Perl 5, все расширения языка требовали статическую линковку.

Динамическая линковка

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

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

Даже имена библиотек системно-зависимы. В Unix системах, они называются разделяемыми объектами (shared objects), а идентифицируются по расширению .so. В Windows, они называются динамически подключаемыми библиотеками (Dynamic Link Libraries) и идентифицируются по расширению .dll. На Macintosh, они называются разделяемыми библиотеками (shared libraries), и (обычно) идентифицируются в finder типом (a finder type) shlb.

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

Наконец, системные вызовы, которые выполняют динамическую линковку, в том числе вызов newXS(), являются вызовами на языке C. Это означает, что мы должны вызывать их из кода на C, а не кода на Perl. Нам нужен xsub, чтобы установить xsub.

DynaLoader

К счастью, все это сделано за нас в модуле Perl, называемом DynaLoader. Когда мы пишем XS модуль, наш модуль наследуется от модуля DynaLoader. Когда наш модуль загружается, он делает один вызов метода DynaLoader::bootstrap, bootstrap находит наши библиотеки для линковки, загружает их, находит наши точки входа, и делает соответствующие вызовы newXS().

DynaLoader справляется с системными зависимостями методом перебора. Если вы взгляните в исходники Perl, то вы обнаружите около 10 различных версий XS кода DynaLoader. Скрипт Configure выбирает корректную версию для каждой системы, когда создается [или собирается] интерпретатор Perl.

Понимание деталей каким образом DynaLoader устанавливает xsub’ы остается в виде упражнения для читателя.

Передача параметров

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

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

Чтобы решить эту проблему, что-то должно конвертировать представления данных на Perl и C. Интерпретатор Perl не может, значит это должна делать xsub. Типично, xsub использует возможности Perl C API, чтобы получить параметры из стека Perl и сконвертировать их в значения данных C. Чтобы вернуть значение, xsub создает объект данных Perl и оставляет указатель на них в стеке Perl.

Непосредственное кодирование на Perl C API тяжело. Более того, мы часто хотим вызывать существующие подпрограммы C из Perl: подпрограммы, которые не знают про Perl C API, и которые ожидают найти свои параметры на стеке процессора. Мы будем ссылаться на существующую подпрограмму C как на целевую подпрограмму (target routine).

Вместо того, чтобы перетащить Perl C API в наш код, мы обычно пишем клей-подпрограммы (glue routines). Интерпретатор Perl вызывает клей-подпрограммы как xsub. Клей-подпрограмма конвертирует параметры Perl в значения данных C, и после вызывает целевую программу, передавая ей значения данных C в виде параметров на стеке процессора. Когда целевая подпрограмма завершается, клей-подпрограмма создает объект данных Perl, чтобы предоставить ее значение возврата, и кладет указатель на этот объект на стек Perl. Наконец, клей-подпрограмма возвращает управление к интерпретатору Perl.

Клей-подпрограммы дают некоторую структуру для [решения] вопроса с потоком данных, но их всеравно сложно писать. Поэтому мы не станем этого делать. Вместо этого, мы напишем XS-код. XS в той или иной степени язык макросов. Он позволяет нам объявить целевую подпрограмму, и определить соответствие между Perl и C типами. Программа под названием xsubpp читает XS код и генерирует клей-подпрограммы на чистом C.

Большая часть работы, и большая часть волшебства, в XS относится к выяснению как написать XS код так, чтобы xsubpp выполнял правильные вещи. Мы поговорим об этом в огромном объеме – в следующем месяце.


Заметки

программа на C

Существует проект с целью переписать интерпретатор Perl на C++. Это может потребовать технических изменений в XS, но в целом не должен затронуть архитектуру.

примерно таким

Чтобы узнать как это выглядит на самом деле, посмотрите на /usr/local/lib/perl/*version*/*architecture*/CORE/sv.h

создана

например, написав \$x

пропадает

либо через переопределение, либо через выход из области видимости

утечек памяти

Круговые структуры, например, $x = \$x, не дают счетчикам ссылок уйти в нуль. Если программист не разъединит круг явно, память не высвободится до завершения программы.

на порядок больше

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

увеличивает

Более точно, ЦП добавляет размер текущей инструкции к PC, чтобы продвинуть его к следующей инструкции. На многих процессорах, различные инструкции имеют различные размеры.

рекурсивными

Серии компьюторов IBM 360/370/3033/3081 не имели стека процессора и выделенного указателя на стек. ЦП сохранял адреса возвратов в регистрах общего назначения. Компиляторы обычно копировали адрес возврата в фиксированную область памяти, ассоциированную с вызывающей подпрограммой. Это позволяло созадавать вложенные, но не рекурсивные вызовы подпрограмм. Это было адекватно для языков, таких как ассемблер, FORTRAN, или COBOL: но было менее адекватным для языков, таких как PL/I и C.

значение параметра

Это именно то, что означает выражение передавать параметры по значению.

она знает

Она знает потому, что компилятор скомпилировал смещение в нее.

корень

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

ссылки на код

Очень важна косвенность посредством ссылки на код. Узел entersub не может хранить указатель корня, потому что кто-то может написать такое

sub One { print "One\n" }
One();
eval 'sub One { print "Two\n" }';
One();

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

те же самые места

корректируя указатель стека, если необходимо

До Perl 5

На самом деле это было хуже, чем сейчас. До Perl 5, все расширения языка требовали изменений в исходниках Perl. Изменение языковых процессоров никогда не было простым делом, поэтому это делалось только для Больших Важных Вещей, таких как интерфейсы баз данных (sybperl, oraperl). Даже тогда, это считалось неудовлетворительным. Одной из целью Perl5 было предоставление возможности людям расширять язык без прикосания к ядру процессора языка.

упражнения для читателя

дополнительно: узнайте как установлены xsub’ы самого DynaLoader.