2009-04-01 10 views
6

Escenario: más de 1,5 GB de texto y archivos csv que necesito procesar matemáticamente. Intenté usar SQL Server Express, pero cargar la información, incluso con la importación BULK, lleva mucho tiempo, e idealmente necesito tener todo el conjunto de datos en memoria, para reducir el IO del disco duro.¿Por qué no puedo aprovechar 4 GB de RAM en mi computadora para procesar menos de 2 GB de información en C#?

Hay más de 120,000,000 de registros, pero incluso cuando intento filtrar la información a una sola columna (en memoria), mi aplicación de consola C# está consumiendo ~ 3.5GB de memoria para procesar solo 125MB (700MB actualmente read-in) de texto.

Parece que las referencias a las cadenas y matrices de cadenas no están siendo recopiladas por el GC, incluso después de establecer todas las referencias a los ID identificables nulos y encapsulantes con la palabra clave using.

Creo que el culpable es el método String.Split() que está creando una nueva cadena para cada valor separado por comas.

Puede sugerir que ni siquiera debería leer las columnas innecesarias * en una matriz de cadenas, pero eso pasa por alto: ¿Cómo puedo colocar este conjunto completo de datos en la memoria, por lo que puedo procesarlo en paralelo en DO#?

Pude optimizar los algoritmos estadísticos y coordinar las tareas con un sofisticado algoritmo de programación, pero esto es algo que esperaba hacer antes de tener problemas de memoria, y no por eso.

He incluido una aplicación de consola completa que simula mi entorno y debería ayudar a replicar el problema.

Cualquier ayuda es apreciada. Gracias por adelantado.

using System; 
using System.Collections.Generic; 
using System.Text; 
using System.IO; 

namespace InMemProcessingLeak 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      //Setup Test Environment. Uncomment Once 
      //15000-20000 files would be more realistic 
      //InMemoryProcessingLeak.GenerateTestDirectoryFilesAndColumns(3000, 3); 
      //GC 
      GC.Collect(); 
      //Demostrate Large Object Memory Allocation Problem (LOMAP) 
      InMemoryProcessingLeak.SelectColumnFromAllFiles(3000, 2); 
     } 
    } 

    class InMemoryProcessingLeak 
    { 
     public static List<string> SelectColumnFromAllFiles(int filesToSelect, int column) 
     { 
      List<string> allItems = new List<string>(); 
      int fileCount = filesToSelect; 
      long fileSize, totalReadSize = 0; 

      for (int i = 1; i <= fileCount; i++) 
      { 
       allItems.AddRange(SelectColumn(i, column, out fileSize)); 
       totalReadSize += fileSize; 
       Console.Clear(); 
       Console.Out.WriteLine("Reading file {0:00000} of {1}", i, fileCount); 
       Console.Out.WriteLine("Memory = {0}MB", GC.GetTotalMemory(false)/1048576); 
       Console.Out.WriteLine("Total Read = {0}MB", totalReadSize/1048576); 
      } 
      Console.ReadLine(); 
      return allItems; 

     } 

     //reads a csv file and returns the values for a selected column 
     private static List<string> SelectColumn(int fileNumber, int column, out long fileSize) 
     { 
      string fileIn; 
      FileInfo file = new FileInfo(string.Format(@"MemLeakTestFiles/File{0:00000}.txt", fileNumber)); 
      fileSize = file.Length; 
      using (System.IO.FileStream fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)) 
      { 
       using (System.IO.StreamReader sr = new System.IO.StreamReader(fs)) 
       { 
        fileIn = sr.ReadToEnd(); 
       } 
      } 

      string[] lineDelimiter = { "\n" }; 
      string[] allLines = fileIn.Split(lineDelimiter, StringSplitOptions.None); 

      List<string> processedColumn = new List<string>(); 

      string current; 
      for (int i = 0; i < allLines.Length - 1; i++) 
      { 
       current = GetColumnFromProcessedRow(allLines[i], column); 
       processedColumn.Add(current); 
      } 

      for (int i = 0; i < lineDelimiter.Length; i++) //GC 
      { 
       lineDelimiter[i] = null; 
      } 
      lineDelimiter = null; 

      for (int i = 0; i < allLines.Length; i++) //GC 
      { 
       allLines[i] = null; 
      } 
      allLines = null; 
      current = null; 

      return processedColumn; 
     } 

     //returns a row value from the selected comma separated string and column position 
     private static string GetColumnFromProcessedRow(string line, int columnPosition) 
     { 
      string[] entireRow = line.Split(",".ToCharArray()); 
      string currentColumn = entireRow[columnPosition]; 
      //GC 
      for (int i = 0; i < entireRow.Length; i++) 
      { 
       entireRow[i] = null; 
      } 
      entireRow = null; 
      return currentColumn; 
     } 

     #region Generators 
     public static void GenerateTestDirectoryFilesAndColumns(int filesToGenerate, int columnsToGenerate) 
     { 
      DirectoryInfo dirInfo = new DirectoryInfo("MemLeakTestFiles"); 
      if (!dirInfo.Exists) 
      { 
       dirInfo.Create(); 
      } 
      Random seed = new Random(); 

      string[] columns = new string[columnsToGenerate]; 

      StringBuilder sb = new StringBuilder(); 
      for (int i = 1; i <= filesToGenerate; i++) 
      { 
       int rows = seed.Next(10, 8000); 
       for (int j = 0; j < rows; j++) 
       { 
        sb.Append(GenerateRow(seed, columnsToGenerate)); 
       } 
       using (TextWriter tw = new StreamWriter(String.Format(@"{0}/File{1:00000}.txt", dirInfo, i))) 
       { 
        tw.Write(sb.ToString()); 
        tw.Flush(); 
       } 
       sb.Remove(0, sb.Length); 
       Console.Clear(); 
       Console.Out.WriteLine("Generating file {0:00000} of {1}", i, filesToGenerate); 
      } 
     } 

     private static string GenerateString(Random seed) 
     { 
      StringBuilder sb = new StringBuilder(); 
      int characters = seed.Next(4, 12); 
      for (int i = 0; i < characters; i++) 
      { 
       sb.Append(Convert.ToChar(Convert.ToInt32(Math.Floor(26 * seed.NextDouble() + 65)))); 
      } 
      return sb.ToString(); 
     } 

     private static string GenerateRow(Random seed, int columnsToGenerate) 
     { 
      StringBuilder sb = new StringBuilder(); 

      sb.Append(seed.Next()); 
      for (int i = 0; i < columnsToGenerate - 1; i++) 
      { 
       sb.Append(","); 
       sb.Append(GenerateString(seed)); 
      } 
      sb.Append("\n"); 

      return sb.ToString(); 
     } 
     #endregion 
    } 
} 

* se necesitarán Estas otras columnas y se accede tanto en forma secuencial y al azar a través de la vida del programa, así que leer desde el disco cada vez que hay una sobrecarga tremendamente exigente.

** Medio Ambiente Notas: 4 GB de memoria SDRAM DDR2 800, Core 2 Duo 2.5Ghz, .NET Runtime 3.5 SP1, Vista 64.

+0

Además de las respuestas a continuación, me he dado cuenta de que utiliza la lista , que se basa en una matriz. Hasta donde yo sé, el tamaño de la matriz se duplica cada vez que alcanzas su capacidad actual. Así que esto podría ser una verdadera amenaza una vez que se alcanza un límite específico. –

Respuesta

14

Sí, String.split crea un nuevo objeto String para cada "pieza" - eso es lo que debe hacer.

Ahora, tenga en cuenta que las cadenas en .NET son Unicode (UTF-16 realmente), y con la sobrecarga del objeto el costo de una cadena en bytes es aproximadamente 20 + 2*n donde n es el número de caracteres.

Eso significa que si tiene un montón de cadenas pequeñas, se necesitará una gran cantidad de memoria en comparación con el tamaño de los datos de texto involucrados. Por ejemplo, una línea de 80 caracteres dividida en cadenas de 10 x 8 caracteres ocupará 80 bytes en el archivo, pero 10 * (20 + 2 * 8) = 360 bytes en la memoria: ¡una explosión de 4.5x!

