Мне нужно будет создать многопоточный проект, который вскоре я увижу в экспериментах (delphitools.info/2011/10/13/memory-manager-investigations), в которых диспетчер памяти Delphi по умолчанию имеет проблемы с многопоточным потоком.
Итак, я нашел этот SynScaleMM. Кто-нибудь может дать некоторую обратную связь об этом или о подобном менеджере памяти?
Ответ 2
Если ваше приложение может разместить лицензионный код GPL, я бы рекомендовал Hoard. Вам придется написать свою собственную обертку, но это очень просто. В моих тестах я не нашел ничего, что соответствовало бы этому коду. Если ваш код не может разместить GPL, тогда вы можете получить коммерческую лицензию на Hoard за значительную плату.
Даже если вы не можете использовать Hoard во внешнем выпуске своего кода, вы можете сравнить его производительность с производительностью FastMM, чтобы определить, есть ли у вашего приложения проблемы с масштабированием распределения кучи.
Я также обнаружил, что распределители памяти в версиях msvcrt.dll, распространяемые в Windows Vista и более поздних версиях, довольно хорошо под конфликтом потоков, безусловно, намного лучше, чем FastMM. Я использую эти процедуры через следующий Delphi MM.
unit msvcrtMM;
interface
implementation
type
size_t = Cardinal;
const
msvcrtDLL = 'msvcrt.dll';
function malloc(Size: size_t): Pointer; cdecl; external msvcrtDLL;
function realloc(P: Pointer; Size: size_t): Pointer; cdecl; external msvcrtDLL;
procedure free(P: Pointer); cdecl; external msvcrtDLL;
function GetMem(Size: Integer): Pointer;
begin
Result := malloc(size);
end;
function FreeMem(P: Pointer): Integer;
begin
free(P);
Result := 0;
end;
function ReallocMem(P: Pointer; Size: Integer): Pointer;
begin
Result := realloc(P, Size);
end;
function AllocMem(Size: Cardinal): Pointer;
begin
Result := GetMem(Size);
if Assigned(Result) then begin
FillChar(Result^, Size, 0);
end;
end;
function RegisterUnregisterExpectedMemoryLeak(P: Pointer): Boolean;
begin
Result := False;
end;
const
MemoryManager: TMemoryManagerEx = (
GetMem: GetMem;
FreeMem: FreeMem;
ReallocMem: ReallocMem;
AllocMem: AllocMem;
RegisterExpectedMemoryLeak: RegisterUnregisterExpectedMemoryLeak;
UnregisterExpectedMemoryLeak: RegisterUnregisterExpectedMemoryLeak
);
initialization
SetMemoryManager(MemoryManager);
end.
Стоит отметить, что ваше приложение должно сильно забивать распределитель кучи, прежде чем конфликты в FastMM станут препятствием для производительности. Как правило, по моему опыту это происходит, когда ваше приложение выполняет большую обработку строк.
Мой главный совет для любого, кто страдает от обсуждения разногласий по куче, состоит в том, чтобы переделать код, чтобы избежать попадания кучи. Вы не только избегаете соперничества, но также избегаете затрат на распределение кучи - классический двухклассник!
Ответ 4
Это блокировка, которая делает разницу!
Есть два вопроса, о которых нужно знать:
- Использование префикса
LOCK
самой Delphi (System.dcu);
- Как FastMM4 обрабатывает конфликты потоков и то, что он делает после того, как не удалось получить блокировку.
Использование префикса LOCK
самой Delphi
Borland Delphi 5, выпущенный в 1999 году, был тем, который представил префикс LOCK
в строковых операциях. Как вы знаете, когда вы назначаете одну строку другой, она не копирует всю строку, а просто увеличивает счетчик ссылок внутри строки. Если вы изменяете строку, она отменяет ссылки, уменьшает счетчик ссылок и выделяет отдельное пространство для измененной строки.
В Delphi 4 и ранее, операции для увеличения и уменьшения счетчик ссылок были обычные операции памяти. Программисты, которые использовали Delphi, знали о них и, если они использовали строки по потокам, то есть передавали строку из одного потока в другой, использовали свой собственный механизм блокировки только для соответствующих строк. Программисты также использовали текстовую копию только для чтения, которая никоим образом не изменяла исходную строку и не требовала блокировки, например:
function AssignStringThreadSafe(const Src: string): string;
var
L: Integer;
begin
L := Length(Src);
if L <= 0 then Result := '' else
begin
SetString(Result, nil, L);
Move(PChar(Src)^, PChar(Result)^, L*SizeOf(Src[1]));
end;
end;
Но в Delphi 5 Borland добавили префикс LOCK
к строковым операциям, и они стали очень медленными, по сравнению с Delphi 4, даже для однопоточных приложений.
Чтобы преодолеть эту медлительность, программисты стали использовать "однопоточные" файлы исправлений SYSTEM.PAS с комментариями блокировки.
Подробнее см. https://synopse.info/forum/viewtopic.php?id=57&p=1.
Обсуждение темы FastMM4
Вы можете изменить исходный код FastMM4 для лучшего механизма блокировки или использовать любую существующую вилку FastMM4, например https://github.com/maximmasiutin/FastMM4
FastMM4 не самый быстрый для многоядерной операции, особенно когда число потоков больше числа физических сокетов, потому что оно по умолчанию конфликтует с потоком (т.е. когда один поток не может получить доступ к данным, заблокирован другой поток) вызывает функцию Windows API Sleep (0), а затем, если блокировка по-прежнему недоступна, вводится в цикл, вызывая Sleep (1) после каждой проверки блокировки.
Каждый вызов Sleep (0) испытывает дорогостоящую стоимость контекстного переключателя, который может быть 10000+ циклов; он также страдает от стоимости кольца 3 до 0 переходов, которые могут быть 1000+ циклов. Что касается Sleep (1) - помимо затрат, связанных с Sleep (0), это также задерживает выполнение не менее 1 миллисекунды, управление привязкой к другим потокам и, если нет потоков, ожидающих выполнения физическим ядром ЦП, помещает ядро в сон, эффективно уменьшая потребление ЦП и потребление энергии.
Вот почему на многопоточном wotk с FastMM использование ЦП никогда не достигало 100% - из-за Sleep (1), выпущенного FastMM4. Такой способ приобретения замков не является оптимальным. Лучшим способом было бы спрятать блокировку порядка 5000 pause
инструкций, и если бы блокировка все еще была занята, вызов вызова SwitchToThread() API. Если pause
недоступен (на очень старых процессорах без поддержки SSE2) или вызове API SwitchToThread() не было доступно (в очень старых версиях Windows, до Windows 2000), лучшим решением было бы использовать EnterCriticalSection/LeaveCriticalSection, у которых нет задержки, связанной с Sleep (1), и которая также очень эффективно уступает управление ядром процессора другим потокам.
Оболочка, о которой я упоминал, использует новый подход к ожиданию блокировки, рекомендованный Intel в Руководство по оптимизациидля разработчиков - спинлооп pause
+ SwitchToThread(), и, если какой-либо из них недоступен: CriticalSections вместо Sleep(). С этими параметрами Sleep() никогда не будет использоваться, но вместо этого будет использоваться EnterCriticalSection/LeaveCriticalSection. Тестирование показало, что подход использования CriticalSections вместо Sleep (который использовался по умолчанию ранее в FastMM4) обеспечивает значительный выигрыш в ситуациях, когда количество потоков, работающих с менеджером памяти, совпадает или больше, чем количество физических ядер. Коэффициент усиления еще более заметен на компьютерах с несколькими физическими процессорами и неравномерным доступом к памяти (NUMA). Я использовал параметры компиляции, чтобы отменить оригинальный метод FastMM4 для использования Sleep (InitialSleepTime), а затем Sleep (AdditionalSleepTime) (или Sleep (0) и Sleep (1)) и заменить их EnterCriticalSection/LeaveCriticalSection, чтобы сэкономить ценные циклы CPU (0) и улучшить скорость (уменьшить латентность), которая была затронута каждый раз не менее чем на 1 миллисекунду спящим (1), потому что критические секции намного более удобны для процессора и имеют определенно более низкую задержку, чем Sleep (1).
Когда эти параметры включены, FastMM4-AVX проверяет: (1) поддерживает ли процессор SSE2 и, следовательно, инструкцию "пауза" и (2) имеет ли операционная система вызов API SwitchToThread(), и, если оба условия выполнены, использует "паузу" спин-петлю для 5000 итераций, а затем SwitchToThread() вместо критических секций; Если у процессора нет "паузы", или Windows не имеет функции API SwitchToThread(), она будет использовать EnterCriticalSection/LeaveCriticalSection.
Вы можете увидеть результаты тестирования, в том числе сделанные на компьютере с несколькими физическими процессорами (сокетами) в этой вилке.
См. также Длинные очереди Spin-wait Loops на технологии Hyper-Threading Включенные процессоры Intel. Вот что пишет об этой проблеме Intel - и это очень хорошо относится к FastMM4:
Длительный цикл цикла ожидания ожидания в этой модели потоков редко приводит к проблеме производительности в обычных многопроцессорных системах. Но это может привести к серьезному штрафу в системе с технологией Hyper-Threading, потому что ресурсы процессора могут потребляться главным потоком, пока они ожидают рабочих потоков. Сон (0) в цикле может приостановить выполнение основного потока, но только тогда, когда все доступные процессоры были заняты рабочими потоками в течение всего периода ожидания. Это условие требует, чтобы все рабочие потоки выполняли свою работу одновременно. Другими словами, рабочие нагрузки, назначенные рабочим потокам, должны быть сбалансированы. Если один из рабочих потоков завершает свою работу раньше других и освобождает процессор, главный поток все равно может работать на одном процессоре.
В обычной многопроцессорной системе это не вызывает проблем с производительностью, потому что ни один другой поток не использует процессор. Но в системе с технологией Hyper-Threading процессор, над которым работает главный поток, является логическим, который разделяет ресурсы процессора с одним из других рабочих потоков.
Характер многих приложений затрудняет обеспечение сбалансированности рабочих нагрузок, назначенных рабочим потокам. Например, многопоточное 3D-приложение может назначать задачи для преобразования блока вершин из мировых координат в просмотр координат в команде рабочих потоков. Объем работы для рабочего потока определяется не только количеством вершин, но и обрезанным статусом вершины, что не предсказуемо, когда главный поток делит рабочую нагрузку на рабочие потоки.
Незначительный аргумент в функции "Сон" заставляет ожидающий поток спать N миллисекунд, независимо от доступности процессора. Он может эффективно блокировать поток ожидания от потребления ресурсов процессора, если период ожидания установлен правильно. Но если период ожидания непредсказуем из рабочей нагрузки в рабочую нагрузку, тогда большое значение N может слишком долго сдерживать ожидающий поток, а меньшее значение N может вызвать его слишком быстрое пробуждение.
Поэтому предпочтительное решение, чтобы избежать потери ресурсов процессора в длительном цикле ожидания ожидания, заключается в замене цикла на API-интерфейс блокировки потоков операционной системы, такой как API-интерфейс Threading Microsoft Windows * WaitForMultipleObjects. Этот вызов заставляет операционную систему блокировать поток ожидания от потребления ресурсов процессора.
Он ссылается на Использование Spin-Loops на процессоре Intel Pentium 4 и процессоре Intel Xeon.
Вы также можете найти очень хорошую реализацию спинового цикла fooobar.com/questions/125873/....
Он также загружает нормальные нагрузки только для проверки перед выпуском хранилища LOCK
-ed, чтобы не заливать CPU заблокированными операциями в цикле, которые блокировали бы шину.
FastMM4 сам по себе очень хорош. Просто улучшите блокировку, и вы получите превосходный многопоточный менеджер памяти.
Также помните, что каждый маленький тип блока заблокирован отдельно в FastMM4.
Вы можете поместить отступы между областями управления небольшим блоком, чтобы каждая область имела собственную линию кеша, не разделялась с другими размерами блоков, и чтобы убедиться, что она начинается с границы размера строки кэша. Вы можете использовать CPUID для определения размера строки кэша CPU.
Итак, с правильно выполненной блокировкой в соответствии с вашими потребностями (т.е. нужно ли вам NUMA или нет, использовать ли LOCK
-издания и т.д., вы можете получить результаты, что процедуры распределения памяти будут в несколько раз быстрее и не будет так сильно страдать от обсуждения разногласий.