Нужен многопоточный менеджер памяти - PullRequest
14 голосов
/ 20 мая 2011

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

enter image description here

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

Спасибо

Ответы [ 6 ]

47 голосов
/ 20 мая 2011

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

РЕДАКТИРОВАТЬ: взгляните на более стабильную 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 экземпляр) в одной низкоуровневой операции ЦП. Он сообщит об изменении другим потокам с очень хорошим многопоточным масштабированием. Вам придется позаботиться о целостности данных, но на практике это работает очень хорошо.
  • Не обменивайтесь данными между потоками, а делайте свою личную копию или полагайтесь на некоторые буферы только для чтения (для масштабирования лучше использовать шаблон RCU );
  • Не использовать индексированный доступ к строковым символам, но полагаться на некоторые оптимизированные функции, такие как, например, PosEx();
  • Не смешивайте переменные / функции AnsiString/UnicodeString и проверяйте сгенерированный asm-код с помощью Alt-F2 для отслеживания любых скрытых нежелательных преобразований (например, call UStrFromPCharLen);
  • Вместо использования function вместо *1056* используйте параметры var в параметрах (функция, возвращающая string, добавит вызов UStrAsg/LStrAsg, который имеет LOCK, который сбрасывает все ядра ЦП);
  • Если вы можете для анализа данных или текста использовать указатели и некоторые статические буферы, выделенные для стека, вместо временных строк или динамических массивов;
  • Не создавайте TMemoryStream каждый раз, когда вам это нужно, но полагайтесь на частный экземпляр в вашем классе, уже измеренный в достаточном количестве памяти, в который вы будете записывать данные, используя Position, чтобы получить конец данных и не изменять его Size (который будет блоком памяти, выделенным ММ);
  • Ограничьте количество создаваемых экземпляров классов: попробуйте повторно использовать один и тот же экземпляр и, если можете, используйте несколько record/object указателей на уже выделенные буферы памяти, отображая данные без копирования их во временную память;
  • Всегда используйте основанную на тестировании разработку с выделенным многопоточным тестом, пытаясь достичь предела наихудшего случая (увеличить количество потоков, содержание данных, добавить некоторые несогласованные данные, сделать паузу в случайном порядке, попытаться подчеркнуть доступ к сети или диску , эталон с таймингом на реальных данных ...);
  • Никогда не доверяйте своему инстинкту, но используйте точную синхронизацию с реальными данными и процессами.

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

12 голосов
/ 20 мая 2011

Если ваше приложение может использовать лицензионный код 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 голосов
/ 14 июля 2017

Это блокировка , которая имеет значение!

Необходимо учитывать две проблемы:

  1. Использование префикса LOCK вСам Delphi (System.dcu);
  2. Как 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например, fork https://github.com/maximmasiutin/FastMM4

FastMM4 не самый быстрый для многоядерной работы, особенно когда число потоков больше, чем число физических сокетов, потому что по умолчанию это из-за конфликта потоков (то есть когдаодин поток не может получить доступ к данным, заблокированным другим потоком), вызывает функцию Windows API Sleep (0), иn, если блокировка все еще недоступна, входит в цикл, вызывая Sleep (1) после каждой проверки блокировки.

Каждый вызов Sleep (0) требует больших затрат на переключение контекста, которое может быть10000+ циклов;он также несет стоимость переходов от кольца 3 к кольцу 0, что может составлять 1000+ циклов.Что касается Sleep (1) - помимо затрат, связанных с Sleep (0) - он также задерживает выполнение не менее чем на 1 миллисекунду, передавая управление другим потокам, и, если нет потоков, ожидающих выполнения физическим ядром ЦП,переводит ядро ​​в спящий режим, эффективно сокращая использование процессора и энергопотребление.

Именно поэтому в многопоточных рабочих средах с FastMM загрузка ЦП никогда не достигала 100% - из-за Sleep (1), выпущенного FastMM4.Этот способ получения замков не является оптимальным.Лучшим способом была бы спин-блокировка из примерно 5000 pause инструкций, и, если блокировка все еще была занята, вызов API-вызова SwitchToThread ().Если pause недоступно (на очень старых процессорах без поддержки SSE2) или API-вызов SwitchToThread () не был доступен (на очень старых версиях Windows до Windows 2000), лучшим решением будет использование EnterCriticalSection / LeaveCriticalSection,которые не имеют задержки, связанной с Sleep (1), и которая также очень эффективно передает управление ядром ЦП другим потокам.

