1 (изменено: Freeman, 27.03.2017 в 19:20)

Тема: Управление памятью в Канторе

О памяти в Rust на Хабре

Пробежал наискосок статью про Rust, увидел следующее определение с примером:

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

let foo = vec![1, 2, 3]; 
// Мы создаем новый вектор (массив), содержащий элеметы 1, 2, и 3,
// и привязываем его к локальной переменной `foo`.

let bar = foo;
// Передаем владение объектом переменной `bar`.
// После этого мы не можем получить доступ к переменной `foo`,
// поскольку она теперь ничем не "владеет" - т.е., не имеет никакой привязки.

Понял, что уже могу попробовать расписать это для Кантора. В Канторе владение и доступность объектов сделаны немного по-иному:

  • Владельцем объекта может выступать или вызывающая сторона, или сам объект:

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

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

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

  • Компилятор выбирает способ размещения объекта в зависимости от его доступности. Этот пункт еще будет уточняться.

В аналогичном Rust-у примере на Канторе бросается в глаза разница из-за объектов первого класса:

var foo = {1, 2, 3}; // переменная-массив фиксированного размера -- Core:Byte[3]
var bar = foo;       // присваивание одного массива другому -- копирование данных
ref var bar2 = foo;  // присваивание простой ссылке в границах одной области видимости
return foo;          // наружу передается значение foo, способ передачи выбирается компилятором в зависимости от способа приема на вызывающей стороне

Тип переменной foo может быть любым, поведение компилятора не изменится:

var foo = 5;         // переменная фиксированного размера -- Core:Byte
var bar = foo;       // присваивание одной переменной другой -- копирование данных
ref var bar2 = foo;  // присваивание простой ссылке в границах одной области видимости
return foo;          // наружу передается значение foo, способ передачи выбирается компилятором в зависимости от способа приема на вызывающей стороне 

В примере на Канторе нет передачи владения как таковой, поскольку в Канторе владение передается только при пересечении объектом области видимости, в которой он был описан.

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

Весь код в Интернете -- УГ

Код примера на Канторе приведен лишь для объяснения его поведения с переменными, чтобы повторить пример на Rust. В реальности же этот код не скомпилируется с настройками Кантора по умолчанию:

Warning: redundant variable use

По умолчанию предупреждения трактуются как ошибки, поэтому компиляция прекратится.

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

Правильный, функциональный пример на Канторе выглядит так:

foo = {1, 2, 3}; // функция, возвращающая массив фиксированного размера -- Core:Byte[3]
bar = foo;       // синоним функции
ref bar2 = foo;  // синоним функции через ссылку; в границах одной области видимости синоним всегда определен, ссылочность будет выкинута компилятором, то есть объявление равноценно bar
return foo;      // наружу передается возвращаемое значение foo, способ передачи выбирается компилятором в зависимости от способа приема на вызывающей стороне

Всего лишь убраны ключевые слова var, а как поменялся смысл!

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

2

Re: Управление памятью в Канторе

Вопросы:
- Чем не устраивает или не нравится концепция владения Rust?
- Где хранятся объекты, размер которых фиксирован, но известен только во время исполнения?

3

Re: Управление памятью в Канторе

Юрий пишет:

Чем не устраивает или не нравится концепция владения Rust?

Поскольку Rust и Кантор ровесники, при разработке Кантора я не мог знать о концепциях, принятых в Rust. Концепции управления памятью в Канторе аутентичны и являются дальнейшим развитием управления памятью Delphi, доработанного до применимости в языке с объектами первого класса. Rust -- императивный язык со встроенными типами, его принципы не могут быть перенесены на Кантор. Они не плохие и не хорошие, они просто другие.

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

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

Юрий пишет:

Где хранятся объекты, размер которых фиксирован, но известен только во время исполнения?

Приведите конкретную ситуацию, пожалуйста.

4

Re: Управление памятью в Канторе

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

$res = $str1 . $str2

5

Re: Управление памятью в Канторе

Юрий пишет:

Конкатенация должна где-то разместить склеенную строку

Ключевое слово здесь -- где-то? В общем случае строка размещается в динамической памяти. В той реализации, которая будет в альфе Кантора, в динамической памяти будет размещен только буфер строки, а ее фиксированная часть (длина, кодовая страница, флаги, индекс, VMT) будет в теле объекта, который в вашем примере будет в сегменте данных или в стеке.

6

Re: Управление памятью в Канторе

На сайте compiler.su есть серия статей о хранении объектов переменной длины в стеке и двухстековой модели памяти:

После их прочтения у меня возникло стойкое ощущение, что в процессе реализации Кантора я бы дошел до чего-то похожего, но с учетом особенностей Кантора — отсутствия конструкторов и ссылок на код. Локальные переменные переменной длины, не выходящие за рамки процедуры, точно планировались. В коде CoreLite даже есть похожая реализация — CoreExceptions.ExceptMsgBox, отмеченная комментарием "// asm service", — в случае Delphi ее пришлось писать на ассемблере.

