Подтвердить что ты не робот

Как запросить выделение выделенной памяти на С++?

Общая ситуация

Приложение, которое чрезвычайно интенсивно использует как пропускную способность, так и использование ЦП, и использование графического процессора, должно передавать около 10-15 ГБ в секунду с одного GPU на другой. Он использует API DX11 для доступа к графическому процессору, поэтому загрузка на GPU может происходить только с буферами, которым требуется сопоставление для каждой отдельной загрузки. Загрузка происходит в кусках по 25 Мбайт за раз, а 16 потоков одновременно записывают буферы в сопоставленные буферы. Там не так много, что можно сделать по любому из этого. Фактический уровень записи concurrency должен быть ниже, если бы не следующая ошибка.

Это мощная рабочая станция с 3 GPU Pascal, high-end процессором Haswell и четырехканальной ОЗУ. На аппаратном уровне не так много улучшений. Он запускает настольную версию Windows 10.

Актуальная проблема

Как только я пропускаю загрузку процессора на уровне 50%, что-то в MmPageFault() (внутри ядра Windows, вызываемого при обращении к памяти, которая была отображена в ваше адресное пространство, но еще не была зафиксирована ОС) ломается ужасно, а оставшаяся 50% загрузка процессора расходуется на спин-замок внутри MmPageFault(). Процессор становится 100% использованным, и производительность приложения полностью ухудшается.

Я должен предположить, что это связано с огромным объемом памяти, который требуется распределять процессу каждую секунду и который также полностью не отображается из процесса каждый раз, когда буфер DX11 не отображается. Соответственно, на самом деле это тысячи вызовов MmPageFault() в секунду, повторяющихся последовательно, когда memcpy() записывается последовательно в буфер. Для каждой отдельной незарегистрированной страницы.

Одна загрузка процессора выходит за пределы 50%, оптимистичная прямая блокировка в ядре Windows, защищающая управление страницей, полностью ухудшает производительность.

Вопросы

Буфер распределяется драйвером DX11. Ничто не может быть изменено в отношении стратегии распределения. Использование другого API памяти и особенно повторного использования невозможно.

Звонки в API DX11 (отображение/развязка буферов) происходит из одного потока. Фактические операции копирования потенциально происходят многопоточными по большему количеству потоков, чем в системе есть виртуальные процессоры.

Уменьшение требований к пропускной способности памяти невозможно. Это приложение в режиме реального времени. Фактически, в настоящее время жестким ограничением является PCIe 3.0 16x пропускная способность основного графического процессора. Если бы я мог, мне уже нужно было продвигаться дальше.

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

Ухудшение производительности спин-блокировки, по-видимому, настолько редки (потому что прецедент так далеко продвигает его), что в Google вы не найдете ни одного результата для имени функции блокировки спина.

Выполняется обновление до API, который дает больший контроль над сопоставлениями (Vulkan), но не подходит для краткосрочного исправления. Переключение на лучшее ядро ​​ОС в настоящее время не является вариантом по той же причине.

Снижение нагрузки на процессор также не работает; слишком много работы, которая должна быть выполнена иначе, чем (обычно тривиальная и недорогая) буферная копия.

Вопрос

Что можно сделать?

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

Как я могу гарантировать, что память будет совершена с наименьшим количеством транзакций?

Экзотические флаги для DX11, которые предотвратили бы отмену выделения буферов после разворачивания, API Windows для принудительной фиксации в одной транзакции, почти все приветствуется.

Текущее состояние

// In the processing threads
{
    DX11DeferredContext->Map(..., &buffer)
    std::memcpy(buffer, source, size);
    DX11DeferredContext->Unmap(...);
}
4b9b3361

Ответ 1

Текущее обходное решение, упрощенный псевдо-код:

// During startup
{
    SetProcessWorkingSetSize(GetCurrentProcess(), 2*1024*1024*1024, -1);
}
// In the DX11 render loop thread
{
    DX11context->Map(..., &resource)
    VirtualLock(resource.pData, resource.size);
    notify();
    wait();
    DX11context->Unmap(...);
}
// In the processing threads
{
    wait();
    std::memcpy(buffer, source, size);
    signal();
}

VirtualLock() заставляет ядро ​​немедленно возвращать указанный диапазон адресов с ОЗУ. Вызов дополнительной функции VirtualUnlock() является необязательным, он происходит неявно (и без каких-либо дополнительных затрат), когда диапазон адресов не отображается из процесса, (Если вызывается явно, это стоит около 1/3 от стоимости блокировки.)

Для того чтобы VirtualLock() работал вообще, SetProcessWorkingSetSize() нужно сначала вызвать, так как сумма всех областей памяти заблокирована на VirtualLock() не может превышать минимальный размер рабочего набора, настроенный для процесса. Установка минимального рабочего размера набора на нечто большее, чем базовый объем памяти вашего процесса, не имеет побочных эффектов, если ваша система фактически не может быть заменена, ваш процесс по-прежнему не будет потреблять больше ОЗУ, чем фактический размер рабочего набора.


Просто использование VirtualLock(), хотя и в отдельных потоках, и использование отложенных контекстов DX11 для вызовов Map/Unmap, мгновенно снизило штраф за выполнение с 40-50% до чуть более приемлемых 15%.

Отказ от использования отложенного контекста и исключительно, вызывающий как все мягкие ошибки, , так и соответствующее деление при снятии привязки в одном потоке, дал необходимо повысить производительность. Суммарная стоимость этой блокировки спина теперь составляет менее 1% от общего использования ЦП.


Резюме

Когда вы ожидаете мягких сбоев в Windows, попробуйте сделать все возможное, чтобы сохранить их в одном потоке. Выполнение параллельного memcpy само является проблематичным, в некоторых ситуациях даже необходимо полностью использовать пропускную способность памяти. Однако это только в том случае, если память уже включена в оперативную память. VirtualLock() - самый эффективный способ гарантировать это.

(Если вы не работаете с API, например DirectX, который отображает память в ваш процесс, вы вряд ли часто будете сталкиваться с незафиксированной памятью. Если вы просто работаете со стандартными С++ new или malloc, ваша память объединяется и перерабатывается во всяком случае, внутри вашего процесса, поэтому мягкие ошибки встречаются редко.)

Просто убедитесь, что во время работы с Windows избегайте любых сбоев при сбое страниц.