15.6. Семантика наследования, основанная на преобразованиях типов 159
но она ведет к некоторым потерям в эффективности — особенно при численных расчетах и при до-
ступе к полям записей, — и эти потери могут оказаться неприемлемыми в высокопроизводительных
реализациях. В этом разделе мы сделаем набросок альтернативной семантики на основе преобразова-
ния типов и обсудим некоторые вопросы, возникающие уже в связи с таким новым подходом. При
желании этот раздел можно пропустить.
15.6.1. Проблемы семантики, основанной на подмножествах
Как мы видели в §15.5, часто бывает удобно разрешить наследование между различными базовыми
типами. Однако некоторые «интуитивно понятные» отношения включения между базовыми типами
могут оказаться вредными с точки зрения производительности. Допустим, например, что мы введем
аксиому Int <: Float, так, чтобы можно было использовать целые числа в вычислениях с плавающей
точкой без явных преобразований — например, писать 4.5 + 6 вместо 4.5 + intToFloat(6). При се-
мантике, основанной на подмножествах это означает, что множество целых числовых значений должно
буквально являться подмножеством значений с плавающей точкой. Однако в большинстве реальных
машин конкретные представления целых чисел и чисел с плавающей точкой совершенно различны:
целые обычно представлены в форме кодов с дополнением до двух, а числа с плавающей точкой разде-
лены на поля мантиссы, показателя степени и знаковое, плюс имеются некоторые особые случаи вроде
NaN (не-число).
Чтобы согласовать такое расхождение в представлениях с семантикой на основе подмножеств, мы
можем принять общее теговое (или упакованное) представление для чисел: целое число представляется
как машинное целое, снабженное тегом (располагающимся либо в отдельном слове-заголовке, либо в
старших разрядах слова, содержащего само целое число), а число с плавающей точкой представляется
как машинное число с плавающей точкой, снабженное другим тегом. В таком случае тип Float относит-
ся ко всему множеству помеченных тегами чисел, в то время как Int относится только к помеченным
целым.
Такая схема вполне разумна: она соответствует стратегии представления, реально применяемой
во многих современных реализациях языков, где теговые биты (или слова) нужны также при сборке
мусора. Недостаток ее состоит в том, что всякая элементарная операция над числами должна реализо-
вываться как сочетание проверки тега на аргументах, нескольких машинных команд для извлечения
самого числа, одной команды для самой операции, и пары команд для навешивания тега на результат.
Изощренные оптимизации в компиляторе могут уничтожить часть лишних действий, но даже с луч-
шими из имеющихся методов производительность сильно страдает, особенно в коде, содержащем много
численной работы, вроде графических или научных вычислений.
Другая проблема с производительностью возникает, когда наследование сочетается с записями — в
частности, при применении правила перестановки полей Наше простое правило вычисления для про-
екции поля
{l
i
=v
i 1..n
i
}.l
j
v
j
(E-ProjRcd)
можно читать так: «найти среди меток полей записи l
j
, и вернуть соответствующее значение v
j
». Од-
нако в настоящей реализации мы, безусловно, не хотим проводить линейный просмотр полей записи в
поисках нужной метки. В языке без наследования (или с наследованием, но без правила перестановки)
можно получить значение намного быстрее: если метка l
j
идет третьей в типе записи, то мы уже во
время компиляции знаем, что все значения данного типа будут содержать l
j
как третье поле, так что
во время выполнения нам вовсе не нужно смотреть на метки (мы вообще можем исключить их из пред-
ставления записей во время выполнения, в сущности, превращая записи в кортежи). Чтобы получить
значение поля l
j
, мы порождаем команду косвенной загрузки через регистр, указывающий на начало
записи, с постоянным смещением в 3 слова. Наличие правила перестановки делает этот метод невоз-
можным, поскольку информация, что некоторое значение записи принадлежит типу, где l
j
является
третьим полем, ничего не говорит нам о том, где на самом деле внутри записи хранится поле l
j
. Опять
же, сложные оптимизации и трюки кодирования могут уменьшить потери, но в общем случае проекция
поля может потребовать некоторой формы поиска во время выполнения.
3
3
Похожие наблюдения применимы и к поиску полей и методов объектов в языках, где наследование объектов позволяет
перестановку полей. Именно по этой причине, например, Java ограничивает наследование между классами так, что новые
поля могут добавляться только в конце. Наследование между интерфейсами (а также между классом и интерфейсом)
rev. 104