Performance: Datos en Memoria con ADO.NET IV

En este artículo quisiera mostrarles cual es el consumo de memoria de algunas técnicas de acceso a datos. En artículos anteriores hemos estudiado y optimizado performance mejorando el tiempo de procesamiento. Como colorario veremos algunos gráficos que siempre ayudan a la comparación.

Este artículo está relacionado con:

Presentación del escenario

Este es el contexto en el que estoy haciendo las mediciones:

Una aplicación Windows Forms, que utiliza 4 mecanismos para recuperar datos “de solo lectura” de la base de datos AdvertureWorks alojada en SQL Server 2005:

  • DataReader cargado en una lista genérica de objetos de entidad
  • DataSet
  • DataTable
  • DataSet tipificado creado con el asistente de Visual Studio 2005

Aquí subrayo “solo lectura” porque, justamente solo quiero recuperar los datos, y no hacer ninguna operación sobre ellos.

Memoria y Garbage Collector

Si bien sabemos que la administración de la memoria en .NET es un trabajo que le compete al Garbage Collector y que no es terreno en el que debamos hurgar, a no ser que sea por administración de memoria no manejada, siempre es bueno saber que uso hacemos de él. Si bien el Garbage Collector es un mecanismo muy optimizado, y hace un muy buen trabajo de recolección de basura (memoria no utilizada), tiene sus limitaciones y su costo. Sería una buena actitud de parte nuestra considerar al Garbage Collector como un recurso más, así como lo es la memoria. Teniendo en cuenta esto lograríamos minimizar su trabajo, lo cual redundaría en un mejor rendimiento de nuestra aplicación.

El Código

La versión completa del código podrás bajarla de aquí. De todas formas démosle un vistazo:

Esta es la sentencia sql a ejecutar en la base de datos AdventureWorks:

Select HumanResources.Employee.EmployeeID, Person.Contact.FirstName,
          Person.Contact.MiddleName, Person.Contact.LastName,
          HumanResources.Employee.Title, HumanResources.Employee.BirthDate, 
          Person.Address.AddressLine1, Person.Address.AddressLine2, 
          Person.Address.City, Person.Address.PostalCode, Person.Contact.EmailAddress, 
          Person.Contact.Phone, HumanResources.Employee.MaritalStatus, HumanResources.Employee.Gender 
          FROM HumanResources.Employee
          INNER JOIN Person.Contact ON HumanResources.Employee.ContactID = Person.Contact.ContactID
          INNER JOIN HumanResources.EmployeeAddress ON HumanResources.Employee.EmployeeID = HumanResources.EmployeeAddress.EmployeeID
          INNER JOIN Person.Address ON HumanResources.EmployeeAddress.AddressID = Person.Address.AddressID
          AND HumanResources.EmployeeAddress.AddressID = Person.Address.AddressID

La clase DataAccess

public class DataAccess
{
   
static readonly string _connString;
   
static readonly string _sqlCmd;

   
static DataAccess()
    {
        _connString =
"Password=;User ID=sa;Initial Catalog=AdventureWorks;Data Source=WALZER3";
       
//Obtengo la sentencia SQL que está en el archivo de texto Consulta.sql
       
StreamReader sr = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("Walzer.Antipracticas.Consulta.sql"));
        _sqlCmd = sr.ReadToEnd();
    }

    static public DataSet TraerDataSet()
    {
        
DataSet ds = null;
        
try
        
{
             
using (SqlConnection conn = new SqlConnection(_connString))
              {
                  conn.Open();
                 
SqlCommand cmd = new SqlCommand();
                  cmd.CommandText =
"GetEmployees";
                  cmd.Connection = conn;
                  cmd.CommandType =
CommandType.StoredProcedure;
                 
SqlDataAdapter da = new SqlDataAdapter(cmd);
                  ds =
new DataSet();
                  da.Fill(ds);
              }
         }
        
catch
        
{
         }
        
return ds;
     }

     static public List<Employee> TraerEmployeesOptimizado()    
     {
    
     List<Employee> employees = new List<Employee>();
         
try
         
{
         
using (SqlConnection conn = new SqlConnection(_connString))
          {
             
SqlCommand cmd = new SqlCommand();
              cmd.CommandText =
"GetEmployees";
              cmd.Connection = conn;
              cmd.CommandType =
CommandType.StoredProcedure;
              conn.Open();
             
using (SqlDataReader dr = cmd.ExecuteReader())
              {
                 
int colEmployeeId = dr.GetOrdinal("EmployeeId");
                 
int colFirstName = dr.GetOrdinal("FirstName");
                  // Omito las líneas similares por cuestión de lectura
                 
int colCount = dr.FieldCount;
                 
object[] values = new object[colCount];
                 
while (dr.Read())
                  {
                   
Employee employee = new Employee();
                    dr.GetValues(values);
                    employee.EmployeeID =
Convert.ToInt32(values[colEmployeeId]);
                    employee.FirstName =
Convert.ToString(values[colFirstName]);
                   
employees.Add(employee);
                  }
                }
             }
          }
         
catch
         
{
          }
         
return employees;
     }

     static public DataTable TraerDataTableOptimizado()
    
