Как скаляры хранятся "под капотом" в perl?

Основные типы в perl отличаются друг от друга, чем большинство языков, причем типы являются скалярными, массивами, хешем (но, по-видимому, не подпрограммами, и amp;, которые, я думаю, действительно являются просто скалярными ссылками с синтаксическим сахаром). Самое странное в том, что наиболее распространенные типы данных: int, boolean, char, string, все подпадают под базовый тип данных "scalar". Кажется, что perl решает скорее рассматривать скаляр как строку, логическое или число, основанное на операторе, который его модифицирует, подразумевая, что сам скаляр фактически не определен как "int" или "String" при сохранении.

Это заставляет меня задуматься о том, как эти скаляры хранятся "под капотом", особенно в отношении того, как это влияет на эффективность (да, я знаю, что языки сценариев приносят пользу для гибкости, но они по-прежнему должны быть максимально оптимизированы, проблемы с гибкостью не затрагиваются). Мне гораздо легче хранить номер 65535 (который принимает два байта), а затем строку "65535", которая принимает 6 байтов, так как распознавание того, что $val = 65535 хранит int, позволит мне использовать 1/3 памяти, в больших массивах это может означать меньшее количество кеш-запросов.

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

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

Изменить:

TJD, связанный с perlguts, который отвечает на половину моего вопроса. Скаляр фактически хранится как строка, int (подписанный, без знака, двойной) или указатель. Я не слишком удивлен, я обычно ожидал, что такое поведение произойдет под капотом, хотя интересно видеть точные типы. Я оставляю этот вопрос открытым, потому что perlguts на самом деле до низкого уровня. Другое, тогда говорящее, что существует 5 типов данных, он не указывает, как perl работает для чередования между ними, то есть как perl решает, какой тип SV использовать, когда скаляр сохраняется, и как он знает, когда/как делать.

Ответ 1

На самом деле существует несколько типов скаляров. Скаляр типа SVt_IV может содержать undef, целое число со знаком (IV) или целое число без знака (UV). Один из типов SVt_PVIV также может содержать строку. Scalaры молча обновляются с одного типа на другой по мере необходимости [1]. Поле TYPE указывает тип скаляра. Фактически, массивы (SVt_AV) и хэши (SVt_HV) на самом деле являются просто типами скаляров.

Пока тип скаляра указывает, что может содержать скаляр, флаги используются для указания того, что содержит скаляр. Это сохраняется в поле FLAGS. SVf_IOK сигнализирует, что скаляр содержит целое число со знаком, а SVf_POK указывает, что он содержит строку [2].

Devel::Peek Dump - отличный инструмент для поиска внутренних скаляров. (Константные префиксы SVt_ и SVf_ опущены Dump.)

$ perl -e'
   use Devel::Peek qw( Dump );
   my $x = 123;
   Dump($x);
   $x = "456";
   Dump($x);
   $x + 0;
   Dump($x);
'
SV = IV(0x25f0d20) at 0x25f0d30       <-- SvTYPE(sv) == SVt_IV, so it can contain an IV.
  REFCNT = 1
  FLAGS = (IOK,pIOK)                  <-- IOK: Contains an IV.
  IV = 123                            <-- The contained signed integer (IV).

SV = PVIV(0x25f5ce0) at 0x25f0d30     <-- The SV has been upgraded to SVt_PVIV
  REFCNT = 1                              so it can also contain a string now.
  FLAGS = (POK,IsCOW,pPOK)            <-- POK: Contains a string (but no IV since !IOK).
  IV = 123                            <-- Meaningless without IOK.
  PV = 0x25f9310 "456"\0              <-- The contained string.
  CUR = 3                             <-- Number of bytes used by PV (not incl \0).
  LEN = 10                            <-- Number of bytes allocated for PV.
  COW_REFCNT = 1

SV = PVIV(0x25f5ce0) at 0x25f0d30
  REFCNT = 1
  FLAGS = (IOK,POK,IsCOW,pIOK,pPOK)   <-- Now contains both a string (POK) and an IV (IOK).
  IV = 456                            <-- This will be used in numerical contexts.
  PV = 0x25f9310 "456"\0              <-- This will be used in string contexts.
  CUR = 3
  LEN = 10
  COW_REFCNT = 1

illguts полностью документирует внутренний формат переменных, но perlguts может быть лучше начать.