ThВ упомянутой выше развилке используется новый подход к ожиданию блокировки, рекомендованный Intel в своем Руководстве по оптимизации для разработчиков - спин-петля pause + SwitchToThread () и, если таковые имеются, недоступны: CriticalSections вместо Sleep (). С этими параметрами Sleep () никогда не будет использоваться, но вместо него будет использоваться EnterCriticalSection / LeaveCriticalSection. Тестирование показало, что подход с использованием CriticalSections вместо Sleep (который ранее использовался по умолчанию в FastMM4) обеспечивает значительный выигрыш в ситуациях, когда количество потоков, работающих с диспетчером памяти, равно или превышает количество физических ядер. Усиление еще более заметно на компьютерах с несколькими физическими процессорами и неоднородным доступом к памяти (NUMA). Я реализовал параметры времени компиляции, чтобы убрать оригинальный подход FastMM4 с использованием Sleep (InitialSleepTime) и затем Sleep (AdditionalSleepTime) (или Sleep (0) и Sleep (1)) и заменил их на EnterCriticalSection / LeaveCriticalSection, чтобы сохранить ценные циклы ЦП. не используется Sleep (0) и улучшает скорость (уменьшает задержку), на которую каждый раз влияет Sleep (1) как минимум на 1 миллисекунду, поскольку критические секции гораздо более дружественны к процессору и имеют определенно более низкую задержку, чем Sleep (1) .

Когда эти опции включены, FastMM4-AVX проверяет: (1) поддерживает ли процессор SSE2 и, таким образом, инструкцию «пауза», и (2) имеет ли операционная система вызов API SwitchToThread (), и, если оба условия выполняются, используется «пауза» спин-цикла для 5000 итераций, а затем SwitchToThread () вместо критических секций; Если у ЦПУ нет функции «паузы» или в Windows нет API-функции SwitchToThread (), он будет использовать EnterCriticalSection / LeaveCriticalSection.

Вы можете увидеть результаты теста, в том числе сделанные на компьютере с несколькими физическими процессорами (сокетами) в этой вилке.

См. Также статью Циклы ожидания с длительным вращением для технологии Intel Hyper-Threading с включенной технологией . Вот что Intel пишет об этой проблеме - и она очень хорошо относится к FastMM4:

В этой многопоточной модели длительный цикл ожидания ожидания редко вызывает проблемы с производительностью в традиционных многопроцессорных системах. Но это может привести к серьезным штрафам в системе с технологией Hyper-Threading, поскольку ресурсы процессора могут потребляться главным потоком, пока он ожидает рабочие потоки. Sleep (0) в цикле может приостановить выполнение главного потока, но только если все доступные процессоры были заняты рабочими потоками в течение всего периода ожидания. Это условие требует, чтобы все рабочие потоки завершили свою работу одновременно. Другими словами, рабочие нагрузки, назначенные рабочим потокам, должны быть сбалансированы. Если один из рабочих потоков завершает свою работу раньше других и освобождает процессор, главный поток все равно может работать на одном процессоре.

В обычной многопроцессорной системе это не вызывает проблем с производительностью, поскольку никакой другой поток не использует процессор. Но в системе с технологией Hyper-Threading процессор, на котором работает главный поток, является логическим и разделяет ресурсы процессора с одним из других рабочих потоков.

Характер многих приложений затрудняет обеспечение сбалансированности рабочих нагрузок, назначенных рабочим потокам. Например, многопоточное 3D-приложение может назначать задачи для преобразования блока вершин из мировых координат в просмотр координат для группы рабочих потоков. Объем работы для рабочего потока определяется не только количеством вершин, но и ограниченным состоянием вершины, что не предсказуемо, когда главный поток делит рабочую нагрузку на рабочие потоки.

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

Поэтому предпочтительным решением, позволяющим избежать напрасной траты ресурсов процессора в длительном цикле ожидания ожидания, является замена цикла на API блокировки потоков операционной системы, такой как API потоков Microsoft Windows *, WaitForMultipleObjects. Этот вызов заставляет операционную систему блокировать ожидающий поток от использования ресурсов процессора.

Это относится к Использование Spin-Loops на процессоре Intel Pentium 4 и процессоре Intel Xeon Замечание по применению.

Вы также можете найти очень хорошую реализацию спин-цикла здесь, в stackoverflow .

Он также загружает обычные нагрузки только для проверки перед выпуском lock -веденного хранилища, просто чтобы не переполнять ЦП заблокированными операциями в цикле, которые бы блокировали шину.

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

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

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

Таким образом, при правильной реализации блокировки в соответствии с вашими потребностями (т. Е. Нужна ли вам NUMA или нет, использовать ли версии lock и т. Д., Вы можете получить результаты, согласно которым процедуры выделения памяти будут в несколько раз быстрее. и не будет так сильно страдать от разногласий по теме.

2 голосов
/ 20 мая 2011

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

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

0 голосов
/ 20 мая 2011

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

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

0 голосов
/ 20 мая 2011

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

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

...