dlangui icon indicating copy to clipboard operation
dlangui copied to clipboard

Resize windows optimization [RU]

Open kildin opened this issue 6 years ago • 20 comments

С учетом прямо таки ударных темпов оптимизации UI в последнее время, что не может не радовать, вынесу на обсуждение еще одно предложение. В режиме программного рендера (за OpenGL / SDL2 режимы не возьмусь говорить) буфер окна представляет собой массив 4 байта на пискель. Соотвественно при ресайзе окна каждый раз "перестраивается" и массив. В идеальных "сферических" условиях при изменении размера окна с изначального размера 100x100 на 200x200 будет 100 ресайзов, на практике событий будет меньше. В D массив - это непрерывный участок памяти, поэтому при увеличении размера массива вполне может оказаться что следующая ячейка памяти уже занята и новый массив чисто физически не может "влезть" в старый участок памяти и будет размещен в новом. При этом не забываем, что есть еще GC который строго говоря не обязан проводить уборку мусора при каждом изменении размера массива. Также путем несложных математических расчетов можно прикинуть, что в FullHD режиме экрана размер массива при маскимальном размере окна составляет порядка 8 мегабайт. В итоге сжатие / растягивание нескольких окон приложения приводит к ощутимому увеличению размера потребляемой памяти и по своей сути подобные операции имхо являются грубым надругательством над GC. Отсюда есть рац. предложение ввести опциональный режим (на уровне всего приложения или конкретно создаваемого окна), когда буфер окна при создании будет сразу равен макимальному разрешению экрана. И отрисовывать не весь буфер, а только какой-то его срез, равный фактическому размеру окна. Понятно что есть еще "особые случаи", когда окно можно растянуть больше чем физический размер экрана, но тогда в этих особых случаях массив можно и динамически увеличивать по факту возникновения такой ситуации.

kildin avatar Oct 18 '17 10:10 kildin

Возможно достаточно заменить в разных DrawBuf массив на указатель и malloc. Остальное поведение трогать не стоит, но как улучшение для дополнительного контроля можно было бы добавить для буферов окон необязательную функцию обработчик которая вызывалась бы и запрашивала нужно ли сжимать буфер в данном случае или оставить как есть. (пример ниже)

Потому что такие вещи не имеют единственного правильного решения и зависят от ситуации, разработчик сам решит что делать в каком случае.

Более того для окна обычно будет 2 буфера, один как вы сказали массив в который рисуется текущий кадр, и (системный) тот который вы видите. В случае с растянутым на весь экран окном в 4к это уже 2 буфера по ~30 Мб (только что проверил example1 потыкав кнопочки и вкладки разошлось почти на 100 Мб), и с этим ничего не сделать. Если использовать minimal конфиг то для окна используется уже не массив а системное DIB изображение https://github.com/buggins/dlangui/blob/master/src/dlangui/platforms/windows/win32drawbuf.d#L147

в этом случае(конфиг minimal) используется уже всего ~40 Мб, но это не значит что память под буфер нигде не выделяется


пример обработчика

enum BufResizeOp
{
Shrink,
Keep,
//Grow ?
}

alias resizeDrawBufHandler = BufResizeOp function(DrawBuf buf, Point currentSize, Point destSize);

или

alias resizeDrawBufHandler = void function(DrawBuf buf, Point currentSize, Point destSize, out bool allowShrink);

вызываем например здесь https://github.com/buggins/dlangui/blob/8a0da9f8dbce08af0ab3417abc979f387b0ce941/src/dlangui/graphics/drawbuf.d#L1501

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

данный обработчик используется лишь подсказка и не должен мешать нормальной работе

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

Наиболее вероятный случай использовать его исключительно для окна(и холстов для рисования если такие имеются), тогда нужен способ получить окно из буфера или просто передавать его как параметр

Superbelko avatar Oct 18 '17 14:10 Superbelko

Во-первых, учитывая общее состояние dlangui я не уверен что это не является преждевременной оптимизацией. Во-вторых, заменять массив указателем точно не стоит никогда - зачем уходить так низко? На данный момент озвученная проблема решается довольно просто добавлением одной строчки:

draw_buffer.reserve(some_rather_big_value);

и все. Мы избежим перевыделения памяти при изменении размера, поскольку она уже зарезервирована. При этом если понадобится еще больший размер - он будет выделен. Все будет обработано штатным рантаймом.

