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

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

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

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


person Nav    schedule 01.02.2011    source источник
comment
Спасибо за профилирование вашего приложения.   -  person GManNickG    schedule 03.02.2011
comment
@Everyone: Хорошо, поэтому Heap Contention - это правильная фраза, которую нужно искать в этом отношении. Очевидно, что glibc v2 и выше обрабатывает malloc параллельно citi.umich.edu/ Projects/linux-scalability/reports/malloc.html, но конфликт с free() будет (вероятно) обрабатываться только начиная с версии 2.2.4 bozemanpass.com/info/linux/malloc/Linux_Heap_Contention.html. Интересно, означает ли это, что такие библиотеки, как Hoard, станут излишними?   -  person Nav    schedule 04.02.2011


Ответы (10)


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

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

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

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

person Mikael Persson    schedule 01.02.2011
comment
Вопрос. Под пулом кучи вы имели в виду следующее: en.wikipedia.org/wiki/Memory_pool ? (Мне интересно, если вы говорите о пуле памяти, тогда я мог бы использовать масштабируемые распределители TBB. Но пользовательские распределители подверглись критике со стороны таких людей, как Скотт Мейерс en.wikipedia.org/wiki/Allocator_%28C%2B%2B%29#Custom_allocators) - person Nav; 03.02.2011
comment
Под пулом кучи я просто имел в виду наличие списка куч, которые вы используете (либо собственные кучи ОС, либо самодельные, либо из такой библиотеки, как boost), и вы выделяете из них те, которые когда-либо не заняты в определенное время (т.е. очередь с приоритетом на основе занятости, доступной памяти и фрагментации). И, конечно же, не рекомендуется использовать собственные аллокаторы, если вы не сделаете это тщательно и очень хорошо. В общем, я бы посоветовал вам использовать некоторые готовые материалы, предложенные другими здесь (HOARD или TBB на первый взгляд кажутся довольно надежными). - person Mikael Persson; 04.02.2011
comment
Микаэль, ваше утверждение неверно. Современные реализации кучи используют такие методы, как кэширование потоков, для ускорения параллельного распределения. Это означает, что вы можете выполнять значительно больше аллокаций с несколькими параллельными потоками, чем с одним потоком. - person Paul Groke; 28.06.2012

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

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

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

Я знаю о:

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

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

person Suma    schedule 01.02.2011
comment
Эй, спасибо! Просто чтобы добавить к списку, Intel Threading Building Blocks также имеет scalable_malloc, scalable_free, scalable_realloc, scalable_calloc, scalable_allocator и cache_aligned_allocator. - person Nav; 01.02.2011
comment
Сума, это тоже неправильно. Все современные версии MSVC по умолчанию используют функции кучи ОС (если не сказано не делать этого). И функции кучи ОС будут работать довольно хорошо, если включена куча с низким уровнем фрагментации, которая по умолчанию включена, начиная с Windows Vista (в Windows XP она может быть включена приложением с помощью простого вызова HeapSetInformation()). А с включенным LFH производительность кучи Windows сравнима с самыми быстрыми из доступных других аллокаторов — я лично провел тест с NedMalloc, и разница была незначительной. - person Paul Groke; 28.06.2012
comment
@PaulGroke Вы правы, я пытался обновить ответ. - person Suma; 28.06.2012

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

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

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

person Matthieu M.    schedule 01.02.2011

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

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

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

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

Удачи!

person murrekatt    schedule 01.02.2011
comment
Я рассматривал предварительное выделение, но программа требует динамического создания экземпляров классов (с использованием виртуальных), поэтому я не могу предварительно создавать экземпляры этих классов. Также нельзя повторно использовать объекты. Я предполагаю, что использование масштабируемого распределителя памяти - единственный вариант сейчас. Спасибо :) - person Nav; 01.02.2011

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

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

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

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

person Tom    schedule 01.02.2011
comment
Хорошо, может быть, это типичная проблема, но она не отвечает на вопрос. - person Olhovsky; 01.02.2011

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