{
         
//Este método está optimizado para cargar un DataTable con datos de SOLO LECTURA
         
DataTable dt = null;
         
try
         
{
              
using (SqlConnection conn = new SqlConnection(_connString))
               {
                    conn.Open(); 
                   
SqlCommand cmd = new SqlCommand();cmd.CommandText = "GetEmployees"; 
                    cmd.Connection = conn; 
                    cmd.CommandType =
CommandType.StoredProcedure; 
                   
SqlDataAdapter da = new SqlDataAdapter(cmd);dt = new DataTable(); 
                    da.Fill(dt); 
               } 
          } 
          
catch 
          

          } 
          
return dt; 
     } 
}

El DataSet tipificado fue creado arrastrando la consulta SQL sobre la superficie de diseño del DataSet, lo único que escribí fue las siguientes líneas para cargar el DataSet tipificado:

DsEmployeesTableAdapters.GetEmployeesTableAdapter da = new Walzer.Antipracticas.DsEmployeesTableAdapters.GetEmployeesTableAdapter();
_dsEmployees = da.GetData();

Lectura del uso de Memoria

Vamos medir el uso de memoria de cada una de estas técnicas de acceso a datos utilizando 3 herramientas:

El CLR Profiler nos revela en una primera lectura de 290 registros representados en memoria por cada una de las técnicas.

En este gráfico podemos observar que el objeto del tipo AntiPracticas.frmMemoria, que es nuestra ventana, y sus referenciados consumen 836 Kb. Aunque la variable que apunta a esta estructura es de solo 368 bytes.

AntiPracticas.frmMemoria tiene cuatro campos privados que apuntan a:

  • un DataSet Tipado (DsEmployees.GetEmployeesDataTable): 313 Kb
  • un DataSet (Data.DataSet): 185 Kb
  • un DataTable (Data.DataTable ): 184 Kb
  • una colección genérica de objetos del tipo Employee (Generic.List<T>): 138 Kb

Aquí mismo podemos apreciar que el DataSetTipado es la estructura más costosa en cuanto a consumo de memoria. Que no hay casi diferencia entre un DataSet y un DataTable, y que la colección de objetos es la más barata. No está demás destacar que todas las estructuras contienen “los mismos datos”.

La misma información podemos verla en JetBrains DotTrace.


Observen la columna “Held Memory, bytes”, que es la memoria referenciada por cada instancia:

  • _dsEmployees (Walzer.Antipracticas.DsEmployees.GetEmployeesDataTable)
  • _ds (System.Data.DataSet)
  • _dt (System.Data.DataTable)
  • _employees (System.Collection.Generic.List<Employee>)

Estructura de Objetos en memoria

Comparemos en las siguientes dos capturas la complejidad de una y otra estructura, las cuales almacenan los mismos datos, de solo lectura en nuestro caso.

La primera figura nos muestra la lista genérica _employees, la cual está implementada internamente por un vector de _items, que contiene un conjunto de objetos Employee, la cual contiene finalmente los datos.

Observemos ahora la estructura de un DataSet tipificado, y el camino para llegar al dato final.

La estructura es mucho más compleja, pero no perdamos de vista que un DataSet fue diseñado con la premisa de propósito general, y mucho de su funcionalidad es útil. Debemos usar nuestro criterio a la hora de decidir que es mejor para nuestro sistema.

Inspeccionado contenido de las variables

Usemos ahora la herramienta .NET Memory Profiler para ver el contenido de un objeto del tipo Employee. Esta figura nos muestra las referencias a la que hace este objeto, que son System.String.