Dudo que se trate de un problema de GC, y le aconsejo que elimine los valores de configuración de sentencias adicionales a nulos cuando no es necesario, simplemente un problema de tener demasiados datos.

Lo que me gustaría sugerir es que lea el archivo línea por línea (usando TextReader.ReadLine() en lugar de TextReader.ReadToEnd()). Claramente tener todo el archivo en la memoria si no lo necesita es un desperdicio.

+0

Respuesta extremadamente informativa. Como sugirió MSalters, parece que tendría que representar los datos de una manera diferente si quiero trabajar con toda la información a la vez. – exceptionerror

+0

Sí, aunque al final todavía te encontrarás con problemas. Si puede encontrar una manera de procesar los datos en forma de transmisión, la solución escalará mucho mejor. –

+0

¿Recomendarías algo como "push" linq para que pueda extraer información relacional entre archivos sin bucle? – exceptionerror

3

Sugiero leer línea por línea en lugar de todo el archivo, o un bloque de hasta 1mb.

Actualización:
De los comentarios de Jon tenía curiosidad y experimentó con 4 métodos:

  • StreamReader.ReadLine (defecto y tampón personalizado tamaño),
  • StreamReader.ReadToEnd
  • Mi método listados arriba.

La lectura de un archivo de registro de 180MB:

  • ms ReadLine: 1937 búfer
  • ReadLine grande, ms ASCII: 1926
  • ms ReadToEnd: 2151
  • ms personalizados: 1415

El StreamReader personalizado era:

01 búfer
StreamReader streamReader = new StreamReader(fileStream, Encoding.Default, false, 16384) 

de StreamReader por defecto es 1024.

Para el consumo de memoria (la pregunta real!) - ~ 800 MB utilizado. Y el método que doy todavía usa un StringBuilder (que usa una cadena) para que no haya menos consumo de memoria.

+0

O simplemente llame a TextReader.ReadLine() ... eso es lo que está ahí para ... –

+0

(También recomendaría usar una declaración "using" para evitar dejar la secuencia abierta en caso de una excepción, y renombrar "bytesRead" "to" charactersRead ".) –

+0

Editaré mi respuesta ya que me contradigo + actualizo ese código de 3 años con sus sugerencias. El tamaño del búfer 16384 fue la principal diferencia que surgió de una discusión en microsoft.public.dotnet.languages.csharp sobre el rendimiento de C++ frente al C# para el tamaño del texto. –

2

Los lenguajes de GC modernos aprovechan las grandes cantidades de RAM baratas para descargar tareas de gestión de memeory. Esto impone una cierta sobrecarga, pero la aplicación empresarial típica en realidad no necesita mucha información de todos modos. Muchos programas funcionan con menos de mil objetos. Administrar manualmente muchos es una tarea ardua, pero incluso miles de bytes por cada sobrecarga de objetos no importarían.

En su caso, la sobrecarga por objeto se está convirtiendo en un problema. Por ejemplo, puede considerar representar cada columna como un objeto, implementado con una sola Cadena y una matriz de compensaciones enteras. Para devolver un solo campo, devuelve una subcadena (posiblemente como una cuña)

+0

Parece que he agotado las mejores prácticas disponibles de C#, y su respuesta me indicó la siguiente mejor opción. Realmente me gusta C#, pero me pregunto si sería una buena idea aprender y trabajar con C++/CLI en el futuro, si encuentro otros desafíos intensivos en datos como este. – exceptionerror

+0

Considere C++ nativo; puede ser bastante eficiente en estos casos. Sí, tendrá que escribir una gran cantidad de código para la funcionalidad que se incluye en C#. Pero ese es exactamente el punto; usted es uno de los pocos que no puede pagar los valores predeterminados de .Net. – MSalters

+0

Tuve un problema muy similar hace unos años cuando experimenté con una utilidad de conversión de .NET DB desechable. No pude hacer que .net funcionara rápido, pero una aplicación OLEDB de C++ muy simple funcionó muy rápido. Pensé que la .net lib que estaba usando era memoria wrt muy ineficiente. – gbjbaanb

Cuestiones relacionadas