需要多线程内存管理器

时间:2011-05-20 13:00:52

标签: delphi delphi-xe

我将很快创建一个多线程项目我很快就看到了实验(delphitools.info/2011/10/13/memory-manager-investigations),显示默认的Delphi内存管理器存在多线程问题。

enter image description here

所以,我找到了这个SynScaleMM。任何人都可以对它或类似的内存管理器给出一些反馈?

由于

6 个答案:

答案 0 :(得分:47)

我们的SynScaleMM仍处于试验阶段。

编辑:看看更稳定的ScaleMM2和全新的SAPMM。但是我的下面的评论仍然值得关注:你做的分配越少,你的规模越大!

但它在多线程服务器环境中按预期工作。对于某些关键测试,缩放比FastMM4要好得多。

但内存管理器可能不是多线程应用程序中的更大瓶颈。如果你不强调它,FastMM4可以很好地工作。

如果你想在Delphi中编写FAST多线程应用程序,这里有一些(不是教条,只是来自实验和低级Delphi RTL的知识)建议:

  • 始终对const中的字符串或动态数组参数使用MyFunc(const aString: String),以避免为每次调用分配临时字符串;
  • 避免使用字符串连接(s := s+'Blabla'+IntToStr(i)),但依赖于最新版本的Delphi中提供的TStringBuilder等缓冲写入;
  • TStringBuilder也不完美:例如,它会创建大量临时字符串以附加一些数字数据,并在添加一些{{1}时使用非常慢的SysUtils.IntToStr()函数} value - 我必须重写许多低级函数,以避免在SynCommons.pas中定义的integer类中的大多数字符串分配;
  • 不要滥用关键部分,让它们尽可能小,但如果你需要一些并发访问,可以依赖一些原子修饰符 - 参见例如TTextWriter;
  • InterlockedIncrement / InterlockedExchangeAdd(来自SysUtils.pas)是更新缓冲区或共享对象的好方法。您在线程中创建某些内容的更新版本,然后在一个低级CPU操作中交换指向数据的共享指针(例如InterlockedExchange实例)。它将通过非常好的多线程扩展通知其他线程的更改。您必须处理数据完整性,但它在实践中非常有效。
  • 不要在线程之间共享数据,而是创建自己的私有副本或依赖一些只读缓冲区(RCU模式更适合扩展);
  • 不要对字符串字符使用索引访问,而是依赖于某些优化函数,例如TObject;
  • 不要混用PosEx()种变量/函数,并通过Alt-F2检查生成的asm代码,以跟踪任何隐藏的不需要的转换(例如AnsiString/UnicodeString);
  • 而是在call UStrFromPCharLen而不是var中使用procedure参数返回一个字符串(返回function的函数将添加一个string调用LOCK将刷新所有CPU内核);
  • 如果可以,对于数据或文本解析,请使用指针和一些静态堆栈分配的缓冲区而不是临时字符串或动态数组;
  • 每次需要时都不要创建UStrAsg/LStrAsg,而是依赖于类中的私有实例,已经在足够的内存中进行调整,您将使用TMemoryStream来编写数据以检索数据数据结束而不更改其Position(将是MM分配的内存块);
  • 限制您创建的类实例的数量:尝试重用相同的实例,如果可以的话,在已分配的内存缓冲区上使用一些Size指针,映射数据而不将其复制到临时内存中;
  • 始终使用测试驱动开发,通过专用的多线程测试,尝试达到更糟糕的限制(增加线程数,数据内容,添加一些不连贯的数据,随机暂停,尝试强调网络或磁盘访问,基准与实际数据的时间安排......);
  • 永远不要相信自己的直觉,但要对实际数据和流程使用准确的时间安排。

我尝试在开源框架中遵循这些规则,如果你看看我们的代码,你会发现很多真实的示例代码。

答案 1 :(得分:12)

如果您的应用可以使用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中的线程争用成为性能障碍之前,您的应用必须非常努力地锤击堆分配器。通常根据我的经验,当您的应用程序执行大量字符串处理时会发生这种情况。

对于在堆分配上遇到线程争用的任何人,我的主要建议是重新编写代码以避免命中堆。你不仅避免了争用,而且还避免了堆分配的费用 - 一个经典的双重角色!

答案 2 :(得分:3)

正是locking才有所作为!

