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.