Если вы начнете писать код XS, имейте в виду, что обычно это плохая идея, чтобы проверить, что содержит скаляр. Вместо этого вы должны запросить, что должно было быть предоставлено (например, с помощью SvIV или SvPVutf8). Perl автоматически преобразует значение в запрошенный тип (и предупреждает, если необходимо). Вызов API задокументирован в perlapi.


  • Все скаляры (включая массивы и хеши, исключая один тип скаляра, который может содержать только undef) имеют два блока памяти на их основе. Указывает на скалярную точку на голову, содержащую поле TYPE и указатель на тело. Обновление скаляра заменяет тело скаляра. Таким образом, указатели на скаляр не будут аннулированы при обновлении.

  • Переменная undef - одна без каких-либо заглавных флагов OK.

Ответ 2

Форматы, используемые Perl для хранения данных, описаны в perlguts perldoc.

Короче говоря, скаляр Perl хранится как структура SV, содержащая одно из нескольких разных типов, например int, a double, a char * или указатель на другой скаляр. (Эти типы хранятся как C union, поэтому только один из них будет присутствовать одновременно, SV содержит флажки, указывающие, какой тип используется.)

(Что касается хеш-ключей, там важно отметить: хеш-ключи всегда являются строками и всегда хранятся в виде строк. Они хранятся в другом типе от других скаляров.)

API Perl включает в себя ряд функций, которые могут использоваться для доступа к значению скаляра как желаемого типа C. Например, SvIV() может использоваться для возврата целочисленного значения SV: если SV содержит int, это значение возвращается непосредственно; если SV содержит другой тип, он принудительно прибегает к целому числу. Эти функции используются во всем интерпретаторе Perl для преобразования типов. Однако нет автоматического вывода типов на выходе; функции, которые работают с строками, всегда будут возвращать скаляр PV (string), например, независимо от того, выглядит ли строка "числом" или нет.

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

Ответ 3

Другие рассмотрели вопрос о том, "как скаляры хранят" часть вашего вопроса, поэтому я пропущу это. Что касается того, как Perl решает, какое представление значения использовать и когда конвертировать между ними, ответ зависит от того, какие операторы применяются к скаляру. Например, с учетом этого кода:

my $score = 0;

Скаляр $score будет инициализирован целочисленным значением. Но тогда, когда эта строка кода запускается:

say "Your score is $score";

Оператор двойной кавычки означает, что Perl потребуется строковое представление значения. Таким образом, преобразование из целого в строку будет происходить как часть процесса сборки аргумента строки функции say. Интересно, что после строкой $score базовое представление скаляра теперь будет включать и целое число и строковое представление, позволяющее последующим операциям напрямую захватывать соответствующее значение без необходимости конвертировать снова. Если к строке применяется числовой оператор (например, $score++), то числовая часть будет обновлена, а часть (теперь недействительная) будет отброшена.

Именно по этой причине Perl-операторы имеют тенденцию появляться в двух вариантах. Например, сравнение значений чисел выполняется с помощью <, ==, >, в то время как выполнение тех же сравнений со строками будет выполняться с помощью lt, eq, gt. Perl будет принуждать значение скаляра (ов) к типу, который соответствует оператору. Вот почему оператор + выполняет числовое добавление в Perl, но для выполнения конкатенации строк необходим отдельный оператор .: + будет принуждать свои аргументы к числовым значениям, а . будет принуждать к строкам.

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

$score = 0;
say ++$score;       # 1
say ++$score;       # 2
say ++$score;       # 3

$score = 'aaa';
say ++$score;       # 'aaa'
say ++$score;       # 'aab'
say ++$score;       # 'aac'

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

my @numbers;
while(<$fh>) {
    if(/^(\d{8})$/) {
        push @numbers, $1;
    }
}

Любые данные, считанные из файла, изначально приходят к нам в виде строки. Регулярное выражение, используемое для проверки данных, также потребует строковое значение в $_. Таким образом, наш массив @numbers будет содержать список строк. Однако, если дальнейшее использование значений будет исключительно в числовом контексте, мы могли бы использовать эту микро-оптимизацию, чтобы гарантировать, что массив содержит только числовые значения:

push @numbers, 0 + $1;

В моих тестах с файлом из 10 000 строк, заполняя @numbers строками, используется почти в три раза больше памяти, чем заполнение целочисленными значениями. Однако, как и в большинстве эталонных тестов, это мало влияет на нормальное ежедневное кодирование в Perl. Вам нужно будет только беспокоиться об этом в ситуациях, когда вы: а) имели проблемы с производительностью или памятью и б) работали с большим количеством значений.

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