drug007 avatar Oct 18 '17 14:10 drug007

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

  1. Делать конечно лучше опционально, конечные задачи бывают разные и каждый пусть сам решает в каком случае какой вариант выделения памяти лучше подойдет для данной задачи.

  2. Окно может быть не "Resizable", для таких окон резервировать память с запасом не имеет никакого смысла.

  3. Ранее уже было мной озвучено предложение для введения у окон свойств maxWeidth / maxHeight https://github.com/buggins/dlangui/issues/455 Обсуждение можно сказать зависло в воздухе. И если все-таки эти свойства будут добавлены в UI, при резервировании памяти "авансом" нужно будет учитывать и их значение. Больше чем там выставлено резервировать также не имеет никакого смысла.

Относительно замены массива указателем, кстати может и имеет смысл. Попиксельная прорисовка может быть "узким горлышком", особенно на разарешениях 4к и каждая лишняя сгенерированная компилятором проверка границ массива может в итоге стоить слишком дорого.

2 Superbelko А как вцелом ведет себя UI при сборке с софтовым рендером под 4К? Насколько ощутимо падает скорость? Вадим говорил, что и под обычный FullHD были жалобы на быстродействие, но у меня комп достаточно мощный чтобы на нем что-то тормозило, а монитор 1920x1080. Проверить бы на среднестатистическом офисном целероне с интегрированным видео. Тогда и вопрос нужно ли менять массив на указатель отпадет сам собой.

kildin avatar Oct 18 '17 15:10 kildin

А ведь в релиз билде проверка границ массива не производится. @drug007, попробовал сейчас reserve, почему-то не работает, хотя в пустой тестовой программе всё нормально. Попробовал просто

    override void resize(int width, int height) {
        ...
        if (_buf.length < _dx * _dy)
            _buf.length = _dx * _dy * 2;
        ...
    }

Ускорения я особо не заметил, зато память перестала скакать при ресайзе. Идея имеет смысл, если придумать красивую реализацию.

dayllenger avatar Oct 18 '17 16:10 dayllenger

Заменять массив указателем нет смысла, потому что из массива всегда можно получить полноценный указатель либо через array.ptr, либо (в @safe коде) через &array[0]. Также если необходимо избежать проверки границ массива это можно спокойно сделать через array.ptr[index] - все, никакой проверки, вообще.

@dayllenger А какой результат ожидали? Если я правильно понимаю, чтобы был эффект от reserve необходимо чтобы буфер не менялся. Т.е.:

ubyte[] arr;
arr.reserve(2^^20);
arr = other_arr; // все, здесь эффект от `reserve` кончился, так как `arr` теперь ссылается
                 // на другой массив

Идея при изменении размера выделять память с запасом безусловно хорошая. Настолько хорошая, что она уже реализована в рантайме, в родных динамических массивах, зачем придумывать то, что уже реализовано? Но если хочется, то вместо коэффициента 2 лучше брать золотое сечение ~1.618. Дополнительная оптимизация - при маленьких размерах массива в начале выделять память фиксированного размера в 512 байт например чтобы сократить количество выделений памяти, а начиная с какого-то размера переходить на множитель.

drug007 avatar Oct 18 '17 19:10 drug007

@drug007 Дело в другом, там если уменьшить длину массива, то резерв обнулится. Сам массив один и тот же.

dayllenger avatar Oct 18 '17 20:10 dayllenger

Пользуюсь динамическими массивами регулярно, но понял, что на 100% не уверен как они точно работают под капотом, поэтому нужно проверить все это дело.

Вообще я бы начал оптимизацию с архитектуры dlangui в целом. Поскольку это порт плюсовой версии, тем более с длинной историей, да еще проект одного человека (за что ему большой респект, безусловно), архитектуру стоило бы перетрясти.

drug007 avatar Oct 18 '17 20:10 drug007

А ведь в релиз билде проверка границ массива не производится.

Вот кстати новость для меня, брал информацию из книги Александриеску, там он как раз приводил обоснования использования указателей именно в том, что это более быстрый способ, по сравнению с массивами, из-за того что компилятор вставляет проверки границ, обеспечивая гарантию безопасности памяти. Я не так давно работаю с D, поэтому и выступаю тут исключительно с предложениями, сам лезть в код UI или IDE с напильником пока объективно не готов. Попробовал взять оптимизировать отдельную функцию програмной отрисовки в буфере, сделал с десяток разных вариантов и понял что весь предыдущий багаж опыта оптимизации в других языках нужно оставить в этих других языках :) В случае с D и даже наверное будет правильнее сказать с DMD проверенные годами методы зачастую дают ровно противоположный ожидаемому эффект.

