Может ли многопоточность ускорить выделение памяти?

Я работаю с 8-ядерным процессором и использую потоки Boost для запуска большой программы. По логике, программу можно разбить на группы, где каждая группа выполняется потоком. Внутри каждой группы некоторые классы вызывают оператор "new" в общей сложности 10000 раз. Rational Quantify показывает, что "новое" выделение памяти занимает максимальное время обработки при запуске программы и замедляет всю программу.

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

Мне неясно, как будет распределяться память здесь. Сможет ли планировщик ОС выделять память параллельно?

10 ответов

Решение

Динамическое распределение памяти использует кучу приложения / модуля / процесса (но не потока). Куча может обрабатывать только один запрос на выделение за раз. Если вы попытаетесь выделить память в "параллельных" потоках, они будут обработаны кучей должным образом. Вы не получите такое поведение, как: один поток ожидает, чтобы получить свою память, в то время как другой может запросить некоторые, в то время как третий получает некоторые. Потоки должны будут выстроиться в очередь, чтобы получить кусок памяти.

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

Я знаю, что Win32 API имеет такие функции, как GetProcessHeap(), CreateHeap(), HeapAlloc() и HeapFree(), которые позволяют создавать новую кучу и выделять / освобождать память из определенной кучи HANDLE. Я не знаю эквивалентности в других операционных системах (я искал их, но безрезультатно).

Конечно, вы должны стараться избегать частых динамических распределений. Но если вы не можете, вы могли бы рассмотреть (для переносимости) создать свой собственный класс "кучи" (не обязательно должен быть кучей как таковой, просто очень эффективным распределителем), который может управлять большим объемом памяти и, конечно же, класс интеллектуальных указателей, который будет содержать ссылку на кучу, из которой он получен. Это позволит вам использовать несколько куч (убедитесь, что они потокобезопасны).

Стандартный ЭЛТ

В то время как в более старых версиях Visual Studio распределитель CRT по умолчанию блокировался, это уже не так, по крайней мере для Visual Studio 2010 и новее, который напрямую вызывает соответствующие функции ОС. Диспетчер кучи в Windows блокировался до тех пор, пока Widows XP не был заблокирован, а в XP дополнительная блокировка с низкой фрагментацией не блокируется, в то время как по умолчанию используется, а более новые ОС (Vista/Win7) по умолчанию используют LFH. Производительность последних (Windows 7) распределителей очень хорошая, сопоставимая с масштабируемыми заменами, перечисленными ниже (вы все равно можете предпочесть их, если они предназначены для более старых платформ или когда вам нужны некоторые другие функции, которые они предоставляют). Существует несколько множественных "масштабируемых распределителей" с разными лицензиями и разными недостатками. Я думаю, что в Linux библиотека времени выполнения по умолчанию уже использует масштабируемый распределитель (некоторый вариант PTMalloc).

Масштабируемые замены

Я знаю о:

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

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

Есть 2 масштабируемых замены для malloc, о которых я знаю:

У меня нет опыта работы с Hoard (который плохо показал себя в исследовании), но Эмери Бергер скрывается на этом сайте и был поражен результатами. Он сказал, что взглянет, и я предполагаю, что в тесте или реализации могли быть какие-то особенности, которые "заманили в ловушку" Хоарда, поскольку общие отзывы обычно хороши.

Одно слово предостережения с jemalloc, он может тратить немного места, когда вы быстро создаете, а затем отбрасываете потоки (так как он создает новый пул для каждого потока, из которого вы выделяете). Если ваши потоки стабильны, с этим не должно быть проблем.

Я считаю, что краткий ответ на ваш вопрос: да, возможно. И как уже указывалось несколькими людьми, здесь есть способы добиться этого.

Помимо вашего вопроса и ответов, уже размещенных здесь, было бы неплохо начать с ваших ожиданий по улучшению, потому что это в значительной степени скажет, какой путь выбрать. Может быть, вам нужно быть в 100 раз быстрее. Кроме того, вы видите, что в ближайшем будущем вы также улучшаете скорость или есть уровень, который будет достаточно хорошим? Не зная вашего приложения или проблемной области, сложно также дать вам совет. Вы, например, находитесь в проблемной области, где скорость должна постоянно улучшаться?

Хорошая вещь, с которой стоит начать при повышении производительности, - это вопрос, нужно ли вам делать то, что вы делаете сейчас? В этом случае, вы можете предварительно выделить объекты? Есть ли в системе максимальное количество объектов X? Не могли бы вы повторно использовать объекты? Все это лучше, потому что вам не обязательно выполнять распределение на критическом пути. Например, если вы можете повторно использовать объекты, пользовательский распределитель с предварительно выделенными объектами будет работать хорошо. Кроме того, на какой ОС вы работаете?

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

Удачи!

Сверните свой собственный не многопоточный новый распределитель памяти, отдельную копию которого имеет каждый поток.

(вы можете переопределить новый и удалить)

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

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

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

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

Возможно, вы захотите взглянуть на The Hoard Memory Allocator: "является заменой для malloc(), которая может значительно повысить производительность приложений, особенно для многопоточных программ, работающих на многопроцессорных системах".

На некоторых платформах, таких как Windows, доступ к глобальной куче сериализуется ОС. Наличие отдельной кучи потока может существенно сократить время выделения.

Конечно, в этом случае, возможно, стоит задать вопрос, действительно ли вам нужно выделение кучи в отличие от какой-либо другой формы динамического распределения.

Вам нужно проверить документацию вашего компилятора, делает ли он поток распределителя безопасным или нет. Если этого не произойдет, вам потребуется перегрузить нового оператора и сделать его безопасным для потоков. Иначе это приведет либо к segfault, либо к UB.

  1. Лучшее, что вы можете попытаться достичь ~8 параллельного выделения памяти (так как у вас есть 8 физических ядер), а не 10000, как вы написали

  2. стандартный malloc использует mutex, а стандартный распределитель STL делает то же самое. Поэтому он не будет ускоряться автоматически, когда вы вводите потоки. Тем не менее, вы можете использовать другую библиотеку malloc (например, Google "ptmalloc"), которая не использует глобальную блокировку. если вы выделяете с помощью STL (например, выделяете строки, векторы), вы должны написать свой собственный распределитель.

Довольно интересная статья: http://developers.sun.com/solaris/articles/multiproc/multiproc.html

Другие вопросы по тегам