Это блокировка , которая имеет значение!
Необходимо учитывать две проблемы:
- Использование префикса
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например, 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
и т. Д., Вы можете получить результаты, согласно которым процедуры выделения памяти будут в несколько раз быстрее. и не будет так сильно страдать от разногласий по теме.