Записи за месяц: April 2010

Ололо

Лектор по русскому разослал всем письмо, в котором сообщил, что для допуска к зачёту нужно пройти тест из 60 вопросов на его сайте. “Пробовать можно только один раз, кто не пройдёт – допущен не будет”.

Через три дня лектор по философии (!) разозлал всем правильные ответы на тест по русскому, с припиской: “занимайтесь лучше философией”.

Приезжали товарищи из ii-subs, намахали вместе 18 км по Москве.

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

Всё так близко.

Bachelor of Science

В Америке оканчивающие вуз получают степень Bachelor of Science, или “Бакалавр наук”, сокращённо BS. В разговоре это используется так:
“I just got my BS in philosophy and I’m looking for a work”
“Try Mac, they’re always hiring”

Удобно, что BS – это также известное сокращение для bullshit, что примерно значит “пустой трёп”, “чушь собачья”.

Bonus track. Также в английском языке популярно слово “major”, которое в качестве глагола значит “специализироваться” или “получать высшее образование в области”:
“I decided to major in literary arts… Am I going to have trouble finding work?”
“Yeah, all the burger seller positions are usually taken by philosophers”.

О паскале и объектах #2

Я написал предыдущий пост, и знакомый меня справедливо спросил, а какие же именно функции вызываются при создании объекта? Проверить несложно!
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.

Но это неправда.
Копейки очень быстро собираются в рубль. Классами нельзя играть с такой же лёгкостью, как рекордами. Их не жалко в ситуациях, которые возникают редко: при создании приложения, открытии окна, загрузке файла. Но при обслуживании запроса по сети? Уже нет. Создавать класс на каждый запрос – слишком дорого. А если их будет тысяча в секунду? А десять тысяч?

Остаётся использовать для таких задач рекорды. Но тогда пропадает вся полезность классов! Из объектов они превращаются в подобие неймспейсов: не воплощают функциональность элементов, а разделяют код.

Delphi очень нужны наследование и vtable рекордам. Ну или возможность сделать класс lightweight, чтобы он не вызывал всю эту борландовскую ерунду при инициализации. Чтобы стандартное Create было одним выделением памяти + занулениями, как с рекордом.

UPD: Эксперименты показали, что я в этом мнении более-менее неправ.