MemoryBlocks para la máxima velocidad: Un Caso Real

A continuación encontrarás traducido al castellano el artículo escrito por Kem Tekinay y publicado originalmente en el Blog oficial de Xojo.

Hace unos años realicé una presentación sobre los MemoryBlock en la Xojo Developer Conference donde mostraba como, en general, los MemoryBlock (y Ptr) deberían de ser evitados excepto en los casos en los que debas utilizarlos realmente; como por ejemplo pueda ser en combinación con Declare, o cuando la velocidad es absolutamente crítica. Ofrecí este consejo porque los MemoryBlock pueden resultar algo tedioso a la hora de utilizarlos, y también pueden derivar en bugs difíciles de identificar.

Pero cuando necesitas ese incremento de velocidad, entonces son una opción a considerar; y recientemente me he encontrado con un escenario donde marcaron una diferencia tremenda.

El Reto del Millón de Filas

Esto ocurrió en la discusión sobre el “Reto del Millón de Filas” en el Foro de Xojo, donde un programador se enfrenta a la tarea de leer un millón de filas a partir de datos de temperatura y consolidarlas en estadísticas para cada una de las ciudades. Nuestro amigo Mike D comenzó un proyecto para mostrar diferentes técnicas, y eventualmente comencé con mi propio proyecto. Mediante el uso de los MemoryBlock, Ptr, y los hilos preemptivos fui capaz de procesar un millón de filas en, aproximadamente, unos 8 segundos.

Pero ese no es el objetivo de este artículo.

Veamos, para procesar los datos, has de crearlos en primer lugar… lo cual no es tan sencillo como pueda parecer a primera vista.

Crear los Datos

Cada fila en el archivo de datos tiene el formato de “Ciudad;temperatura”, donde “temperatura” es un número decimal que puede oscilar entre -99.9 y 99.9. Para el propósito de dichos límites utilicé de forma arbitraria un total de 413 ciudades aleatorias a partir de una lista que contenía todas las ciudades. El código original tenía aproximadamente el siguiente aspecto, donde “bs” se corresponde con un BinaryStream:

Var r As New Random
 
For i As Integer = 1 To rowCount
  Var city As String = cities(r.InRange(0, cities.LastIndex))
  Var temp As Double = r.InRange(-999, 999) / 10.0
 
  bs.Write city + ";" + temp.ToString("#0.0") + EndOfLine
Next

Este es simple, fácil… y lento. Generar la lista completa con un millón de filas llevaba 2,5 horas.

Liberando Memoria

Tras un poco de investigación volví a descubrir lo que ya sabía. Trabajar con cadenas, tanto en su conversión como en su concatenación, es un cuello de botella. No quiero pasar por todas las iteraciones aquí, pero nada de lo que intenté marcó una diferencia considerable. La única solución fue eliminar las cadenas por completo.

Comencé creando un MemoryBlock a modo de “buffer” (“outMB”) de 1 MB con un Ptr asociado (“outPtr”). (Puedes acceder a los contenidos del MemoryBlock a través de sus métodos, pero estos son llamadas a funciones, lo que implica un sobrecoste en el rendimiento. Los métodos de Ptr son operadores que funcionan directamente sobre los bytes, de modo que son más rápidos.) El plan consistía en rellenar el buffer tanto como fuese posible, escribirlo sobre el archivo, y comenzar de nuevo sobre la parte superior del buffer.

Manteniendo un índice de posición, comencé escribiendo la ciudad utilizando outMBS.StringValue dado que no hay un método equivalente en Ptr para ello. Luego, añadí el valor de un punto y coma con outPtr.Byte(outMBIndex) = 59.

Trabajar con enteros es más rápido que el hacerlo con valores de coma flotante, de modo que utilicé un poco de matemáticas para añadir los valores de temperatura directamente mediante el uso de instrucciones If y outPtr.Byte.

Por último, utilicé outPtr.Byte(outMBIndex) = 10 para añadir el retorno de línea (ASCII 10).

El código final mostraba, más o menos, el siguiente aspecto:

Const kEOL As Integer = 10
Const kHyphen As Integer = 45
Const kDot As Integer = 46
Const kZero As Integer = 48
Const kSemicolon As Integer = 59
 
Var r As New Random
 
Var outMB As New MemoryBlock(1000000)
Var outPtr As Ptr = outMB
 
Var outMBIndex As Integer = 0
 
For row As Integer = 1 To rows
  Var cityIndex As Integer = r.InRange(0, cities.LastIndex)
  Var city As string = cities(cityIndex)
  Var cityBytes As Integer = city.Bytes
 
  If (outMBIndex + cityBytes + 10) >= outMB.Size Then
    bs.Write outMB.StringValue(0, outMBIndex)
    outMBIndex = 0
  End If
 
  outMB.StringValue(outMBIndex, cityBytes) = city
  outMBIndex = outMBIndex + cityBytes
 
  outPtr.Byte(outMBIndex) = kSemicolon
  outMBIndex = outMBIndex + 1
 
  If r.InRange(0, 4) = 0 Then
    outPtr.Byte(outMBIndex) = kHyphen
    outMBIndex = outMBIndex + 1
  End If
 
  Var temp As Integer = r.InRange(0, 999)
  Var t1 As Integer = temp \ 100
  Var t2 As Integer = (temp \ 10) Mod 10
  Var t3 As Integer = temp Mod 10
 
  If t1 <> 0 Then
    outPtr.Byte(outMBIndex) = t1 + kZero
    outMBIndex = outMBIndex + 1
  End If
 
  outPtr.Byte(outMBIndex) = t2 + kZero
  outMBIndex = outMBIndex + 1
 
  outPtr.Byte(outMBIndex) = kDot
  outMBIndex = outMBIndex + 1
 
  outPtr.Byte(outMBIndex) = t3 + kZero
  outMBIndex = outMBIndex + 1
 
  outPtr.Byte(outMBIndex) = kEOL
  outMBIndex = outMBIndex + 1
next
 
If outMBIndex <> 0 Then
  bs.Write outMB.StringValue(0, outMBIndex)
End If

Este código es mucho más largo, difícil de seguir, y también difícil de mantener, lo que nos lleva a la observación original sobre por qué los MemoryBlock deberían de ser evitados.

También genera un millón de filas de datos en torno a un minuto (en comparación con las 2,5 horas).

En cualquier caso, está bien disponer de dicha opción.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *