Programar a bajo nivel, quizás es una de esas cosas que aprendes en la carrera y que nunca vas a necesitar. Sin embargo, es muy curioso como algunos lenguajes de alto nivel, según van evolucionando, van añadiendo más características de bajo nivel.
Tabla de contenidos
Para mí, programar a bajo nivel es programar lo más cerca posible al uso directo de la memoria del ordenador y al juego de instrucciones básicas del procesador. Esto evita usar abstracciones como clases, operadores propios del lenguaje, etc. acercándote lo máximo posible a programar en ensamblador.
En el día a día, seguro que el 99% de los desarrolladores no necesitamos programar a bajo nivel. Pero que no forme parte de nuestro día a día, no quiere decir que todavía no sea muy necesario en algunas áreas:
Teniendo claro los conceptos básicos anteriores, podemos hacer un pequeño repaso a algunas de las cosas que ya tenía C# antes de .NET Core 3 para programar a bajo nivel:
Sin embargo, .NET Core 3 va a traer bajo el namespace System.Runtime.Intrinsics acceso al juego de instrucciones tanto de la arquitectura x86 como de ARM y sus sucesivas ampliaciones SSE2, SSE3…
Para entender el ejemplo que voy a hacer utilizando la nueva API de C# que nos permite realizar llamadas a instrucciones concretas de nuestra CPU, primero creo interesante abordar algunos conceptos básicos.
Los ordenadores de sobremesa y portátiles utilizan hoy en día procesadores Intel o AMD, ambos utilizan las arquitecturas x86 y AMD64, para 32bits y 64bits respectivamente. En el caso de los móviles, lo más común es que usen arquitecturas ARM o ARM64, para 32bits y 64bits respectivamente. Por simplicidad me voy a centrar en x86.
La arquitectura x86 sigue un diseño CISC (Complex Instruction Set Computing), lo que implica que tienen muchas instrucciones para hacer operaciones complejas de manera óptima, que están diseñadas a nivel de hardware no por software.
Dentro de la arquitectura x86, un conjunto de instrucciones muy importante para agilizar en gran medida los cálculos son las SIMD (Single Instruction Multiple Data). Que en pocas palabras, permiten operar con una misma instrucción sobre múltiples datos al mismo tiempo.
Para profundizar en estos conceptos, puede visitar la siguiente explicación preparada por el Dr. Nicolás Wolovick de la Universidad Nacional de Córdoba, Argentina.
La instrucción de bajo nivel que voy a utilizar es PHMINPOSUW, una instrucción que partiendo recibe un vector de 128bits formado por 8 elementos de tipo unsinged short, calcula el menor de los mismos. Para ello, según la documentación de Intel se utiliza el siguiente pseudocódigo.
index[2:0] := 0 min[15:0] := a[15:0] FOR j := 0 to 7 i := j*16 IF a[i+15:i] < min[15:0] index[2:0] := j min[15:0] := a[i+15:i] FI ENDFOR dst[15:0] := min[15:0] dst[18:16] := index[2:0] dst[127:19] := 0
Aunque se ve más claro, utilizando la siguiente representación gráfica:
Fuente: Officedaytime
Con el siguiente código, podríamos computar el mínimo de 8 elementos unsigned short vía la instrucción PHMINPOSUW, desde C#:
using System; using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics; ... public ushort MinSIMD(ushort us0, ushort us1, ushort us2, ushort us3, ushort us4,ushort us5, ushort us6, ushort us7){ var values = Vector128.Create(us0,us1,us2,us3,us4,us5,us6,us7); return Sse41.MinHorizontal(values).GetElement(0); }
Ahora desgranemos cada una de las líneas:
Ahora os preguntaréis, ¿cómo sabes que llamar a Sse41.MinHorizontal utiliza la instrucción PHMINPOSUW? A lo que os respondo, que es sencillo porque así lo indica la documentación:
// // Summary: // __m128i _mm_minpos_epu16 (__m128i a) PHMINPOSUW xmm, xmm/m128 public static Vector128<ushort> MinHorizontal(Vector128<ushort> value);
Desde el inicio del artículo, todo lo escrito defendía la tesis de que programar a bajo nivel nos da acceso a algunas características del procesador y de la memoria del ordenador, que permiten agilizar los cálculos, es hora de demostrarlo.
Lo primero que se necesita es ver cómo vamos a medir el rendimiento, para ello en C# la opción más sencilla es usar BenchmarkDotNet. Además, es la misma librería que utiliza Microsoft para medir el desempeño de .NET Core y las mejoras que van introduciendo.
Ahora solo nos falta ver contra qué lo vamos a comparar, para ello he creado dos métodos que buscan el mínimo de 8ushorts, uno mediante LINQ y otro iterando sobre el propio array de manera manual:
public ushort MinLINQ(ushort us0, ushort us1, ushort us2, ushort us3, ushort us4,ushort us5, ushort us6, ushort us7){ return new ushort[8]{us0,us1,us2,us3,us4,us5,us6,us7}.Min(); } public ushort MinLoop(ushort us0, ushort us1, ushort us2, ushort us3, ushort us4,ushort us5, ushort us6, ushort us7){ ushort min = us0; foreach(var value in new ushort[7]{us1,us2,us3,us4,us5,us6,us7}){ if(value<min) min=value; } return min; }
Como ya vimos en el artículo, 3 maneras de optimizar tus programas en C#, la implementación vía LINQ es siempre más lenta, y el uso de arrays, es más óptimo que otras estructuras de datos para este fin.
Los resultados de los tiempos de ejecución de los 3 métodos son los siguientes:
Método | Media (ns) | Error (ns) | Desviación estándar (ns) |
---|---|---|---|
FindWithMinSIMD | 3.696 | 0.0225 | 0.0200 |
FindWithLINQ | 182.543 | 3.5925 | 4.2767 |
FindWithLoop | 29.490 | 0.1920 | 0.1796 |
Con los resultados anteriores, se puede afirmar que el uso de esta instrucción mejora en casi 10 veces, la implementación mediante bucle y es entorno a 60 veces más rápida que la realizada con LINQ. El ejemplo completo se puede encontrar en mi GitHub
Espero que les haya gustado el artículo, si tienen algún comentario, no se olviden de dejarlo.
En el siguiente recurso, en inglés, puedes ver una amplicación de una optimización similar para el problema de la ordenación.
En los últimos tiempos no he podido escribir con toda la frecuencia que me gustaría,…
Uno de los problemas más comunes a los que se enfrentan los usuarios que empiezan…
Cuando empiezas un proyecto hay una serie de aspectos comunes que suelen ser resueltos mediante…
Si alguna vez has tenido que realizar un desarrollo de front-end seguramente te habrás dado…
Una vez que una persona ya ha aprendido lo básico sobre un lenguaje de programación,…
Hoy en día, un gran porcentaje de los proyectos que se desarrollan son páginas webs.…