Я написал предыдущий пост, и знакомый меня справедливо спросил, а какие же именно функции вызываются при создании объекта? Проверить несложно!
tmp := TObject.Create;
tmp.Destroy;
Оказывается, вот полный список всех вызовов при создании объекта: (далее)
Create
@ClassCreate
TObject.NewInstance
TObject.InstanceSize
@GetMem
TObject.InitInstance
@AfterConstruction
TObject.AfterConstruction
Из них TObject.InstanceSize и TObject.AfterConstruction не делают вообще ничего серьёзного, остальные не слишком много (20-30 инструкций).
Выглядит довольно безобидно! Как же на самом деле? Чтобы посмотреть, насколько быстро создаются объекты, я написал простенькую программу (pastebin). В ней в цикле создаётся и уничтожается 50 миллионов объектов и рекордов.
Для рекордов я использовал два варианта создания/удаления: GetMem/FreeMem и New/Dispose. Последний отличается тем, что Дельфи автоматически генерирует код, инициализирующий и очищающий так называемые "сложные" поля - строки и указатели на интерфейсы. Разумеется, Dispose не может быть быстрее FreeMem, поскольку в конечном счёте его же и вызывает!
Итак, пустой объект и пустой рекорд, результаты в миллисекундах:
Objects: 5469
Records through new/dispose: 344
Records through GetMem/FreeMem: 343
Неплохо! Объекты в десяток раз медленней! Однако у этого есть причина, которая станет ясна, если мы повторим эксперимент, добавив в объект и в рекорд по одному полю типа integer.
Теперь мы получаем:
Objects: 5453
Records through new/dispose: 1094
Records through GetMem/FreeMem: 1109
Откуда такой прирост у рекордов? Дело в том, что размер рекорда в прошлом эксперименте был равен нулю. GetMem/FreeMem просто игнорировала эти пустые запросы. У объекта же существуют скрытые поля. Если мы запросим размер объекта
tmp := TObject.Create;
writeln('Size: '+IntToStr(tmp.InstanceSize));
То получим:
Size: 4
Как только мы добавили поле и в рекорд, оптимизация GetMem перестала работать, и время создания записи подскочило в три раза. Но это ещё не всё! Добавим в рекорд поле типа string, чтобы проиллюстрировать разницу между New и Dispose.
Получаем:
Objects: 6188
Records through new/dispose: 3687
Records through GetMem/FreeMem: 1094
"Корректное" создание рекордов уже всего в два раза медленней объектов! Рекорды через GetMem/FreeMem работают с прежней скоростью, поскольку размер объекта не изменился: переменная типа string занимает те же четыре байта, что и integer.
Примерно то же получится, если добавить в рекорд динамический массив: он тоже требует финализации. А вот статические массивы не требуют: память для них выделяется за один запрос, вместе с памятью записи:
FField: array[0..40] of integer;
Objects: 6844
Records through new/dispose: 1188
Records through GetMem/FreeMem: 1187
Казалось бы, я совершенно напрасно ругал объекты! Ведь любой сколь-либо сложный набор данных в рекорде создаётся всего в два раза быстрее объекта. Ну, два раза для таких быстрых операций - это ерунда. Я уже готов был придти к такому выводу, как решил посмотреть, сколько занимает сложение строк:
s := s + 'test';
if Length(s) > 10000 then s := ''; //Чтобы не разбухала
1500 микросекунд! А разница между New и GetMem в примере со строкой была 2500!
Иными словами, расширение места под строку и копирование слова "test" занимает меньше времени, чем инициализация/финализация пустого поля типа string! Да как такое может быть?
Оказывается, вот как. Оказывается, для инициализации и финализации полей Дельфи вставляет не сам код, а вызов внутренних функций @New/@Dispose с параметром, в котором зашифровано, что именно удалять. Внутри этих функций довольно громоздкий процесс разборы параметра на части.
Попробуем сделать всё вручную! Напишем:
GetMem(rec, SizeOf(rec^))
pointer(rec.FField) := 0; //инициализация строки
rec.FField := ''; //финализация строки
FreeMem(rec);
Во-первых, дельфи могла бы догадаться, что на момент присваивания пустой строки rec.FField и так пустой. Но Дельфи этого, слава богу, не делает, и честно проверяет "if rec.FField<>nil then @LStrClr", образно говоря. Таким образом, мы выполняем все операции, необходимые для создания/очистки рекорда со строковым полем.
Время? ~1300. Меньше, чем на 100 миллисекунд отличается от простого GetMem/FreeMem. Остальные 2400 микросекунд уходят на шатания по функциям @New/@Dispose с выяснением в рантайме вещей, которые и так известны на момент компиляции.
Теперь сделать решительный вывод опять стало сложно. Получается, если делать всё действительно оптимально - то есть, вручную - то рекорды примерно в шесть раз быстрее объектов - и этот разрыв будет расти с ростом сложности! Шестикратное замедление - это уже вполне значительная разница.
С другой стороны, если пользоваться для инициализации рекордов средствами Delphi (New/Dispose), то разница всего лишь в два раза, и она будет уменьшаться с ростом сложности! Ведь чем сложнее объект, тем большую часть создания занимает инициализация, а она общая у объектов и у New/Dispose рекордов.
Во всяком случае, вывод надо сделать такой: с рекордами не стоит использовать New/Dispose, это убивает весь их выигрыш в скорости. Если же вы используете New/Dispose, то уже не очень жалко превратить рекорды в объекты. Это уже мало (в пару раз) замедлит дело. Совершенно неожиданный вывод, поскольку я всегда считал New/Dispose быстрыми обёртками над GetMem/FreeMem.
Вообще же говоря, чтобы оценить порядок временных затрат: время на создание пустого TObject примерно равно одной десятитысячной микросекунды. В общем, я скорее всё же был неправ, обвиняя объекты в медлительности. Разумеется, было бы лучше, чтобы объекты создавались без дополнительной суеты, но её не так много, чтобы это делало их использование при большой нагрузке непрактичным.
tmp := TObject.Create;
tmp.Destroy;
Оказывается, вот полный список всех вызовов при создании объекта: (далее)
Create
@ClassCreate
TObject.NewInstance
TObject.InstanceSize
@GetMem
TObject.InitInstance
@AfterConstruction
TObject.AfterConstruction
Из них TObject.InstanceSize и TObject.AfterConstruction не делают вообще ничего серьёзного, остальные не слишком много (20-30 инструкций).
Выглядит довольно безобидно! Как же на самом деле? Чтобы посмотреть, насколько быстро создаются объекты, я написал простенькую программу (pastebin). В ней в цикле создаётся и уничтожается 50 миллионов объектов и рекордов.
Для рекордов я использовал два варианта создания/удаления: GetMem/FreeMem и New/Dispose. Последний отличается тем, что Дельфи автоматически генерирует код, инициализирующий и очищающий так называемые "сложные" поля - строки и указатели на интерфейсы. Разумеется, Dispose не может быть быстрее FreeMem, поскольку в конечном счёте его же и вызывает!
Итак, пустой объект и пустой рекорд, результаты в миллисекундах:
Objects: 5469
Records through new/dispose: 344
Records through GetMem/FreeMem: 343
Неплохо! Объекты в десяток раз медленней! Однако у этого есть причина, которая станет ясна, если мы повторим эксперимент, добавив в объект и в рекорд по одному полю типа integer.
Теперь мы получаем:
Objects: 5453
Records through new/dispose: 1094
Records through GetMem/FreeMem: 1109
Откуда такой прирост у рекордов? Дело в том, что размер рекорда в прошлом эксперименте был равен нулю. GetMem/FreeMem просто игнорировала эти пустые запросы. У объекта же существуют скрытые поля. Если мы запросим размер объекта
tmp := TObject.Create;
writeln('Size: '+IntToStr(tmp.InstanceSize));
То получим:
Size: 4
Как только мы добавили поле и в рекорд, оптимизация GetMem перестала работать, и время создания записи подскочило в три раза. Но это ещё не всё! Добавим в рекорд поле типа string, чтобы проиллюстрировать разницу между New и Dispose.
Получаем:
Objects: 6188
Records through new/dispose: 3687
Records through GetMem/FreeMem: 1094
"Корректное" создание рекордов уже всего в два раза медленней объектов! Рекорды через GetMem/FreeMem работают с прежней скоростью, поскольку размер объекта не изменился: переменная типа string занимает те же четыре байта, что и integer.
Примерно то же получится, если добавить в рекорд динамический массив: он тоже требует финализации. А вот статические массивы не требуют: память для них выделяется за один запрос, вместе с памятью записи:
FField: array[0..40] of integer;
Objects: 6844
Records through new/dispose: 1188
Records through GetMem/FreeMem: 1187
Казалось бы, я совершенно напрасно ругал объекты! Ведь любой сколь-либо сложный набор данных в рекорде создаётся всего в два раза быстрее объекта. Ну, два раза для таких быстрых операций - это ерунда. Я уже готов был придти к такому выводу, как решил посмотреть, сколько занимает сложение строк:
s := s + 'test';
if Length(s) > 10000 then s := ''; //Чтобы не разбухала
1500 микросекунд! А разница между New и GetMem в примере со строкой была 2500!
Иными словами, расширение места под строку и копирование слова "test" занимает меньше времени, чем инициализация/финализация пустого поля типа string! Да как такое может быть?
Оказывается, вот как. Оказывается, для инициализации и финализации полей Дельфи вставляет не сам код, а вызов внутренних функций @New/@Dispose с параметром, в котором зашифровано, что именно удалять. Внутри этих функций довольно громоздкий процесс разборы параметра на части.
Попробуем сделать всё вручную! Напишем:
GetMem(rec, SizeOf(rec^))
pointer(rec.FField) := 0; //инициализация строки
rec.FField := ''; //финализация строки
FreeMem(rec);
Во-первых, дельфи могла бы догадаться, что на момент присваивания пустой строки rec.FField и так пустой. Но Дельфи этого, слава богу, не делает, и честно проверяет "if rec.FField<>nil then @LStrClr", образно говоря. Таким образом, мы выполняем все операции, необходимые для создания/очистки рекорда со строковым полем.
Время? ~1300. Меньше, чем на 100 миллисекунд отличается от простого GetMem/FreeMem. Остальные 2400 микросекунд уходят на шатания по функциям @New/@Dispose с выяснением в рантайме вещей, которые и так известны на момент компиляции.
Теперь сделать решительный вывод опять стало сложно. Получается, если делать всё действительно оптимально - то есть, вручную - то рекорды примерно в шесть раз быстрее объектов - и этот разрыв будет расти с ростом сложности! Шестикратное замедление - это уже вполне значительная разница.
С другой стороны, если пользоваться для инициализации рекордов средствами Delphi (New/Dispose), то разница всего лишь в два раза, и она будет уменьшаться с ростом сложности! Ведь чем сложнее объект, тем большую часть создания занимает инициализация, а она общая у объектов и у New/Dispose рекордов.
Во всяком случае, вывод надо сделать такой: с рекордами не стоит использовать New/Dispose, это убивает весь их выигрыш в скорости. Если же вы используете New/Dispose, то уже не очень жалко превратить рекорды в объекты. Это уже мало (в пару раз) замедлит дело. Совершенно неожиданный вывод, поскольку я всегда считал New/Dispose быстрыми обёртками над GetMem/FreeMem.
Вообще же говоря, чтобы оценить порядок временных затрат: время на создание пустого TObject примерно равно одной десятитысячной микросекунды. В общем, я скорее всё же был неправ, обвиняя объекты в медлительности. Разумеется, было бы лучше, чтобы объекты создавались без дополнительной суеты, но её не так много, чтобы это делало их использование при большой нагрузке непрактичным.
@темы: Delphi, Компьютеры
В папке Delphi есть файлик \Source\Rtl\Sys\system.pas Найдите его и поизучайте. Там как раз реализация методов _ClassCreate, _New, _GetMem и т.п. Код их достаточно прост для понимания.
Суть в том, что GetMem ТОЛЬКО выделяет память под некую структуру и ничего боле. New как вызывает GetMem для выделения памяти, так и проводит инициализацию этой памяти (просто тупо забивает этот блок нулями).
При создании объекта всё несколько сложнее. Сам объект нижнего уровня не так интересен. Вся прелесть объектов раскрывается уже в наследовании. Поэтому при создании объекта не только выделяется память, не только инициализируется (заполняется нулями), но после по таблицам виртуальных методов (для всей вложенности наследования) вызываются конструкторы данных объектов. Кроме того, сама операция присвоения вновь созданного объекта некой переменной сопровождается кодом проверки соответствия типов, а для этого опять же через таблицу виртуальных методов идёт проход по всей вложенности с определениями типов. Кроме того, вся эта конструкция оборачивается механизмом обработки исключений, чтобы в случае поднятия исключения в одном из конструкторов - корректно удалить объект и освободить память из под него.
Конечно всё это несёт дополнительные накладные расходы. Но удобство работы с объектами всё это полностью компенсирует.
На счёт теста с созданием строк. Зря вы попугались распухания памяти и добавили строчку if Length(s) > 10000 then s := ''; Теоретический максимальный размер строк в Delphi - 4 Гб. Так что попробуйте это ограничение изменить на большее, например: if Length(s) > 10 000 000 then s := ''; Всего-то порядка 10 Мб, зато сразу заметите как нелинейно вырастет время.
Если интересно - потом расскажу в чём здесь секрет.
Я знаю, я этот код в дизассемблере прочитал же, когда дерево вызовов составлял.
Суть в том, что GetMem ТОЛЬКО выделяет память под некую структуру и ничего боле. New как вызывает GetMem для выделения памяти, так и проводит инициализацию этой памяти (просто тупо забивает этот блок нулями).
Я это прекрасно понимаю, и даже написал об этом:
"Последний отличается тем, что Дельфи автоматически генерирует код, инициализирующий и очищают так называемые "сложные" поля - строки и указатели на интерфейсы"
Но вы не вполне правы. New не забивает блок нулями. Она только инициализирует строки, динамические массивы и указатели на интерфейсы. Если таких в вашем рекорде нет, New просто вызовет GetMem, ничего больше не делая. Можете проверить, написав
type TMyRecord = record
FField: integer;
end;
PMyRecord = ^TMyRecord;
var rec: PMyRecord;
begin
New(rec);
end;
В дизассемблере вы увидите, что это преобразуется в код:
mov eax, 4
call @GetMem
Больше никаких действий не делается. Никакого зануления.
но после по таблицам виртуальных методов (для всей вложенности наследования) вызываются конструкторы данных объектов
Это я тоже прекрасно понимаю. Но вызов пользовательских конструкторов - это неизбежная трата, его никак нельзя исключить. В конце концов, я сам пишу эти конструкторы! Разумеется, я знаю, сколько времени они тратят. Меня же беспокоили накладные расходы, о которых я не просил: вызовы внутренних функций Delphi.
Кроме того, сама операция присвоения вновь созданного объекта некой переменной сопровождается кодом проверки соответствия типов
Хмм, в этом случае - нет. Можно легко проверить, написав
var tmp: TObject
begin
tmp := TObject.Create;
end;
В дизассемблере вы опять же увидите только "call TObject.Create", и никаких других действий. По правде сказать, приведённое в посте дерево вызовов я как раз так и получил, так что можете ему доверять: при создании объекта действительно выполняются только указанные мной функции и ничего больше.
Проверка соответствия типов в рантайме происходит в случаях, когда вы кастуете объект вверх:
var tmp: TSomeObject;
obj: TObject;
begin
tmp := (obj as TSomeObject);
end;
Если же вы кастуете вниз (TSomeObject до TObject), то проверка выполняется в момент компиляции! Если каст невозможен, компилятор просто выдаст ошибку.
Всего-то порядка 10 Мб, зато сразу заметите как нелинейно вырастет время
Попробовал. Для предела в 100 * 1000 время = 1650. Для предела в 1000 * 1000 время = 2469. Для предела в 10 * 1000 * 1000 время = 3078. По-моему, вполне линейный рост, нет?
Если интересно - потом расскажу в чём здесь секрет.
Если вы собираетесь рассказать, что когда у строки refcount 1, дельфи по возможности расширяет её на месте, то я это тоже знаю
А вобще есть замечательные слова: надо в первую очередь оптимизировать алгоритмы, а вот когда они уже вылизаны и не дают прироста производительности - только тогда опускаться на нижний уровень и пытаться оптимизировать там.
Да-а, но не совсем. Правильно выбранные решения на нижнем уровне полезны в любом случае.
mov eax, 4
call @GetMem
Больше никаких действий не делается. Никакого зануления.
Может это после оптимизации уже...
Глянул. Действительно, для:
генерится такой код. А вот для:
Код уже совершенно другой. Как раз в виде вызова функции New, а не GetMem. Которая уже проводит инициализацию некоторых типов, при чём цепочка вызовов не совсем короткая
Хотя если объявить как:
То опять получаем простой вызов GetMem.
Какие накладные расходы вас в ДАННОМ случае беспокоят?
Про проверку типов, да тут протупил маленько.
Да-а, но не совсем. Правильно выбранные решения на нижнем уровне полезны в любом случае.
Может быть. Но вот в реальной работе пока только на 1 проблему с этим столкнулся. Когда парнишка знакомый читал данные из файла в динамический массив. данных было достаточно много. И он на каждой итерации делал увеличение размерности массива на 1. Вот тут у него дикие тормоза были. И причина как раз была в нижем уровне. Когда я ему предложил задать размер массива сходу, до начала цикла, тут да, у него получился прирост в 200-300 раз ли, если не больше. Хотя парень как раз использовал рекорды, а не классы.
Но такие случаи единичны бывают.
А вобще по опыту, если мне нужна структура только для хранения, тогда именно рекорды и использую. А вот где уже эта структура тесно увязана с независимой логической обработкой, а не дай бог нужно ещё и наследование, то классам альтерантивы нету. И пофиг, что идут накладные расходы, зато повышается читабельность кода, а она дорогого стоит.
А так ещё и от задачи нужно идти, вот честно говоря сложно задачу представляю, где надо постоянно создавать/прибивать сотни и тысячи классов. Можете пример привести?
Да. Мне интересно, вы читали вообще мой пост, который комментируете?
То опять получаем простой вызов GetMem. Скорее всего из-за того что обычный тип String = array of char, т.е. динамический массив. И переменная такого типа - указатель, который необходимо корректно инициализировать.
Нет, тут дело не в том, что string = array of char. Строки в дельфи немного сложнее (и ещё сложнее, начиная с Delphi 2009). Просто это инициализируемый тип. Кроме того, инициализируемые типы - интерфейсы, динамические массивы, варианты (variant и OleVariant) и ещё кое-что. Указатели не инициализируются, кстати. Если вы вставите
FField: TObject
илиFField: pointer
, то дополнительного кода не будет. Это потому, что указатели не reference-counted, иными словами, объекты не уничтожаются автоматически, в отличие от строк, массивов и содержимого вариантов.А "ещё сложнее" в Delphi 2009, о котором я говорил - это дополнительное поле "кодировка" перед каждой строкой. Теперь в памяти строки выглядят так:
[Кодировка: 4 байта][Число ссылок: 4 байта][Длина: 4 байта][Символы строки, каждый размером два байта, индексированные с единицы]
Все те, о которых я написал. Вызовы любых иных функций, кроме GetMem. ClassCreate, InitInstance, AfterConstruction, и т.д., результирующие в шестикратном замедлении Create.
Когда парнишка знакомый читал данные из файла в динамический массив. данных было достаточно много. И он на каждой итерации делал увеличение размерности массива на 1. Вот тут у него дикие тормоза были.
Э, ну да, конечно. Вот так надо делать:
var
Items: array of TMyRecord;
ItemCount: integer;
procedure Init;
begin
ItemCount := 0;
end;
procedure AddItem(AItem: TMyRecord);
begin
if ItemCount >= Length(Items) then
SetLength(Items, Length(Items)*2 + 5);
Items[ItemCount] := AItem;
Inc(ItemCount);
end;
Для случая, когда итоговый размер заранее неизвестен, это самый производительный способ, фактически - моментальный. Суммарные затраты на выделения памяти - log(N), затраты на проверки на каждой итерации вообще никакие.
И пофиг, что идут накладные расходы, зато повышается читабельность кода, а она дорогого стоит.
В общем, да, я с вами теперь согласен. Я полагал, что классы ещё более медленные. Но всё-таки, шестикратное замедление тоже напрягает немного.
А так ещё и от задачи нужно идти, вот честно говоря сложно задачу представляю, где надо постоянно создавать/прибивать сотни и тысячи классов. Можете пример привести?
Легко: обработка запросов через интернет. Каждый запрос кладётся в структуру, в секунду легко могут приходить тысячи запросов. Если не десятки тысяч.
Весь этот код .NewInstance делает именно создание объекта в памяти с дополнительными полями, типа ссылки на VMT, счётчика ссылок и т.п.
Далее следует реализация написанного тобой конструктора.
А вот этот блок нужен лишь если ты хочешь в момент после создания некоего объекта выполнить ряд действий. Для этого тебе надо переопределить виртуальный метод: AfterConstruction. У базового класса он просто пустой.
А вобще догадываюсь зачем здесь всё сделано не прямыми вызовами, легко оптимизируемыми, а через классовые функции и вызовы их через VMT. Ты же класс можешь создать не напрямую а по ссылке. Вот для этой поддержки вся эта обвязка кода и существует.
Глянь трассировку вызовов:
Ну и нафига тут классы вобще, раз приходят структуры?
Единственный вариант вижу, что каждый такой запрос - это по своей сути уже некий объект, который необходимо не только положить в структуру но ещё и как-то запустить, а метод запуска тоже определяется запросом. Но что-то очень сомнительно что даже в таком случае стоит выполнять и держать все эти объекты сразу. А не выстроить очередь из этих запросов и обрабатывать её потом. Но опять же - это уже вопросы оптимизации алгоритма обработки, а не выбор низкого уровня структура или класс.
Примерно так и поступает TList у себя во внутрях, только похитрее:
[Кодировка: 4 байта][Число ссылок: 4 байта][Длина: 4 байта][Символы строки, каждый размером два байта, индексированные с единицы]
Ну раньше кодировки не было и символы были однобайтовые. А всё остальное так и было. Ещё в конце дополнительный символ - признак конца строки (с кодом 0) для простого преобразования к PChar. Во всяком случае в Delphi 5 был. Думаю и здесь так же. А фактически в 2009 как раз вместо String используется то что было раньше WideString. И слава богу, что наконец-то пришли к ней.
А вот структуру перед строкой я онаружил в исходниках
Так что всё таки там не кодировка лежит.
Приходят просто данные. Как их хранить - это уже решать программисту. Очень часто удобно хранить с наследованием. Представьте:
type TBasicMessage = class
Size: integer;
Type: integer;
end;
TAuthMessage = class(TBasicMessage)
Size: integer; //==SizeOf(TAuthMessage)
Type: integer; //==MSG_AUTH
Login: string[64];
Pass: string[64];
end;
TTextMessage = class(TBasicMessage)
Size: integer; //==SizeOf(TTextMessage);
Type: integer; //==MSG_TEXT
Text: string[255];
end;
(Текст для простоты передаётся по-идиотски, не обращайте внимания). Такое не сделаешь с рекордами, а между тем этот приём дизайна сообщений очень удобен и часто используется.
Примерно так и поступает TList у себя во внутрях, только похитрее:
Ну, да, как приращивать длину - тут может быть много способов. Можно константой, можно удваивать, можно постоянно растущим блоком (т.е. +20 +40 +60 +80 итд). Лучше выбирать в зависимости от того, как планируется использовать. Но с нынешними объёмами памяти и с виртуальной памятью, к тому же, по-моему, просто удвоение уже можно не стесняться использовать.
Ещё в конце дополнительный символ - признак конца строки (с кодом 0)
Да, про него забыл.
И слава богу, что наконец-то пришли к ней.
Ох я бы не сказал. Мне нынешний стринг кажется тяжеловесным, в основном из-за этого поля кодировки, которое туда впихнули не пришей козе нога. Я бы предпочёл, чтобы стринг оставили старым, AnsiString, поскольку в куче кода подразумевалось именно AnsiString под ним. А компоненты перевели на новый UnicodeString прямо.
-- himself (пишу с работы, лень логинится)
Даа, но у него нет завершающего нуля и индексируется он не с единицы.
Или может в 2009 отличается эта структура?
А где вы смотрели? Вообще да, отличается, НАЧИНАЯ с 2009. Если вы смотрели в 2010, у вас должна такая же быть.
Кодировка там есть, это точно, я в манах читал.
-- himself
Либо шашечки, либо ехать.
Чем не вариант для данной ситуации?
Я бы предпочёл, чтобы стринг оставили старым, AnsiString, поскольку в куче кода подразумевалось именно AnsiString под ним. А компоненты перевели на новый UnicodeString прямо.
Да не думаю, что можно было бы поменять токо в одном месте. Они щас тупо тип переопределили и везде отразилось. Зато щас ВСЕ строки Unicode и меньше будет у начинающих программистов проблем с кодировками.
А где вы смотрели?
\Source\Rtl\Sys\system.pas - тут же откопал. Тип StrRec. Я до сих пор на 5 делфе сижу, так что было бы интересно глянуть как они его в 2009 определили.
Это несерьёзно в больших проектах. Даже в скромного размера сервере у меня ~ 50 различных типов сообщений, и они разбросаны по разным файлам. Представьте себе, какой это будет кейс! Даже дельфийский TVarRec пожухнет в палящем сиянии этого солнца пустыни. Так жить нельзя.
Кроме того, двойное наследование в кейсе уже вообще не сделать.
\Source\Rtl\Sys\system.pas - тут же откопал.
Я имел в виду версию дельфи, ну неважно, вы её тоже привели.
В 2010 так:
StrRec = packed record
codePage: Word;
elemSize: Word;
refCnt: Longint;
length: Longint;
end;
--himself
А вот гуд ли такие вещи для больших проектов? Как раз очень тяжело разгребать чужой проект, где тесно связанные вещи так сильно размазаны по модулям?
Я бы в данном случае скорее уж сделал в каком-нибудь виде типа:
Где Data - именно блок сообщения как он пришёл по сети. Постарался бы написать типы отдельных сообщений в виде record, которые бы по возможности мапились на данный блок данных. (Плохо если тама идут строки не фиксированной длины, с признаком окончания строки. Такие конечно структуры не отмапишь.) И по месту работы с ними просто этот блок данных представлял как указатель на структуру и всё.
Тяжело разгребать, когда всё сброшено в одну кучу. Наборы сообщений разных подсистем должны быть разложены по разным файлам.
Я бы в данном случае скорее уж сделал в каком-нибудь виде типа:
Можно, но для чтения вам потребуется лишнее выделение памяти.
GetMem(@sg, SizeOf(msg^));
ReadDataFromNetwork(msg, SizeOf(integer)*2);
GetMem(msg.Data, msg.Size);
ReadDataFromNetwork(msg.Data, msg.Size)
В моём же случае чтение линейное:
var base: TBaseMessage; //выделяется в стеке
begin
PeekDataFromNetwork(@base, SizeOf(TBaseMessage));
GetMem(msg, base.Size);
ReadDataFromNetwork(msg, base.Size);
end;
Понятно, что всё это можно оптимизировать так, что выделений памяти в среднем вообще не понадобится (временный буфер на каждый поток с приростом по мере необходимости), но лишний pointer всё равно неудобен.
Кроме того, это не решает проблемы наследования Data:
type TBaseMessage = record
Size: integer;
Type: integer;
end;
TSessionMessage = record
Size: integer;
Type: integer;
SessionID: int64;
end;
TSessionStatusMessage = record
Size: integer;
Type: integer;
SessionID: int64;
Status: integer;
end;
Структуры объявляешь в одном месте (некий модуль который содержит лишь описания всех структур сообщений), а вот работа с отдельными структурами может быть уже в разных файлах раскидана. Зато если тебе надо сразу глянуть ВСЕ сообщения - тебе не надо лазить по модулям выискивая: а какие ещё сообщения поддерживает твой сервер.
Идея c Data: Pointer была в том, чтобы потом объявить более простые структуры без Size и Type и тот указатель на Data кастовать к этим структурам (конечно их тоже друг от друга не отнаследуешь
А при вашем методе чтения - классы вобще никак и не задействуешь.
Вот таким образом уже сходу сообщение не прочитаешь, если msg является классом.
Я кончено когда то в детстве и так читал. В бытность свою студентом сохранял и восстанавливал данные класса таким методом на турбо сишнике.
type TDrink = class(TObject)
Amount: integer;
procedure Produce; virtual;
end;
TCoffee = class(TDrink)
Kind: TCoffeeKind;
procedure Produce; override;
end;
То экземпляры класса будут разложены в памяти так:
a: TDrink;
b: TCoffee;
a -> [TDrink_vptr; Amount];
b -> [TCofee_vptr; Amount; Kind];
Поэтому фактически так читать можно.
Во-первых, я уже приводил пример, что на турбо сишнике сохранял и восстанавливал объекты. Там по адресу экземпляра объекта первые 4 байта - были указателем на таблицу VMT, и только потом шла структура. И никто тебе не гарантирует как это поменяется или сохранится в следующей версии компилятора.
Во-вторых, надеюсь не стоит говорить о выравнивании в памяти? А если, в пакете у тебя одно из полей 1 байт? Для структуры ещё можно указать опцию packed, а для класса что ты укажешь?
Потому для классов таки методы чтения 1 блоком в память не приемлемы.
Что и куда пишется при создании объекта - можно оценить, прогнав под отладчиком: TObject.InitInstance, так на чтение тяжело воспринимается, а прогонять влом.
Если это не поменялось за столько лет, вероятно, не поменяется и в ближайшее время. Впрочем, я согласен, что это не очень хороший трюк. Одно из неприятных последствий того, что в дельфи нет классов в смысле C++.
а для класса что ты укажешь?
{$A-}
Поясни, что в Delphi классах не так?
{$A-}
В справке как-то невнятно прописано: The $A directive controls alignment of fields in record types. In the {$A+} state, fields in record types that are declared without the packed modifier are aligned.
Создаётся ощущение, что эта директива относится исключительно к типу record. К тому же, мы же за скорость боремся, и из-за этого вводим такое выравнивание, чтобы грузить одним вызовом сразу блок в память. А отключая выравнивание - проигрываем на скорости доступа к полям объекта.
Так что смысла всё таки в такой задаче использовать классы - не видно вобще.