kildin avatar Oct 18 '17 21:10 kildin

Вариант выделения памяти с запасом по коэффициенту от текущего много где используется. Например в СУБД, когда данные заполняют физический файл и нужно этот файл расширять. Но не уверен, что это правильный подход для работы с окнами. Могу предложить еще один алгоритм выделения памяти. При создании окна выделять память ровно под размер окна, при уменьшении юзером размер массива не трогать, а вот при увеличении сразу резервировать под максимальный размер экрана. Возможно этот вариант будет "золотой серединой".

kildin avatar Oct 18 '17 21:10 kildin

В таком случае вопрос к экспертам: как узнать максимальный размер экрана?

dayllenger avatar Oct 18 '17 21:10 dayllenger

Тут очевидно 2 варианта, либо средствами стандартной библиотеки языка. Если таковых не предусмотрено, то через API конкретной ОС.

kildin avatar Oct 18 '17 22:10 kildin

Под win, через API:

// size of screen in pixels including size of taskbar
int width  = GetSystemMetrics(SM_CXSCREEN);
int height = GetSystemMetrics(SM_CYSCREEN);

// size of screen in pixels excluding size of taskbar
int width  = GetSystemMetrics(SM_CXFULLSCREEN);
int height = GetSystemMetrics(SM_CYFULLSCREEN);

https://msdn.microsoft.com/ru-ru/library/windows/desktop/ms724385(v=vs.85).aspx

kildin avatar Oct 19 '17 01:10 kildin

@kildin Проверял в 4к example1(0.9.162) как наиболее близкий по сложности к реальному приложению. в дебаге понятное дело все плохо, в релизе:

minimal-x64 - в целом можно использовать но производительности явно не хватает на размере больше чем FullHD. за исключением grid примера, здесь даже в FulHD заметна задержка при выделении ячеек, а в 4к это в целом неюзабильно с точки зрения пользователя, на глаз 5-8 кадров/сек. при бешеном растягивании окна ситуация чуть лучше, задержки заметны, но тут на глазок уже 10 кадров/сек при использовании LDC2 ситуация чуть чуть получше но пользователи это все равно не оценят.

default-x64 - он же OpenGL, даже пример с таблицей уже идет терпимо , что удивительно учитывая что каждый кадр сцена и буферы могут перестраиваться. основные тормоза при диком ресайзе окна, но даже здесь это не так страшно как в минимале. а используя LDC2 уже вполне можно жить. сложности только в таблицах.

Касательно минимал конфига, в моем случае используется быстрый процессор с 4-4.2ГГц, и те самые проблемы и паузы со сборкой мусора в различных управляемых средах о которых все говорят у меня не так заметны как даже на intel i7 2600, возможно также за счет более быстрой памяти. Поэтому скорее всего на офисный селерон такое можно даже не пытаться собирать.


Не смотрел как работает measure в разных виджетах, но вероятно можно оптимизировать и его чтобы не вычислять один и тот же размер если виджет не менялся

Superbelko avatar Oct 19 '17 06:10 Superbelko

Переделал буфер ColorDrawBuf на выделенный через malloc. Теперь большой DrawBuf не замедляет GC. Изменения размеров буфера в Resize редко происходят.

buggins avatar Oct 19 '17 11:10 buggins

Вадим, в 0.9.169 эти изменения вошли?

kildin avatar Oct 19 '17 15:10 kildin

ага

buggins avatar Oct 19 '17 19:10 buggins

Сравнил сборки Example1 из 168 и 169 в варианте minimal. Какой-то разницы честно говоря не увидел. Расход памяти идентичный и при ресайзах размер памяти все также "скачет".

kildin avatar Oct 19 '17 19:10 kildin

Разница заметна, в обеих конфигах стало быстрее, и теперь при ресайзе окно почти успевает за мышью

Superbelko avatar Oct 20 '17 02:10 Superbelko

значит скачет не из-за буфера, а в процессе layout/measure или даже отрисовки.

buggins avatar Oct 20 '17 04:10 buggins

Забыл, что в Win32 свой drawbuf - Win32ColorDrawBuf Там пересоздается bitmap.

buggins avatar Oct 20 '17 05:10 buggins