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

Нужен многопоточный менеджер памяти

Мне нужно будет создать многопоточный проект, который вскоре я увижу в экспериментах (delphitools.info/2011/10/13/memory-manager-investigations), в которых диспетчер памяти Delphi по умолчанию имеет проблемы с многопоточным потоком.

enter image description here

Итак, я нашел этот SynScaleMM. Кто-нибудь может дать некоторую обратную связь об этом или о подобном менеджере памяти?

Спасибо

4b9b3361

Ответ 1

Наш SynScaleMM все еще экспериментальный.

EDIT: взгляните на более стабильный ScaleMM2 и новый SAPMM. Но мои замечания ниже по-прежнему заслуживают внимания: чем меньше вы занимаетесь, тем лучше вы масштабируетесь!

Но он работал, как ожидалось, в многопоточной серверной среде. Масштабирование намного лучше, чем FastMM4, для некоторых критических тестов.

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

Вот некоторые (а не догматические, как раз из опыта и знания низкого уровня Delphi RTL) советы, если вы хотите написать многопоточное приложение FAST в Delphi:

  • Всегда используйте const для параметров строкового или динамического массива, например, в MyFunc(const aString: String), чтобы не выделять временную строку для каждого вызова;
  • Избегайте использования конкатенации строк (s := s+'Blabla'+IntToStr(i)), но полагайтесь на буферизованное письмо, например TStringBuilder, доступное в последних версиях Delphi;
  • TStringBuilder также не идеален: например, он создаст много временных строк для добавления некоторых числовых данных и будет использовать чрезвычайно медленную функцию SysUtils.IntToStr() при добавлении значения integer - мне пришлось перезапишите множество низкоуровневых функций, чтобы избежать большинства строк в нашем классе TTextWriter, как определено в SynCommons.pas;
  • Не злоупотребляйте критическими разделами, позволяйте им быть как можно меньше, но полагайтесь на некоторые атомные модификаторы, если вам нужен параллельный доступ - см., например, InterlockedIncrement / InterlockedExchangeAdd;
  • InterlockedExchange (из SysUtils.pas) - хороший способ обновления буфера или общего объекта. Вы создаете обновленную версию некоторого контента в своем потоке, тогда вы обмениваетесь общим указателем на данные (например, экземпляр TObject) в одной низкоуровневой работе ЦП. Он будет уведомлять об изменении других потоков, с очень хорошим многопоточным масштабированием. Вам нужно будет заботиться о целостности данных, но на практике это работает очень хорошо.
  • Не разделяйте данные между потоками, а скорее создавайте собственную личную копию или полагайтесь на некоторые буферы только для чтения (шаблон

Ответ 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 станут препятствием для производительности. Как правило, по моему опыту это происходит, когда ваше приложение выполняет большую обработку строк.

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

Ответ 3

FastMM имеет дело с многопоточным просто. Это менеджер памяти по умолчанию для Delphi 2006 и выше.

Если вы используете более старую версию Delphi (Delphi 5 и выше), вы все равно можете использовать FastMM. Он доступен на SourceForge.

Ответ 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 -издания и т.д., вы можете получить результаты, что процедуры распределения памяти будут в несколько раз быстрее и не будет так сильно страдать от обсуждения разногласий.

Ответ 5

Вы можете использовать TopMM: http://www.topsoftwaresite.nl/

Вы также можете попробовать ScaleMM2 (SynScaleMM основан на ScaleMM1), но мне нужно исправить ошибку в отношении interthread памяти, поэтому не готово к производству все же:-( http://code.google.com/p/scalemm/

Ответ 6

Deplhi 6 Диспетчер памяти устарел и совершенно плох. Мы использовали RecyclerMM как на сервере с высокой нагрузкой, так и в многопоточном настольном приложении, и у нас не было никаких проблем с ним: он быстрый, надежный и не вызывает избыточной фрагментации. (Фрагментация была худшей проблемой менеджера памяти запаса Delphi).

Единственным недостатком RecyclerMM является то, что он не совместим с MemCheck из коробки. Тем не менее, небольшого изменения источника было достаточно, чтобы сделать его совместимым.