Категория: Компьютеры

Заметки о программировании и на околокомпьютерные темы

Вложенные категории: Delphi, Игры

О паскале и объектах #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: Эксперименты показали, что я в этом мнении более-менее неправ.

Новости с браузерного фронта

В Опере 10.5 новый Javasсript-движок с компиляцией в нативный код и новый графический движок с аппаратным ускорением. В результате Опера 10.5 в шестнадцать раз быстрее Оперы 9.5 в тесте SunSpider, и обходит в нём все браузеры, включая Chrome (славящийся именно быстрым JS).
Кроме того, у неё 100/100 в Acid3.

Кроме того, поддерживаются HTML5 теги video и canvas. Ускорение работает прекрасно, анимация на канвасе даёт десятки FPS там, где Хром и Файрфокс выдавливают из себя два-три.

В общем, Опера уделала всех и вся на поле боя, докуда дотянулись руки.

You know you’re screwed when…

У меня на компе две винды, XP и preview Windows 7. Для тех, кто не знает: превью-релиз семёрки вышел прошлой весной, и распространялся бесплатно. Обещалось, что он будет работать бесплатно год, после чего выключится, и придётся купить полную версию. Таким образом Майкрософт хотела заранее протестировать систему на ошибки.

Так вот, решил я переустановить XP. Перезагрузился в семёрку, забэкапил все данные, тяжело вздохнул – и удалил старую копию XP с диска.
“Вы точно хотите удалить папку?”
“В папке есть файлы, помеченные флагом read-only, вы точно хотите их удалить?”
“В папке есть защищённые системные файлы, вы точно хотите их удалить?”
“Последний раз спрашиваю: удаляем?”
“Удаляй!”

“Удалено. Кстати говоря, вы пользуетесь превью-версией Windows 7, а срок её действия только что истёк. До свидания”. Система выключается, щёлк – чёрный экран.

И я остаюсь с компьютером, на котором нет ни одной рабочей системы :)

На самом деле, семёрка работает и после истечения срока, просто время от времени злится и вырубается. Всё равно заставила понервничать.

С Баша

Пьер Косталь: Стоит объяснить коллеге, что “данные следует передавать в стрингах” – как наступает тишина в автобусе :)
Разумеется, поскольку ни один нормальный человек не передаёт данные в стрингах!

DirectX

Есть известная шутка о регэкспах: допустим, у вас возникла проблема, и вы прикидываете, как бы её решить. Тут вам приходит в голову: о, регэкспы!
Теперь у вас две проблемы.

В DirectX иначе: у вас по-прежнему одна проблема, потому, что прошлая проблема в сравнении с новой покажется ерундой…

Читая Accelerando…

Мыслепотоки, распределённо исполняющиеся на близлежащих процессорах – это ладно, но кое-что весёлое можно сделать уже сейчас. Программу для мобильников с GPS, действующую так: любая фотка автоматически загружается на сервер, который ищет в базе фотки в пределах 12 метров (точность GPS) вокруг, поворачивает их так и сяк, чтобы угадать, к чему относится нынешняя фотка.
И добавляет в базу под нынешним числом.

Профиты:
1. Появляется смысл щёлкать своей идиотской фотокамерой!
2. У любого населённого места на планете есть дофига фотографий со всех возможных сторон, по которым можно даже, наверное, восстановить 3d-версию места.
3. Установив место, мобильник коннектится к гугл-мапс, и рисует на экране “это столовая”, “это автобусная остановка”, “автобусы идут туда и туда”.

Пиарю-пиарю свою утилиту для добавления файлов в анидб: anidbtool.

Работает так. Из командной строки:

anidb mylistadd D:\Anime\Toradora\*.*
anidb mylistadd /S D:\Anime\*.mkv

Из проводника: правой клавишей по файлу или папке, “Отправить” -> “Добавить в Anidb”.

Добавляю функциональность по запросам. Если у вас есть затаённая мечта, которой вы с детства стеснялись поделиться, говорите мне.

Быстрый поиск

Быстрый поиск такая удобная штука, что его стоило бы изобрести на тысячу лет раньше. Привыкнув в Опере, на автомате пытаюсь жмакать “/” и искать файлы в проводнике.

Об авто-обновлении

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

Это почти всегда бесполезно, (…)

поскольку пользователь обычно запускает программу не из праздного любопытства. Он запускает её, чтобы сделать что-то конкретное. Иными словами, у него есть цель. Ему совсем не по душе отвлекаться, выключать программу, скачивать и устанавливать новую версию.

Чтобы решить эту проблему, автоматическое обновление стараются сделать как можно более простым. Программы скачиваются сами собой, устанавливаются сами собой, некоторые даже перезагружаются в новую версию автоматически. Но! Но!
Всё равно каждая задаёт пользователю идиотские вопросы.

“Доступна новая версия, хотите скачать и установить?”
Или, в хамовато-наглой версии Adobe Updater:
“Я без спросу скачал обновления, хотите установить?”

Ответ на это всегда будет “нет”. Потому, что пользователь не дурак. Он понимает, что если бы установка действительно была простой и быстрой, его бы не спрашивали. Обновление бы просто установилось само собой. (В конце концов, программисты ведь тоже не дураки, так?) А раз его спрашивают – ожидаются проблемы и перемены. А ему сейчас не до проблем, он другим занят.
И пользователь жмёт “Нет”.

Чтобы модель автоматического обновления работала, она должна удовлетворять трём условиям. Во-первых, при установке программма должна спросить, желает ли пользователь обновлять программу автоматически. И если пользователь не желает, не обновлять и больше не приставать (привет, Adobe). Во-вторых, если пользователь согласился, отныне и впредь скачивать и устанавливать обновления настолько незаметно, насколько это возможно.

Это значит – никаких подтверждений. Никаких всплывающих окон. Никаких значков “идёт обновление” возле часов. Никаких “обновление ещё не докачано, хотите выйти?” Никаких “установите и перезагрузите”. Полностью автоматически. Само нашло, само скачало, само установило при следующем запуске и загрузило уже новую версию. Пользователь должен вообще ничего не замечать.

И, в третьих, необходимо разделять обновления на ветки. “Маленькие” обновления, которые устанавливаются автоматически, не должны менять ничего серьёзного в интерфейсе. Только добавлять новые функции или исправлять ошибки. Никаких “автоматически устанавливает google toolbar”. Никаких “в новой версии добавили рекламу”. Если производитель софта хоть раз допустит подобное, пользователи просто выключат его авто-апдейт. Вот и всё, чем кончится.
Крупные же обновления нельзя устанавливать автоматически, и лучше даже не предлагать к установке до тех пор, пока нынешняя ветка не будет закрыта. В крайнем случае, предложить один раз – и лучше не всплывающим окном и не сообщением с кнопкой “OK”, а небольшой заметкой где-нибудь в статусной строке или на панели подсказки.