Распределение памяти происходит медленно — если вы делаете это несколько раз, особенно на большом количестве потоков, вам нужен редизайн. Можете ли вы заранее выделить достаточно места в начале, можете ли вы просто выделить большой кусок с «новым», а затем разбить его самостоятельно?

person Martin Beckett    schedule 01.02.2011
comment
Неа. Я использую виртуальные функции и копирую множество объектов, внутри которых есть матрицы повышения. Таким образом, выделение памяти должно выполняться динамически. Я думаю, что «редизайн» - единственный вариант. - person Nav; 01.02.2011
comment
Выделение памяти происходит медленно, это сильно зависит от платформы. Я привык к этому, используя стандартную Visual Studio CRT, но недавно я начал использовать масштабируемые распределители, и, к моему удивлению, их производительность превосходна — большинство из них значительно снижают затраты на выделение памяти даже для однопоточного использования и имеют отличную масштабируемость с несколькими потоками. ядра. Смотрите мой ответ ниже. - person Suma; 01.02.2011
comment
@Suma: медленно по сравнению со стеком или предварительным распределением. - person murrekatt; 01.02.2011
comment
@Suma - и медленно по сравнению с тем, чтобы этого не делать ;-) - person Martin Beckett; 01.02.2011
comment
Я просто хотел указать, что некоторые из современных масштабируемых распределителей часто близки к тому, чтобы выделить большой кусок с «новым», а затем разбить его самостоятельно? если только они не наткнутся на какой-то патологический для них паттерн, и использование их сейвов дает вам почти такую ​​же производительность с элегантностью поддержки родного и естественного языка. - person Suma; 01.02.2011

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

person Arunmu    schedule 01.02.2011
comment
Что ж, в этой ветке говорится, что new «обычно» потокобезопасен в gcc: stackoverflow.com/questions/796099/ - person Nav; 01.02.2011
comment
@Nav: я считаю, что новый оператор является повторно вводимым, но его потокобезопасность зависит от реализации. Я был бы рад увидеть любую стандартную документацию по тому же самому, если бы вы могли опубликовать ее. - person Arunmu; 01.02.2011

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

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

person Puppy    schedule 01.02.2011
comment
Что такое «куча, разделенная потоками»? Распределение кучи — это динамическое выделение, верно? Какие другие формы динамического размещения доступны? en.wikipedia.org/wiki/Dynamic_memory_allocation - person Nav; 01.02.2011
comment
@Nav: Некоторые ОС могут создавать несколько куч. Вы можете выделить по одному для каждого потока. Существуют разные формы динамического размещения, например пулы объектов. Если у вас есть известный шаблон распределения объектов, вы, вероятно, можете написать собственный распределитель, который будет намного эффективнее. Существующие подпрограммы выделения кучи спроектированы таким образом, чтобы их производительность была максимально гибкой. - person Puppy; 01.02.2011

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

person Eugen Constantin Dinca    schedule 01.02.2011

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

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

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

person Gregory    schedule 03.02.2011
comment
Теперь упоминание о мьютексе было очень-очень полезным! Я хотел знать, произошло ли это серийно. Восемь выделений немного разочаровывают. Не думаете ли вы, что это может произойти быстрее с пулом кучи, о котором упоминали другие? - person Nav; 03.02.2011
comment
@Nav: Ну: нет никакой магии - у вас есть 8 ядер, так что это параллелизм, которого вы можете достичь. - person Gregory; 03.02.2011
comment
извините, отправил комментарий рано. Я думаю, пул кучи — это то, что ptmalloc делает внутри. Не думайте, что у вас есть какие-то причины для самостоятельной реализации пула кучи. PS: добавил ворс в статью к моему ответу - person Gregory; 03.02.2011
comment
С другой стороны, если вы уменьшите количество реального выделения кучи, может помочь выделение по блокам. В любом случае это может помочь, так как malloc довольно дорогая операция. - person Gregory; 03.02.2011