2012-05-11 11 views
35

Durante mucho tiempo he notado que la versión Win64 de la aplicación de mi servidor tiene una fuga de memoria. Si bien la versión Win32 funciona bien con una memoria relativamente estable, la memoria utilizada por la versión de 64 bits aumenta regularmente, tal vez 20Mb/día, sin ninguna razón aparente (Huelga decir que FastMM4 no informó ninguna fuga de memoria para ambos) . El código fuente es idéntico entre la versión de 32 bits y la de 64 bits. La aplicación se basa en el componente Indy TIdTCPServer, es un servidor altamente multiproceso conectado a una base de datos que procesa los comandos enviados por otros clientes con Delphi XE2.¿Pérdida de memoria en Win64 Delphi RTL durante el cierre del hilo?

Paso mucho tiempo revisando mi propio código y tratando de entender por qué la versión de 64 bits gastó tanta memoria. Terminé usando herramientas MS diseñadas para rastrear fugas de memoria como DebugDiag y XPerf y parece que hay un error fundamental en Delphi 64bit RTL que hace que se filtren algunos bytes cada vez que un hilo se separó de un archivo DLL. Este problema es particularmente crítico para las aplicaciones con múltiples subprocesos que deben ejecutarse 24/7 sin reiniciarse.

He reproducido el problema con un proyecto muy básico que está compuesto por una aplicación de host y una biblioteca, ambos construidos con XE2. La DLL está vinculada estáticamente con la aplicación de host. La aplicación de host crea hilos que acaba de llamar al procedimiento y salida ficticia exportados:

Aquí está el código fuente de la librería:

library FooBarDLL; 

uses 
    Windows, 
    System.SysUtils, 
    System.Classes; 

{$R *.res} 

function FooBarProc(): Boolean; stdcall; 
begin 
    Result := True; //Do nothing. 
end; 

exports 
    FooBarProc; 

la aplicación host utiliza un temporizador para crear un hilo que llame a la exporta procedimiento:

TFooThread = class (TThread) 
    protected 
    procedure Execute; override; 
    public 
    constructor Create; 
    end; 

... 

function FooBarProc(): Boolean; stdcall; external 'FooBarDll.dll'; 

implementation 

{$R *.dfm} 

procedure THostAppForm.TimerTimer(Sender: TObject); 
begin 
    with TFooThread.Create() do 
    Start; 
end; 

{ TFooThread } 

constructor TFooThread.Create; 
begin 
    inherited Create(True); 
    FreeOnTerminate := True; 
end; 

procedure TFooThread.Execute; 
begin 
    /// Call the exported procedure. 
    FooBarProc(); 
end; 

Aquí es algunas capturas de pantalla que muestran la fuga utilizando VMMap (ver la línea roja llamado "Montón"). Las siguientes capturas de pantalla se tomaron dentro de un intervalo de 30 minutos.

El binario de 32 bits muestra un aumento de 16 bytes, que es totalmente aceptable:

Memory usage for the 32 bit version http://img401.imageshack.us/img401/6159/soleak32.png

El bit binario 64 muestra un aumento de 12476 bytes (de 820K a 13296K), que es más problemático :

Memory usage for the 64 bit version http://img12.imageshack.us/img12/209/soleak64.png

El aumento constante de la memoria de pila se confirma también por XPerf:

XPerf usage http://desmond.imageshack.us/Himg825/scaled.php?server=825&filename=soxperf.png&res=landing

Usando DebugDiag que era capaz de ver la ruta de código que se asigna la memoria perdida:

LeakTrack+13529 
<my dll>!Sysinit::AllocTlsBuffer+13 
<my dll>!Sysinit::InitThreadTLS+2b 
<my dll>!Sysinit::::GetTls+22 
<my dll>!System::AllocateRaiseFrame+e 
<my dll>!System::DelphiExceptionHandler+342 
ntdll!RtlpExecuteHandlerForException+d 
ntdll!RtlDispatchException+45a 
ntdll!KiUserExceptionDispatch+2e 
KERNELBASE!RaiseException+39 
<my dll>!System::::RaiseAtExcept+106 
<my dll>!System::::RaiseExcept+1c 
<my dll>!System::ExitDll+3e 
<my dll>!System::::Halt0+54 
<my dll>!System::::StartLib+123 
<my dll>!Sysinit::::InitLib+92 
<my dll>!Smart::initialization+38 
ntdll!LdrShutdownThread+155 
ntdll!RtlExitUserThread+38 
<my application>!System::EndThread+20 
<my application>!System::Classes::ThreadProc+9a 
<my application>!SystemThreadWrapper+36 
kernel32!BaseThreadInitThunk+d 
ntdll!RtlUserThreadStart+1d 

Remy Lebeau helped me on the Embarcadero forums de entender lo que estaba sucediendo:

El segundo aspecto de fugas más como un error definido. Durante el cierre del hilo , se llama a StartLib(), que llama a ExitThreadTLS() a libera el bloque de memoria TLS del hilo llamante, luego llama a Halt0() al llama a ExitDll() para generar una excepción capturada por DelphiExceptionHandler() llamar a AllocateRaiseFrame(), que indirectamente llama a GetTls() y, por tanto, InitThreadTLS() cuando tiene acceso a una variable threadvar llamada ExceptionObjectCount.Eso vuelve a asignar el bloque de memoria TLS de la cadena de llamada que todavía está en el proceso de apagado. Por lo tanto, StartLib() no debería llamar a Halt0() durante DLL_THREAD_DETACH, o DelphiExceptionHandler no debería estar llamando a AllocateRaiseFrame() cuando detecta que se está produciendo _TExitDllException.

Parece claro para mí que hay una falla importante en el modo Win64 para manejar el cierre de subprocesos. Un comportamiento de este tipo prohíbe el desarrollo de cualquier aplicación de servidor multiproceso que deba ejecutarse 27/7 bajo Win64.

Así:

  1. ¿Qué opinas de mis conclusiones?
  2. ¿Alguno de ustedes tiene una solución para este problema?

El código fuente de prueba y los binarios can be downloaded here.

Gracias por su contribución!

Editar: QC Report 105559. Estoy esperando tus votos :-)

+3

"Alguno de ustedes tiene una solución para este problema" Usaría la aplicación de 32 bits hasta la próxima estable lanzamiento de delphi con compilador de 64 bits viene junto ... – ComputerSaysNo

+4

Si yo fuera usted, lo reduciría a una muestra de tamaño mínimo, que presenta la fuga, y simplemente enviarlo a control de calidad. –

+0

@DorinDuminica: eso sería Delphi XE4;) – whosrdaddy

Respuesta

2

Un trabajo muy simple es reutilizar el hilo y no crearlo ni destruirlo. Los hilos son bastante caros, probablemente también recibas un impulso de perf. ... Felicitaciones por la depuración ...

+0

Sí, esa fue mi primera idea. En mi caso específico, ciertamente podría usar un grupo de subprocesos. Pero eso no evitará que el código de terceros incluido en mi proyecto programe nuevos hilos que también se filtrarán ... –

+0

Eso es cierto, pero si tiene la fuente para sus cosas de terceros, puede adaptarlo para usar su grupo de hilos , si no, no hay nada que pueda hacer al respecto de todos modos ... Si tiene que ejecutar dlls con fugas externas, debe hacerlo en un proceso separado que puede reiniciar cada de vez en cuando, pero eso no siempre es posible. – Eric