有两个问题需要注意:

  1. Delphi本身使用LOCK前缀(System.dcu);
  2. FastMM4如何处理线程争用及其在获取锁定失败后的作用。
  3. Delphi本身使用LOCK前缀

    1999年发布的Borland Delphi 5是在字符串操作中引入lock前缀的那个。如您所知,当您将一个字符串分配给另一个字符串时,它不会复制整个字符串,只会增加字符串中的引用计数器。如果修改字符串,则为de-references,减少引用计数器并为修改后的字符串分配单独的空间。

    在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相比,它们变得非常慢,即使对于单线程应用程序也是如此。

    为了克服这种缓慢,程序员开始使用&#34;单线程&#34;带锁定的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毫秒,将控制权交给其他线程,如果没有线程等待物理CPU核心执行,使核心进入睡眠状态,有效降低CPU使用率和功耗。

    这就是为什么在使用FastMM的多线程wotk上,CPU使用从未达到100% - 因为FastMM4发布了Sleep(1)。这种获取锁的方式不是最佳的。更好的方法是旋转锁定约5000 pause个指令,如果锁仍然忙,则调用SwitchToThread()API调用。如果pause不可用(在没有SSE2支持的非常旧的处理器上)或者SwitchToThread()API调用不可用(在非常旧的Windows版本上,在Windows 2000之前),最好的解决方案是使用EnterCriticalSection / LeaveCriticalSection,没有Sleep(1)关联的延迟,也非常有效地控制CPU核心到其他线程。

    我所提到的分支使用了一种新的方法来等待锁定,英特尔在其Optimization Manual中为开发人员推荐 - 一个pause + SwitchToThread()的自旋循环,以及如果其中任何一个不可用:CriticalSections而不是Sleep()。使用这些选项,将永远不会使用Sleep(),而是使用EnterCriticalSection / LeaveCriticalSection。测试表明,使用CriticalSections而不是Sleep(在FastMM4之前默认使用)的方法在使用内存管理器的线程数与物理内核数相同或更高的情况下提供了显着的增益。在具有多个物理CPU和非统一内存访问(NUMA)的计算机上,增益更加明显。我已经实现了编译时选项来取消使用Sleep(InitialSleepTime)然后Sleep(AdditionalSleepTime)(或Sleep(0)和Sleep(1))的原始FastMM4方法,并用EnterCriticalSection / LeaveCriticalSection替换它们以节省宝贵的CPU周期被Sleep(0)浪费并提高速度(降低延迟),每次受Sleep(1)影响至少1毫秒,因为关键部分对CPU更友好,并且具有明显低于睡眠的延迟(1)

    当启用这些选项时,FastMM4-AVX会检查:(1)CPU是否支持SSE2,因此&#34;暂停&#34;指令,以及(2)操作系统是否具有SwitchToThread()API调用,如果满足两个条件,则使用&#34; pause&#34;旋转循环5000次迭代然后SwitchToThread()而不是关键部分;如果CPU没有&#34;暂停&#34; instrcution或Windows没有SwitchToThread()API函数,它将使用EnterCriticalSection / LeaveCriticalSection。

    您可以看到测试结果,包括在该分支中具有多个物理CPU(插槽)的计算机上进行的测试。

    另请参阅Long Duration Spin-wait Loops on Hyper-Threading Technology Enabled Intel Processors文章。以下是英特尔针对此问题所写的内容 - 它非常适用于FastMM4:

      

    此线程模型中的长持续时间自旋等待循环很少会导致传统多处理器系统出现性能问题。但是它可能会对使用超线程技术的系统造成严重的损失,因为主线程在等待工作线程时可以使用处理器资源。循环中的Sleep(0)可以暂停主线程的执行,但仅在整个等待期间工作线程已经占用所有可用处理器时。此条件要求所有工作线程同时完成其工作。换句话说,必须平衡分配给工作线程的工作负载。如果其中一个工作线程比其他线程更快地完成其工作并释放处理器,则主线程仍然可以在一个处理器上运行。

         

    在传统的多处理器系统上,这不会导致性能问题,因为没有其他线程使用处理器。但是在具有超线程技术的系统上,主线程运行的处理器是一个逻辑的,它与其他工作线程之一共享处理器资源。

         

    许多应用程序的性质使得很难保证分配给工作线程的工作负载得到平衡。例如,多线程3D应用程序可以分配用于将顶点块从世界坐标变换到查看坐标到工作线程团队的任务。工作线程的工作量不仅取决于顶点的数量,还取决于顶点的剪切状态,这在主线程划分工作线程的工作负载时是不可预测的。

         

    Sleep函数中的非零参数会强制等待线程在N毫秒内休眠,无论处理器是否可用。如果正确设置等待时间,它可以有效地阻止等待线程消耗处理器资源。但是如果从工作负载到工作负载的等待时间是不可预测的,那么大的N值可能会使等待的线程睡眠时间过长,而较小的N值可能会导致它过快地唤醒。

         

    因此,避免在长时间的自旋等待循环中浪费处理器资源的首选解决方案是用操作系统线程阻塞API(例如Microsoft Windows *线程API)替换循环,    WaitForMultipleObjects的。此调用导致操作系统阻止等待线程消耗处理器资源。

    它指的是Using Spin-Loops on Intel Pentium 4 Processor and Intel Xeon Processor应用说明。

    您还可以找到一个非常好的自旋循环实现here at stackoverflow

    它还会加载正常负载,只是为了在发出lock - ed存储之前进行检查,只是为了不使用循环中的锁定操作来占用CPU,这会锁定总线。

    FastMM4本身非常好。只需改进锁定,你就会得到一个优秀的多线程内存管理器。

    另请注意,每个小块类型都在FastMM4中单独锁定。

    您可以在小块控制区域之间放置填充,使每个区域都有自己的缓存行,不与其他块大小共享,并确保它从缓存行大小边界开始。您可以使用CPUID来确定CPU缓存行的大小。

    因此,通过正确实现锁定以满足您的需求(即是否需要NUMA,是否使用lock - 版本等,您可以获得内存分配例程将为几个的结果时间更快,不会受到线程争用的严重影响。

答案 3 :(得分:2)

FastMM处理多线程就好了。它是Delphi 2006及以上版本的默认内存管理器。

如果您使用的是旧版本的Delphi(Delphi 5及更高版本),您仍然可以使用FastMM。它可以在SourceForge上找到。

答案 4 :(得分:0)

您可以使用TopMM: http://www.topsoftwaresite.nl/

您也可以尝试使用ScaleMM2(SynScaleMM基于ScaleMM1)但我必须修复有关内置内存的错误,因此尚未准备好生产:-( http://code.google.com/p/scalemm/

答案 5 :(得分:-1)

Deplhi 6内存管理器已经过时而且完全不好。我们在高负载生产服务器和多线程桌面应用程序上都使用RecyclerMM,我们没有遇到任何问题:它快速,可靠并且不会导致过多的碎片。 (碎片化是Delphi的库存管理器最糟糕的问题)。

RecyclerMM的唯一缺点是它与开箱即用的MemCheck不兼容。但是,一个小的源更改足以使其兼容。