Pero, ¿dónde está el campo _idEmployee que es del tipo int o _birthDate que es de tipo DateTime? Bien, estos están contenidos en el mismo espacio de memoria que el objeto del tipo Employee ya que son tipos básicos, int y ulong respectivamente. En cambio System.String es una referencia al espacio de memoria donde está guardada la cadena de caracteres. La solapa Field Values nos muestra el contenido de la instancia #12,729 del objeto del tipo Employee. Además de esta información podemos apreciar, cuales son los caminos al Root de este objeto, y cuál fue el Call Stack que instanció este objeto en memoria.

Cantidad de Objetos referenciados

Un dato que no es menor aquí es el que nos muestra la columna “Held Objects”. Esta nos dice cuantos objetos son referenciados en toda la estructura en memoria.

En este caso la cantidad de filas en memoria para cada estructura es de 10, valor que se asemeja más a la realidad, ya que no es buena práctica pasar todas las filas del resultado entre capas, sino usar técnicas de paginación.

  • _dsEmployees (Walzer.Antipracticas.DsEmployees.GetEmployeesDataTable): 320
  • _ds (System.Data.DataSet): 213
  • _dt (System.Data.DataTable): 206
  • _employees (System.Collection.Generic.List<Employee>): 122

Más allá de la cantidad de memoria en bytes, la cantidad de objetos referenciados nos da una idea del trabajó que tendrá el Garbage Collector al momento de deshacerse de estos objetos. Cuantas más referencias en memoria, más recursos consumidos por este algoritmo.

Comparación de resultados

Veamos una serie de gráficos que resumen las lecturas realizadas. Tomé lecturas de 290 registros, 10 registros (que es el típico caso del tamaño de una página cuando se realiza paginación) y 1 registro.

 

Bytes en Memoria 290 reg 10 reg 1 reg
DataSet 189112 19028 14856
DataTable 188848 18764 14588
List<> 141774 4790 514
DataSet Tipado (wizard) 319478 33114 28902

 Fig1: Tabla Comparativa de Bytes en Memoria

Fig2: Bytes en memoria para 290 registros

Fig3: Bytes en memoria de 10 y 1 registro.

 

Referencias 290 reg 10 reg 1 reg
DataSet 3581 213 105
DataTable 3574 206 98
List<> 3477 122 13
DataSet Tipado (wizard) 3788 300 192

Fig4: Tabla Comparativa de Objetos Referenciados

Fig5: Instancias referenciadas para 290 registros

Fig6: Instancias referenciadas para 10 y 1 registro

Estos gráficos muestran claramente que la técnica más económica es pasar entre capas una lista genérica de un tipo específico. Y que la ferretería utilizada por las estructuras del tipo DataSet se puede despreciar cuanto mayor es el volumen que contienen.

Conclusión

Hemos comprobado que el uso correcto de las técnicas de acceso a datos en ADO.NET nos permite lograr un mayor rendimiento en nuestras aplicaciones. También hemos aprendido algo de cómo funciona internamente ADO.NET, y como son las estructuras en memoria y el uso que se hace de ellas.

Siempre es bueno conocer cómo funcionan internamente los frameworks que utilizamos para construir nuestras aplicaciones para poner en la balanza, facilidad y agilidad de uso contra rendimiento y consumo de recursos.

Published Tuesday, May 20, 2008 9:50 PM by cwalzer

Comments

# re: Performance: Datos en Memoria con ADO.NET IV

Friday, May 23, 2008 11:52 AM by Jersson

Creo que esta demas decir que está muy bueno el artículo!

ya probaste el yslow?

Un saludo.

# La Caja - Presentación / Ndepend (Analyze Code/Structure Tool!) / Artículo Recomendado

Saturday, May 24, 2008 5:49 PM by Jersson on Geeks·ms

Presentación Buenas, tal como habia comentado en un post anterior, la idea de esta sección es (no, no

# La Caja - Presentación / Ndepend (Analyze Code/Structure Tool!) / Artículo Recomendado

Saturday, May 24, 2008 5:59 PM by Jersson on Geeks·ms

Presentación Buenas, tal como habia comentado en un post anterior, la idea de esta sección es (no, no

Leave a Comment

(required) 
(required) 
(optional)
(required) 
Powered by Community Server (Commercial Edition), by Telligent Systems