В качестве идеи, похожей на двухстековую память, я рассматриваю особый режим работы диспетчера динамической памяти (или даже отдельный мини-диспетчер), выделяющий память в режиме LIFO. Для Кантора этого будет достаточно, и даже подмены регистра стека не понадобится, поскольку диспетчер LIFO будет использоваться только для данных, выделяемых относительно большими блоками, больше размера регистра, — по сути, стековыми кадрами. В x86 нет отдельных опкодов для стековых кадров (ENTER/LEAVE не в счет в данном контексте), резервирование/освобождение кадров в стеке делается обычными операциями сложения и вычитания. Реализация диспетчера LIFO не будет отличаться от резервирования обычных стековых кадров, а для сложения, вычитания и косвенной адресации не имеет значения, какой регистр использовать. Следовательно, регистр стека диспетчеру LIFO не нужен, стек в Канторе будет использоваться обычным способом.

7 (изменено: Юрий, 29.03.2016 в 18:43)

Re: Управление памятью в Канторе

Т.е., если я правильно понял, часть объекта, имеющая фиксированную длину, будет располагаться как обычно – в стеке, а собственно данные переменного размера – в особом типе динамической памяти с очередью LIFO?

Ещё вопрос: а чем, собственно говоря, Вам не угодили конструкторы, что Вы их предали анафеме? Мне в конструкторах C++ не нравится только одно – они ничего не возвращают. А ведь могли бы вернуть указатель/ссылку на созданный объект. А так – вполне оправданная конструкция, когда специализированная функция выполняет действия по инициализации.

8

Re: Управление памятью в Канторе

Юрий пишет:

Т.е., если я правильно понял, часть объекта, имеющая фиксированную длину, будет располагаться как обычно – в стеке, а собственно данные переменного размера – в особом типе динамической памяти с очередью LIFO?

Нужно различать решения, относящиеся к альфе Кантора и кодогенератору окончательной версии. Ответ № 5 относится больше к альфе Кантора, а рассуждения о диспетчере LIFO -- к окончательной.

Юрий пишет:

Мне в конструкторах C++ не нравится только одно – они ничего не возвращают. А ведь могли бы вернуть указатель/ссылку на созданный объект.

Вы же сами себе противоречите! Самостоятельно порождать объект можно только в динамической памяти, то есть плакало наше размещение в стеке! В C++ объект создается вызывающей стороной, а в конструктор передается ссылка на его неинициализированное тело. В Delphi аналогично.

Юрий пишет:

Ещё вопрос: а чем, собственно говоря, Вам не угодили конструкторы, что Вы их предали анафеме?

Конструкторы -- следствие реализации ООП через процедуры. В Канторе объекты первичны, и помощи процедур для их порождения не требуется:

public class Country of
  public var Core:String[2] Code; // обязательное поле
  public var Core:String Name;    // обязательное поле
  public var Core:Boolean Historical = False;
end;

russia = new Country of
  Code = 'RU';
  Name = 'Россия';
  // Historical получает значение по умолчанию
end;

ussr = new Country of
  Code = 'SU';
  Name = 'СССР';
  Historical = True; // перекрытие умолчания
end;

Оператор new выполняет те же действия, что и конструктор, но при этом не является процедурой, что дает преимущества:

  • Проверка инициализации всех обязательных полей на этапе компиляции. Обязательное поле нельзя забыть -- код не скомпилируется. В будущем класс Country можно сколько угодно расширять необязательными полями, но если добавить обязательное, компилятор потребует внести изменения во все new, где он порождается.

  • Отслеживание множества путей инициализации без написания дополнительного кода. Скажем, прямоугольник может инициализироваться установкой либо всех четырых координат, либо установкой верхней левой точки и размеров. Это реализуется декларативно и произвольно комбинируется... или же дает декартово произведение перегруженных конструкторов и потому в процедурном ООП не встречается. smile

Попробуйте переписать пример с Country на Delphi или C++ и посмотрите, сколько лишнего кода придется написать из-за конструкторов.

Хороший пример вложенного new есть в статье про DSL.

9

Re: Управление памятью в Канторе

Самостоятельно порождать объект можно только в динамической памяти, то есть плакало наше размещение в стеке!

Ну что Вы… Создание объекта можно полностью перепоручить конструктору. Место создания объекта определяется явным или неявным параметром. В этом случае создание объекта происходит в 4 этапа:

  • определение необходимого размера (для объектов фиксированного  размера оно известно заранее);

  • резервирование памяти, место размещения зависит от параметра; это либо динамическое размещение в куче (new, malloc()), либо динамическое размещение в стеке, либо статическое размещение в стеке (для объектов, чей размер известен во время компиляции);

  • инициализация объекта (конструкторы C++ делают только эту работу);

  • возврат адреса созданного объекта, если он создавался динамически.

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

(класс Страна
   код: строка [2]
   наименование: строка
   (Страна*  Страна (метод: функция размещения, код страны: строка, наименование страны: строка)
      this = метод (определить размер объекта (код страны, наименование страны))
      код = код страны
      наименование = наименование страны
      вернуть this)
)
неизвестная  страна = Страна (new, читать код страны(), читать наименование страны())
моя страна = Страна ('RU', 'Россия')

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

Такой конструктор длиннее, чем на Канторе, но его вызов становится короче.

10

Re: Управление памятью в Канторе

Юрий пишет:

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

Поясните, пожалуйста, вызовы Страна() в вашем коде являются процедурными или функциональными?