Размер кэшлинии и страницы памяти
Для x86/x64 архитектуры:
* кэшлиния: 64 байта
* страница: 4Кб
Можно увеличить до 4Мб если активировать large pages
Для ARMv7 архитектуры:
* кэшлиния: 64 байта
* страницы: 4Кб, 64Кб, 1Мб
Для ARMv8 архитектуры:
* кэшлиния: 64 байта
* страницы: кратные 4Кб, 16Кб, 64Кб в зависимости от ЦП.
источник
Для Mac M1 (ARMv8):
* кэшлиния: 128 байт
* страница: 16Кб
Зачем нужно знать размер кэш линии и так понятно (DOD, lock-free, false sharing), а вот размер страницы памяти нужен не так часто.
В некоторых рекомендациях по оптимизации говорят, что копирование между страницами памяти работает быстрее.
Еще есть нюансы по работе встроенного аллокатора в C++, он выделяет страницу памяти и разбивает ее на мелкие блоки, при этом аллокация страницы памяти намного медленее, чем поиск свободного блока.
#blog
Для x86/x64 архитектуры:
* кэшлиния: 64 байта
* страница: 4Кб
Можно увеличить до 4Мб если активировать large pages
Для ARMv7 архитектуры:
* кэшлиния: 64 байта
* страницы: 4Кб, 64Кб, 1Мб
Для ARMv8 архитектуры:
* кэшлиния: 64 байта
* страницы: кратные 4Кб, 16Кб, 64Кб в зависимости от ЦП.
источник
Для Mac M1 (ARMv8):
* кэшлиния: 128 байт
* страница: 16Кб
Зачем нужно знать размер кэш линии и так понятно (DOD, lock-free, false sharing), а вот размер страницы памяти нужен не так часто.
В некоторых рекомендациях по оптимизации говорят, что копирование между страницами памяти работает быстрее.
Еще есть нюансы по работе встроенного аллокатора в C++, он выделяет страницу памяти и разбивает ее на мелкие блоки, при этом аллокация страницы памяти намного медленее, чем поиск свободного блока.
#blog
Как у меня сделана проверка на корректность синхронизаций.
Для этого я написал логгер команд, который выдает читаемый лог вызовов Vulkan комманд и результат не меняется в зависимости от запусков, что позволяет следить за изменениями. Но все синхронизации придется один раз вручную проверить на корректность.
Пример лога
Исходники тут
Другой вариант - запустить vkconfig и включить полную валидацию синхронизаций - Synchronization preset.
Guide to Vulkan Synchronization Validation
Либо из кода через расширение VK_EXT_validation_features включить VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT.
Так как VK_EXT_validation_features - расширение слоя валидации, то оно не указано в vkEnumerateInstanceExtensionProperties.
Пару лет назад я тестировал открытые движки и фреймворки на правильность синхронизаций и слой валидации легко находил ошибки. Из чего я сделал вывод, что никто не трогал настройки слоев валидации. Только мой FG автоматически расставлял синхронизации без ошибок.
#blog #vk
Для этого я написал логгер команд, который выдает читаемый лог вызовов Vulkan комманд и результат не меняется в зависимости от запусков, что позволяет следить за изменениями. Но все синхронизации придется один раз вручную проверить на корректность.
Пример лога
Исходники тут
Другой вариант - запустить vkconfig и включить полную валидацию синхронизаций - Synchronization preset.
Guide to Vulkan Synchronization Validation
Либо из кода через расширение VK_EXT_validation_features включить VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT.
Так как VK_EXT_validation_features - расширение слоя валидации, то оно не указано в vkEnumerateInstanceExtensionProperties.
Пару лет назад я тестировал открытые движки и фреймворки на правильность синхронизаций и слой валидации легко находил ошибки. Из чего я сделал вывод, что никто не трогал настройки слоев валидации. Только мой FG автоматически расставлял синхронизации без ошибок.
#blog #vk
Еще в прошлом году планировал написать большую статью про архитектуру асинхронного движка и игры на нем, но сама архитектура часто корректировалась, так как у меня просто не было опыта в подобном.
Сейчас в планах выкладывать части статьи в виде коротких заметок, из чего потом соберу большую статью.
#blog
Сейчас в планах выкладывать части статьи в виде коротких заметок, из чего потом соберу большую статью.
#blog
Профилирование.
Тут все сильно отличается, с одной стороны разбивая код на таски я получил возможность легко написать свой профайлер, достаточно вставить замеры до и после вызова таска. Также каждый таск подписан, что упрощает привязку данных профайлера к коду, не нужно вставлять макросы в функции или использовать VTune API.
С другой стороны без визуализации сложно понять что происходит внутри кадра и в каком порядке выполняются таски. При этом обычный профайлер начинает показывать время работы планировщика тасков, что замусоривает результаты, особенно на демках, где нет большой нагрузки на потоки.
#blog #engine
Тут все сильно отличается, с одной стороны разбивая код на таски я получил возможность легко написать свой профайлер, достаточно вставить замеры до и после вызова таска. Также каждый таск подписан, что упрощает привязку данных профайлера к коду, не нужно вставлять макросы в функции или использовать VTune API.
С другой стороны без визуализации сложно понять что происходит внутри кадра и в каком порядке выполняются таски. При этом обычный профайлер начинает показывать время работы планировщика тасков, что замусоривает результаты, особенно на демках, где нет большой нагрузки на потоки.
#blog #engine
Запись команд на Vulkan и Metal.
В Vulkan все просто - есть буфер команд, он заполняется и отправляется на ГП.
В Metal немного сложнее - запись команд идет через энкодеры, переключение между которыми не бесплатное.
Каждый энкодер записывает команды, которые могут выполняться параллельно с другими энкодерами, это решает внутренний фреймграф.
У меня в движке за основу взяты энкодеры из Metal, получились контексты: DrawCtx, GraphicsCtx, ComputeCtx, TransferCtx, ASBuildCtx, RayTracingCtx.
Для Vulkan это также полезно, так как на мобильных архитектурах переключения `graphics -> compute -> graphic`s работают неоптимально и их надо избегать, также разные контексты содержат независимые друг от друга этапы, а значит они могут выполняться параллельно.
Контексты можно использовать только внутри таска
1. Создается массив командных буферов -
2. Создается
3. Внутри таска создается контекст, он принимает ссылку на таск.
4. Записываются команды.
5. В конце, командный буфер передается в
6. Когда все командные буферы записаны и переданны в
Аналогично работает параллельное рисование через
Софтварный командный буфер.
В Vulkan и Metal командный буфер может использоваться только в одном потоке. Если не ограничивать количество потоков рендера, то может создаться много командных буферов, и это особенно накладно, если записывается всего несколько команд. Намного удобнее записать команды в память и в одном потоке записать их в нативный командный буфер.
Интерфейсы контекстов тут
#blog #engine
В Vulkan все просто - есть буфер команд, он заполняется и отправляется на ГП.
В Metal немного сложнее - запись команд идет через энкодеры, переключение между которыми не бесплатное.
Каждый энкодер записывает команды, которые могут выполняться параллельно с другими энкодерами, это решает внутренний фреймграф.
У меня в движке за основу взяты энкодеры из Metal, получились контексты: DrawCtx, GraphicsCtx, ComputeCtx, TransferCtx, ASBuildCtx, RayTracingCtx.
Для Vulkan это также полезно, так как на мобильных архитектурах переключения `graphics -> compute -> graphic`s работают неоптимально и их надо избегать, также разные контексты содержат независимые друг от друга этапы, а значит они могут выполняться параллельно.
DrawCtx
- только команды рисования, работает для одного сабпасса, соответствует MTLRenderCommandEncoder.GraphicsCtx
- только для манипуляций с рендер пассами, для Metal это вызовы MTLCommandBuffer::renderCommandEncoder и MTLCommandBuffer::parallelRenderCommandEncoder.TransferCtx
- команды копирования, соответствует MTLBlitCommandEncoder и MTLResourceStateCommandEncoder.ASBuildCtx
- построение и копирование ускоряющих структур, сответствует MTLAccelerationStructureCommandEncoder, для Vulkan все выполняется на этапе VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR.RayTracingCtx
- только команды трассировки, нет аналогов на Metal. Для Vulkan это этап VK_PIPELINE_STAGE_2_RAY_TRACING_SHADER_BIT_KHR и VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR для пайплайнов и дескрипторов.Контексты можно использовать только внутри таска
RenderTask
, последовательность выглядит так:1. Создается массив командных буферов -
CommandBatch
.2. Создается
RenderTask
, для него выделяется уникальный индекс в массиве команд.3. Внутри таска создается контекст, он принимает ссылку на таск.
4. Записываются команды.
5. В конце, командный буфер передается в
CommandBatch
.6. Когда все командные буферы записаны и переданны в
CommandBatch
, они отправляются на ГП (queue submit).Аналогично работает параллельное рисование через
DrawTask
и DrawCommandBatch
.Софтварный командный буфер.
В Vulkan и Metal командный буфер может использоваться только в одном потоке. Если не ограничивать количество потоков рендера, то может создаться много командных буферов, и это особенно накладно, если записывается всего несколько команд. Намного удобнее записать команды в память и в одном потоке записать их в нативный командный буфер.
Интерфейсы контекстов тут
#blog #engine
Низкоуровневый рендер в движке.
При планировании архитектуры был выбор из двух вариантов:
1. Привязка к кадрам. Все команды отправляются для конкретного кадра, даже async compute не может выполняться несколько кадров - при двойной буферизации 2й кадр будет ждать завершения всех команд. Но это дает и преимущества - кадр более предсказуемый, что дает и более стабильное время кадра.
2. Без привязки к кадрам. Это позволяет делать долгие асинхронные вычисления или копирования, но делает поведение менее предсказуемым.
Я выбрал вариант с привязкой к кадрам, таким образом:
* Ресурсы не удаляются сразу, а с задержкой в 2 кадра.
* Используется общий staging buffer, выделенная память гарантированно валидна в пределах кадра, это упростило работу с память. Например в при чтении из видеопамяти в
* Ограничен максимальный размер staging buffer на кадр, так чтобы все данные успели передаться по шине PCI-E за время кадра. Таким образом ГП не простаивает в ожидании данных с ЦП и время кадра более стабильное.
#blog #engine
При планировании архитектуры был выбор из двух вариантов:
1. Привязка к кадрам. Все команды отправляются для конкретного кадра, даже async compute не может выполняться несколько кадров - при двойной буферизации 2й кадр будет ждать завершения всех команд. Но это дает и преимущества - кадр более предсказуемый, что дает и более стабильное время кадра.
2. Без привязки к кадрам. Это позволяет делать долгие асинхронные вычисления или копирования, но делает поведение менее предсказуемым.
Я выбрал вариант с привязкой к кадрам, таким образом:
* Ресурсы не удаляются сразу, а с задержкой в 2 кадра.
* Используется общий staging buffer, выделенная память гарантированно валидна в пределах кадра, это упростило работу с память. Например в при чтении из видеопамяти в
ITransferContext::ReadbackImage ()
.* Ограничен максимальный размер staging buffer на кадр, так чтобы все данные успели передаться по шине PCI-E за время кадра. Таким образом ГП не простаивает в ожидании данных с ЦП и время кадра более стабильное.
RenderTask
- используется для ассинхронной записи командного буфера.DrawTask
- используется для ассинхронной записи вторичного командного буфера.CommmandBatch
- хранит масив командных буферов и семафоров для синхронизации с другими батчами и с ЦП. Аналогичен одному вызову vkQueueSubmit.DrawCommandBatch
- хранит массив вторичных командных буферов, которые затем выполняются в IGraphicsContext::ExecuteSecondary()
.RenderTaskScheduler
- переключает кадры и управляет батчами команд: создает их и отправляет на ГП, когда они заполнятся.RenderGraph
- сделан поверх предыдущих классов, добавляет отслеживание состояния ресурсов и синхронизациями между батчами.#blog #engine
Синхронизации без рендерграфа.
Когда рендер кадра состоит из заранее известных проходов, то управлять состояниями ресурсов можно и вручную.
Первым проходом должно быть обновление данных на ГП.
Синхронизация
Все копирования и обновление юниформ должно быть в этом проходе, чтоб избавиться от лишних синхронизаций в других проходах, зачем это нужно я объяснял тут (п.3).
Далее идут проходы рисования и вычислений.
Предполагается, что все неизменяемые ресурсы уже находятся в том состоянии, в котором они используются в дескрипторах.
Остаются синхронизации для изменяемых ресурсов (Attachment, StorageImage, StorageBuffer), их легко отслеживать и синхронизировать вручную, для проверки корректности есть способы.
Синхронизации между очередями.
Есть 2 подхода:
1. Сделать ресурсы общими для всех очередей (VK_SHARING_MODE_CONCURRENT), тогда достаточно сделать синхронизации семафорами, чтобы избежать одновременной записи или чтения и записи (data race), в движке это делается через
пример: Async compute + shared resources
2. Явно передавать ресурсы между очередями (queue ownership transfer). Внутри рендер таска это сложнее отслеживать, поэтому такие барьеры удобнее вынести в интерфейс
пример: Async compute + queue ownership transfer
#blog #engine
Когда рендер кадра состоит из заранее известных проходов, то управлять состояниями ресурсов можно и вручную.
Первым проходом должно быть обновление данных на ГП.
Синхронизация
Host_Write -> CopySrc|IndexBuffer|VertexBuffer|UniformBuffer
нужна при записи данных на стороне ЦП.Все копирования и обновление юниформ должно быть в этом проходе, чтоб избавиться от лишних синхронизаций в других проходах, зачем это нужно я объяснял тут (п.3).
Далее идут проходы рисования и вычислений.
Предполагается, что все неизменяемые ресурсы уже находятся в том состоянии, в котором они используются в дескрипторах.
Остаются синхронизации для изменяемых ресурсов (Attachment, StorageImage, StorageBuffer), их легко отслеживать и синхронизировать вручную, для проверки корректности есть способы.
Синхронизации между очередями.
Есть 2 подхода:
1. Сделать ресурсы общими для всех очередей (VK_SHARING_MODE_CONCURRENT), тогда достаточно сделать синхронизации семафорами, чтобы избежать одновременной записи или чтения и записи (data race), в движке это делается через
CommandBatch::AddInputDependency (CommandBatch &)
. Минус этого подхода - на AMD на общих ресурсах не включается компресия рендер таргетов, что снижает производительность.пример: Async compute + shared resources
2. Явно передавать ресурсы между очередями (queue ownership transfer). Внутри рендер таска это сложнее отслеживать, поэтому такие барьеры удобнее вынести в интерфейс
CommandBatch
, так появился метод CommandBatch::DeferredBarriers()
и initial, final
параметры при создании рендер таска. Теперь управление перемещением ресурсов происходит на этапе планирования батчей команд.пример: Async compute + queue ownership transfer
#blog #engine
Синхронизации с помощью рендерграфа.
Реализация более простая чем в FG, тут не поддерживается перестановка команд, но и лучше чем в DE, так как совместим с многопоточностью.
Контекст для записи команд.
Работает поверх существующих контекстов, о которых уже рассказывал. Добавленно только отслеживание состояний ресурсов в пределах
Этап планирования.
Точно также создаются батчи, но теперь через builder паттерн, где можно указать какие ресурсы будут использоваться в батче и их начальное/конечное состояние. Если ресурс используется только в одной ГП-очереди (VkQueue), то указывать его не обязательно. Но если ресурс используется в нескольких очередях, то требуется явно добавить его в батч, тогда внутри вставятся все необхрдимые синхронизации.
Для каждого рендер таска также можно указать начальное и конечное состояние ресурса, это позволит оптимизировать синхронизации между тасками.
Данный рендерграф нужен для прототипирования и в случаях, когда проходы рендера задаются в более высокоуровневом коде, например в скриптах. В таких случаях потери от использования рендерграфа не так важны как скорость разработки.
пример: async compute + RG
#blog #engine
Реализация более простая чем в FG, тут не поддерживается перестановка команд, но и лучше чем в DE, так как совместим с многопоточностью.
Контекст для записи команд.
Работает поверх существующих контекстов, о которых уже рассказывал. Добавленно только отслеживание состояний ресурсов в пределах
RenderTask
и автоматическое перемещение их в нужное состояние. Начальное и конечное состояние ресурса это либо дефолтное как в FG, либо задается вручную на этапе планирования рендер графа, иногда это добавляет ненужные синхронизации, но потери на них минимальные, если не приводят к декомпресии рендер таргетов.Этап планирования.
Точно также создаются батчи, но теперь через builder паттерн, где можно указать какие ресурсы будут использоваться в батче и их начальное/конечное состояние. Если ресурс используется только в одной ГП-очереди (VkQueue), то указывать его не обязательно. Но если ресурс используется в нескольких очередях, то требуется явно добавить его в батч, тогда внутри вставятся все необхрдимые синхронизации.
Для каждого рендер таска также можно указать начальное и конечное состояние ресурса, это позволит оптимизировать синхронизации между тасками.
Данный рендерграф нужен для прототипирования и в случаях, когда проходы рендера задаются в более высокоуровневом коде, например в скриптах. В таких случаях потери от использования рендерграфа не так важны как скорость разработки.
пример: async compute + RG
#blog #engine
Выравнивание данных в Vulkan/Metal.
В Vulkan все выравнивания надо получать в рантайме и под каждый девайс.
В Metal выравнивание указано в документации и привязано к GPUFamily, которое дополнительно разделяется на Common/Apple/Mac со своими требованиями к выравниванию.
В движке выравнивания вынесены в DeviceProperties для использования в рантайме и
Это позволяет на этапе компиляции ресурсов и кода рассчитать размер буфера или задать все смещения константами, что упрощает проверки и дает гарантии, что код будет одинаково работать на разных устройствах.
А еще это дает микрооптимизации на вычисление и доступ к памяти, для тех, кто считает такты, но в некоторых случаях расплачиваться приходится большим расходом памяти на ГП.
#blog #engine
В Vulkan все выравнивания надо получать в рантайме и под каждый девайс.
В Metal выравнивание указано в документации и привязано к GPUFamily, которое дополнительно разделяется на Common/Apple/Mac со своими требованиями к выравниванию.
В движке выравнивания вынесены в DeviceProperties для использования в рантайме и
DeviceLimits
для использования в компайлтайме. DeviceLimits
содержит максимальное выравнивание для большинства ГП. В Metal все выравнивания - константы, поэтому тут просто, а для Vulkan эти значения проверяются через vulkan.gpuinfo, а также при старте движка сравниваются с текущими выравниваниями.Это позволяет на этапе компиляции ресурсов и кода рассчитать размер буфера или задать все смещения константами, что упрощает проверки и дает гарантии, что код будет одинаково работать на разных устройствах.
А еще это дает микрооптимизации на вычисление и доступ к памяти, для тех, кто считает такты, но в некоторых случаях расплачиваться приходится большим расходом памяти на ГП.
#blog #engine
Часто вижу в примерах по Vulkan код вида:
Но с тех времен кое-что изменилось - добавили
Правильнее передавать больше флагов:
1. HOST_VISIBLE | HOST_COHERENT - с optFlags.
2. HOST_VISIBLE | HOST_CACHED - без optFlags.
#blog #vk
Это пошло с первых примеров, тогда на дискретных ГП были такие типы памяти:
GetMemoryTypeIndex( mem_req.memoryTypeBits, DEVICE_LOCAL );
DEVICE_LOCAL, HOST_COHERENT, HOST_CACHED
.Но с тех времен кое-что изменилось - добавили
DEVICE_LOCAL | HOST_COHERENT
память для прямого доступа со стороны ЦП, обычно эта память ограничена в 256 Мб, что достаточно для юниформ буферов. Но эта же память может использоваться для staging буферов и всех DEVICE_LOCAL буферов, если этот тип памяти окажется в списке раньше других. То есть сейчас все работает как раньше только потому, что новый тип памяти добавили в конец списка и перебор до него не доходит.Правильнее передавать больше флагов:
Пример использования:
uint GetMemoryTypeIndex (uint memoryTypeBits, VkMemoryPropertyFlags includeFlags, VkMemoryPropertyFlags optFlags, VkMemoryPropertyFlags excludeFlags)
Возвращает:
GetMemoryTypeIndex( mem_req.memoryTypeBits, include: HOST_VISIBLE, opt: HOST_COHERENT, exclude: DEVICE_LOCAL );
1. HOST_VISIBLE | HOST_COHERENT - с optFlags.
2. HOST_VISIBLE | HOST_CACHED - без optFlags.
#blog #vk
Проверка ошибок.
Метка
Все проверки можно было заменить одной
В итоге, даже в сложных случаях, таких как промисы и корутины, удалось сделать проверку ошибок и корректный выход из функции без использования исключений.
#blog #engine
CHECK_ERR( expr, [opt] result )
- разворачивается в if ( not (expr) ) return result
.CHECK_TE( expr )
- работает только внутри AsyncTask::Run()
, помечает таск как отмененный и выходит из функции. Такой же результат даст и исключение внутри таска.CHECK_PE( expr )
- используется внутри Promise<>
и возвращает специальное значение CancelPromise
, которое помечает промис/таск как отмененный и у зависящих от него промисов выполнится функция, переданная в Promise::Except( fn )
.CHECK_CE( expr )
- используется внутри корутин (coroutine) и также помечает корутину/таск как отмененный.CHECK_THROW( expr, [opt] exception )
- разворачивается в if ( not (expr) ) throw exception
. Чаще всего используется чтобы прокинуть исключение в скрипты.CATCH_ERR( code, [opt] result )
- возвращает 'result', если 'code' бросает исключение. Используется для оборачивания stl вызовов, которые могут бросить исключение.Метка
[opt]
означает опциональный параметр, по-умолчанию возвращается false
или пустой объект {}
.Все проверки можно было заменить одной
CHECK_THROW
, но исключения дают меньшую производительность, что не так важно, так как случается редко. Также хочется оставить возможность отказаться от исключений, поэтому используются специализированные макросы. Например wasm
по-умолчанию выключает исключения при -O1
и выше для наилучшей производительности.В итоге, даже в сложных случаях, таких как промисы и корутины, удалось сделать проверку ошибок и корректный выход из функции без использования исключений.
#blog #engine
По документации Vulkan в некоторых случаях содержимое ресурсов может быть потеряно:
* Барьер с
* Ресурс с
* Аттачменты рендер пасса с
* Несколько ресурсов используют одну память, но только один ресурс содержит валидные данные.
* Неиспользуемые аттачменты в сабпасе рендерпасса, если не используется как
Это нужно чтобы драйвер выбирал более быстрое поведение, например сжатый рендер таргет (delta color compression) при смене лейаута на
На тайловых архитектурах
Таким образом легко допустить ошибку и продолжить использовать содержимое текстуры, тогда как на другом драйвере или железе поведение изменится.
Для этих случаев у меня есть самописный слой валидации, который принудительно чистит содержимое ресурсов случайными значениями, чтобы поведение было одинаковым на всех системах.
#blog #vk #engine
* Барьер с
oldLayout = UNDEFINED
.* Ресурс с
exclusive sharing
используется в разных очередях без queue ownership transfer
(нужно всегда явно передавать ресурс из одной очереди в другую, что иногда неудобно).* Аттачменты рендер пасса с
loadOp = DONT_CARE
и storeOp = DONT_CARE
.* Несколько ресурсов используют одну память, но только один ресурс содержит валидные данные.
* Неиспользуемые аттачменты в сабпасе рендерпасса, если не используется как
preserve attachment
.Это нужно чтобы драйвер выбирал более быстрое поведение, например сжатый рендер таргет (delta color compression) при смене лейаута на
GENERAL
тратит дополнительное время на расжатие, а oldLayout = UNDEFINED
позволяет это пропустить, потеряв содержимое текстуры, но, например, при такой смене лейаутов: UNDEFINED (было SHADER_READ) -> SHADER_READ
, содержимое не изменится.На тайловых архитектурах
loadOp = DONT_CARE
и storeOp = DONT_CARE
позволяют не загружать данные из глобальной памяти в кэш тайла, но на других архитектурах данные никуда не загружаются, поэтому содержимое сохраняется.Таким образом легко допустить ошибку и продолжить использовать содержимое текстуры, тогда как на другом драйвере или железе поведение изменится.
Для этих случаев у меня есть самописный слой валидации, который принудительно чистит содержимое ресурсов случайными значениями, чтобы поведение было одинаковым на всех системах.
#blog #vk #engine
Как получить информацию о ЦП на Android.
Через
Константы можно посмотреть здесь: arm cputype, arm64 cputype, julia/processor_arm.cpp - расширенный список, включает ЦП для IoT.
То же самое можно получить через getauxval() с AT_HWCAP и AT_HWCAP2.
Через
По максимальной частоте можно сгруппировать ядра по типу производительности:
* высокопроизводительные (X1, X2)
* производительные (A78)
* энергоэффективные (A55)
Либо через параметр
#blog #arm #cpu_prof
Через
/proc/cpuinfo
можно узнать количество ядер и их описание.processor
- номер ядра.CPU implementer
- производитель ЦП (`ARM_CPU_IMP_*`).CPU part
- номер модели ЦП (`*_CPU_PART_*`).Константы можно посмотреть здесь: arm cputype, arm64 cputype, julia/processor_arm.cpp - расширенный список, включает ЦП для IoT.
Features
- список расширенных инструкций ЦП, расшифровка есть здесь: Arm CPU features tableТо же самое можно получить через getauxval() с AT_HWCAP и AT_HWCAP2.
BogoMIPS
- мера производительности, но часто выдает одинаковые значения для всех типов ядер.Через
/sys/devices/system/cpu/cpu[x]
доступна дополнительная информация:cpufreq/cpuinfo_min_freq
, cpufreq/cpuinfo_max_freq
- минимальная и максимальная частота ядра.cpufreq/scaling_cur_freq
- текущая частота ядра, используется как замена cpuinfo_cur_freq
, который недоступен.По максимальной частоте можно сгруппировать ядра по типу производительности:
* высокопроизводительные (X1, X2)
* производительные (A78)
* энергоэффективные (A55)
Либо через параметр
/sys/devices/system/cpu/cpu[x]/cpu_capacity
- чем он больше, тем производительнее ядро.#blog #arm #cpu_prof
Как получить информацию о ЦП на Windows.
IsProcessorFeaturePresent - для проверки поддерживаемых инструкций типа SSE, то же самое возвращает и
GetSystemCpuSetInformation - возвращает подробную информацию о ЦП, например
CallNtPowerInformation - с параметром
GetLogicalProcessorInformationEx - с параметром
NtQuerySystemInformation - с параметром
Счетчики обновляются не так часто, при 100Гц дельта может быть 0, лучше обновлять каждую секунду. Так можно рассчитать % использования ЦП:
С параметром
#blog #cpu_prof
IsProcessorFeaturePresent - для проверки поддерживаемых инструкций типа SSE, то же самое возвращает и
__cpuid()
.GetSystemCpuSetInformation - возвращает подробную информацию о ЦП, например
EfficiencyClass
позволит обнаружить энергоэффективные ядра на новых Intel.CallNtPowerInformation - с параметром
ProcessorInformation
позволяет получить частоту ядра ЦП, но параметр CurrentMhz
не меняется.GetLogicalProcessorInformationEx - с параметром
RelationCache
позволяет получить информацию о кэше, но часто информация неполная.NtQuerySystemInformation - с параметром
SystemProcessorPerformanceInformation
позволяет получить значения счетчиков для каждого ядра, в WinAPI структуры определены без полей, но есть информация на сторонних ресурсах SYSTEM_PROCESSOR_PERFORMANCE_INFORMATIONСчетчики обновляются не так часто, при 100Гц дельта может быть 0, лучше обновлять каждую секунду. Так можно рассчитать % использования ЦП:
LONGLONG idle_time = next.IdleTime - prev.IdleTime;
LONGLONG kernel_time = next.KernelTime - prev.KernelTime;
LONGLONG user_time = next.UserTime - prev.UserTime;
LONGLONG total_time = idle_time + kernel_time + user_time;
float user_usage = float(user_time) / float(total_time);
float kernel_usage = float(kernel_time) / float(total_time);
С параметром
SystemPerformanceInformation
возвращается структура SYSTEM_PERFORMANCE_INFORMATION, где есть дополнительные счетчики производительности.#blog #cpu_prof
Общее впечатление от GDC.
Все больше презентаций не содержат достаточно технических деталей для воспроизведения техник.
Если это многопоточка, то нет деталей реализации и оптимизации, как например в статье Unity. Возможно у всех также сделано не идеально и есть большие потери на управление тасками, поэтому и умалчивают, чтоб не облажаться.
Построение новой многопоточной архитектуры движка и игры это очень сложно, а удачных примеров все еще мало, а подробной архитектуры ААА игры вообще не найти, что замедляет развитие аналогов. Зато все переходят на UE, забрасывая свои движки, а архитектура UE тоже не идеальна.
Если это нейросети, то нет сравнения с написаным по старинке алгоритмом. Может потому что даже не способны написать подобный алгоритм?
С ИИ еще давно перестали появляться хорошие публикации, последнее что помню это от начала 2010х.
#blog
Все больше презентаций не содержат достаточно технических деталей для воспроизведения техник.
Если это многопоточка, то нет деталей реализации и оптимизации, как например в статье Unity. Возможно у всех также сделано не идеально и есть большие потери на управление тасками, поэтому и умалчивают, чтоб не облажаться.
Построение новой многопоточной архитектуры движка и игры это очень сложно, а удачных примеров все еще мало, а подробной архитектуры ААА игры вообще не найти, что замедляет развитие аналогов. Зато все переходят на UE, забрасывая свои движки, а архитектура UE тоже не идеальна.
Если это нейросети, то нет сравнения с написаным по старинке алгоритмом. Может потому что даже не способны написать подобный алгоритм?
С ИИ еще давно перестали появляться хорошие публикации, последнее что помню это от начала 2010х.
#blog
Инструменты профилирования для Android.
Есть стандартные инструменты: Google AGI, Adreno GPU profiler, Mali profiler, но поддержка устройств ограничена и у меня ни один профайлер не стал работать с Vulkan.
Остается прямой доступ к счетчикам, для этого есть несколько библиотек:
HWCPipe - счетчики для ARM CPU и Mali GPU. Не поддерживается на Qualcomm CPU (Kryo) и некоторых ARM CPU, даже если в названии Cortex Axx. Пример использования можно посмотреть в Vulkan samples.
hardware-perfcounter - счетчики для Mali и Adreno, доступно намного больше счетчиков, в том числе специфичные для Adreno 5xx и 6xx.
Прямой доступ к счетчикам позволяет написать алгоритм для самооптимизации, когда идет переключение между разными алгоритмами, подгонка итераций/констант и тд.
#blog #gpu_prof #cpu_prof
Есть стандартные инструменты: Google AGI, Adreno GPU profiler, Mali profiler, но поддержка устройств ограничена и у меня ни один профайлер не стал работать с Vulkan.
Остается прямой доступ к счетчикам, для этого есть несколько библиотек:
HWCPipe - счетчики для ARM CPU и Mali GPU. Не поддерживается на Qualcomm CPU (Kryo) и некоторых ARM CPU, даже если в названии Cortex Axx. Пример использования можно посмотреть в Vulkan samples.
hardware-perfcounter - счетчики для Mali и Adreno, доступно намного больше счетчиков, в том числе специфичные для Adreno 5xx и 6xx.
Прямой доступ к счетчикам позволяет написать алгоритм для самооптимизации, когда идет переключение между разными алгоритмами, подгонка итераций/констант и тд.
#blog #gpu_prof #cpu_prof
Пришло время и мне рассказать про нейросети.
Начну со своего опыта, в отличие от нейросетей, где подбирали коэффициенты матриц, я пытался написать кодогенератор, то есть программу, которая по тестовым данным будет создавать наиболее оптимизированный код. Начал я с полного перебора и он работал очень медленно, но попытка использовать генетический алгоритм провалилась - он работал еще медленее, градиентный спуск вообще нельзя было применить так как менялись инструкции, а не константы и соседние результаты сильно отличались. В итоге все оптимизации были на ускорение тупого перебора, устранение дублирования кода, валидация кода до тестов, ранний выход, если тесты сильно отличались и тд.
Какой из этого можно сделать вывод - только полный перебор гарантированно дает наилучший результат, но он перестает справляться на больших данных из-за экспоненциальной сложности.
Теперь про нынешние нейросети, я их не разрабатывал и подробно не изучал, но кое-что знаю. Как происходит обучение - нейросеть гоняют на тестовых данных, она постоянно пытается "заучить" правильные ответы и из-за этого ее размер растет, но проверяющий алгоритм ищет наиболее компактное решение, поэтому выигрывает вариант который меньше заучивает, а значит он "понимает" зависимость между входными данными и результатом, то есть выводит алгоритм.
Только проблема в том, что в огромных нейросетях невозможно полностью избавиться от заученных частей, следовательно в какой-то момент нейросеть вместо применения алгоритма вернет заученый ответ и это чаще всего будет ошибкой. Другая проблема это недостаточное количество данных из-за которого выведенный алгоритм будет неточным.Чтобы проверить такую нейросеть на корректность всех ответов, нужно перебрать все возможные входные данные и проверить ее ответ. Для распознавания ситуации на дороге, для поддерживания разговора (чат) это почти бесконечный набор входных данных, то есть ошибки будут и будут всегда, какой бы совершенной не была бы нейросеть.
Остается вариант комбинации нейросети и написанных человеком алгоритмов, которые будут проверять результат и при обнаружении ошибок добавлять новые тестовые данные. А чем больше тестовых данных тем дольше будет обучение и тем дороже оно обойдется.
#blog
Начну со своего опыта, в отличие от нейросетей, где подбирали коэффициенты матриц, я пытался написать кодогенератор, то есть программу, которая по тестовым данным будет создавать наиболее оптимизированный код. Начал я с полного перебора и он работал очень медленно, но попытка использовать генетический алгоритм провалилась - он работал еще медленее, градиентный спуск вообще нельзя было применить так как менялись инструкции, а не константы и соседние результаты сильно отличались. В итоге все оптимизации были на ускорение тупого перебора, устранение дублирования кода, валидация кода до тестов, ранний выход, если тесты сильно отличались и тд.
Какой из этого можно сделать вывод - только полный перебор гарантированно дает наилучший результат, но он перестает справляться на больших данных из-за экспоненциальной сложности.
Теперь про нынешние нейросети, я их не разрабатывал и подробно не изучал, но кое-что знаю. Как происходит обучение - нейросеть гоняют на тестовых данных, она постоянно пытается "заучить" правильные ответы и из-за этого ее размер растет, но проверяющий алгоритм ищет наиболее компактное решение, поэтому выигрывает вариант который меньше заучивает, а значит он "понимает" зависимость между входными данными и результатом, то есть выводит алгоритм.
Только проблема в том, что в огромных нейросетях невозможно полностью избавиться от заученных частей, следовательно в какой-то момент нейросеть вместо применения алгоритма вернет заученый ответ и это чаще всего будет ошибкой. Другая проблема это недостаточное количество данных из-за которого выведенный алгоритм будет неточным.Чтобы проверить такую нейросеть на корректность всех ответов, нужно перебрать все возможные входные данные и проверить ее ответ. Для распознавания ситуации на дороге, для поддерживания разговора (чат) это почти бесконечный набор входных данных, то есть ошибки будут и будут всегда, какой бы совершенной не была бы нейросеть.
Остается вариант комбинации нейросети и написанных человеком алгоритмов, которые будут проверять результат и при обнаружении ошибок добавлять новые тестовые данные. А чем больше тестовых данных тем дольше будет обучение и тем дороже оно обойдется.
#blog
Особенности present mode в Vulkan
VK_PRESENT_MODE_FIFO_KHR - то что раньше называли vsync. Захватывается swapchain image, рисуется, отправляется в presentation engine, затем по сигналу выводится на экран. Главный недостаток - блокировка на стороне ЦП при вызове vkAcquireNextImageKHR и vkQueuePresentKHR, из-за этого поток простаивает 8мс (при 100Гц экране), если нет свободного swapchain image. На разных устройствах блокируются в разный момент времени, на NV в vkQueuePresentKHR, на Intel и смартфонах - vkAcquireNextImageKHR. Использование VkFence ничего не меняет на NV. На некоторых смартфонах ситуация еще хуже - при вызове Acquire/Present из разных потоков происходит более долгая блокировка в 50-100мс. Преимущество - в таком режиме рисуется только то, что попадает на экран, а значит более энергоэффективно, потому что драйвер часто понижает частоты ГП при низкой нагрузке.
VK_PRESENT_MODE_MAILBOX_KHR - рендеринг идет без задержек, а на экран выводится последнее отрендеренная картинка, если частота рисования выше частоты экрана, то часть картинок теряется. Исправляет все блокировки ЦП режима FIFO, но более энергозатратный.
VK_PRESENT_MODE_IMMEDIATE_KHR - сразу же выводит картинку на экран, что может привести к наложению двух кадров (visible tearing). Этот режим не поддерживается на смартфонах. Может быть использован в бенчмарках, где важна максимальная производительность, а не картинка на экране.
#blog #vk
VK_PRESENT_MODE_FIFO_KHR - то что раньше называли vsync. Захватывается swapchain image, рисуется, отправляется в presentation engine, затем по сигналу выводится на экран. Главный недостаток - блокировка на стороне ЦП при вызове vkAcquireNextImageKHR и vkQueuePresentKHR, из-за этого поток простаивает 8мс (при 100Гц экране), если нет свободного swapchain image. На разных устройствах блокируются в разный момент времени, на NV в vkQueuePresentKHR, на Intel и смартфонах - vkAcquireNextImageKHR. Использование VkFence ничего не меняет на NV. На некоторых смартфонах ситуация еще хуже - при вызове Acquire/Present из разных потоков происходит более долгая блокировка в 50-100мс. Преимущество - в таком режиме рисуется только то, что попадает на экран, а значит более энергоэффективно, потому что драйвер часто понижает частоты ГП при низкой нагрузке.
VK_PRESENT_MODE_MAILBOX_KHR - рендеринг идет без задержек, а на экран выводится последнее отрендеренная картинка, если частота рисования выше частоты экрана, то часть картинок теряется. Исправляет все блокировки ЦП режима FIFO, но более энергозатратный.
VK_PRESENT_MODE_IMMEDIATE_KHR - сразу же выводит картинку на экран, что может привести к наложению двух кадров (visible tearing). Этот режим не поддерживается на смартфонах. Может быть использован в бенчмарках, где важна максимальная производительность, а не картинка на экране.
#blog #vk
Как расчитать TBN в шейдере
Tangent и Bitangent вектора должны быть направлены в ту же сторону что и текстурные координаты UV.
Это легко сделать в фрагментном шейдере на основе деривативов.
Деривативы
Зная как меняется
Работает отлично на плоских поверхностях и детализированных изогнутых.
Если не работает, то достаточно поменять направление
#blog #cg
Tangent и Bitangent вектора должны быть направлены в ту же сторону что и текстурные координаты UV.
Это легко сделать в фрагментном шейдере на основе деривативов.
Деривативы
dFdx/dFdy
вернут разницу переданного значения между соседними пикселями.Зная как меняется
worldPos
и uv
можно рассчитать нормаль как векторное произведение и касательные (TB) как 3D вектор для uv
.float3x3 ComputeTBNinFS (float2 uv, float3 worldPos)
{
float3 wp_dx = dFdx( worldPos );
float3 wp_dy = dFdy( worldPos );
float2 uv_dx = dFdx( uv );
float2 uv_dy = dFdy( uv );
float3 t = normalize( wp_dx * uv_dy.t - wp_dy * uv_dx.t );
float3 b = normalize( -wp_dx * uv_dy.s + wp_dy * uv_dx.s );
float3 n = normalize( cross( wp_dy, wp_dx ));
return float3x3( t, b, n );
}
Работает отлично на плоских поверхностях и детализированных изогнутых.
Если не работает, то достаточно поменять направление
uv
.#blog #cg