Actualizado a: 19 de enero de 2024
Hay que pensar sobre los desafíos de rendimiento que surgen en los sistemas de CPU multinúcleo utilizados en los ordenadores actuales, debido al modelo de Von Neumann, que utiliza un único pozo de memoria compartida. A medida que aumenta el número de unidades de ejecución, núcleos e hilos de ejecución en una CPU, se producen conflictos en el acceso a los datos/instrucciones y en la gestión de las direcciones de memoria y variables de los programas. Para abordar estos problemas, se puede usar lo que se denomina como memoria transaccional. Esta tecnología se utiliza para resolver los conflictos de acceso a datos e instrucciones en sistemas multinúcleo, y aquí te la vamos a explicar.
El problema de la memoria
Cuando se escribe un programa, lo que se introduce realmente es un conjunto de instrucciones que, en apariencia, se ejecutan en secuencia. Es decir, se deben ejecutar una tras otra. Sin embargo, debido al paralelismo de instrucciones, a la predicción de saltos, a los modelos OoOE (Out-of-Order Execution), etc., se puede alterar el orden de ejecución de estas instrucciones, según las dependencias de datos, la disponibilidad de las mismas, etc. Por tanto, será necesario un buffer de reordenamiento a nivel de hardware, agregando complejidad al acceso a la memoria.
Cuando hay muchas solicitudes de acceso a la memoria (de lectura o escritura), se crea una congestión en el acceso a la misma memoria, un cuello de botella que se acrecenta si tenemos en cuenta que ya la memoria de por sí trabaja más lentamente que la CPU. Esto resulta en retrasos con un impacto en el rendimiento importante, lo que aumenta la latencia entre la memoria y la CPU en ciertas instrucciones y afecta el ancho de banda RAM-CPU. Para abordar este problema, existen mecanismos que intentan minimizar los conflictos en el acceso a la memoria, permitiendo que los procesos accedan a la memoria de manera ordenada. Esto evita problemas de modificación de datos en la jerarquía de la memoria, reduce los conflictos y, como resultado, disminuye la latencia en el acceso.
La forma más sencilla de lograr esto es mediante el uso de cerrojos, que son secciones del código fuente en el programa en las que especificamos que no deben ejecutarse simultáneamente por diferentes hilos de ejecución, para que la CPU ejecute las instrucciones de otra manera. En otras palabras, solo un núcleo puede encargarse de esa parte del código a la vez, mientras que los demás núcleos quedan bloqueados hasta que se llega a una instrucción que levanta el cerrojo. Esto sucede cuando la parte del código aislada para todos los núcleos, excepto uno, ha sido completada.
¿Qué es la memoria transaccional? ¿Cómo funciona?
Para poder resolver esos problemas descritos en el apartado anterior, se puede hacer mediante lo que se conoce como memoria transaccional, o también puede aparecer como STM (Software Transactional Memory).
Es importante destacar que la memoria transaccional no es un tipo de memoria ni de almacenamiento en sí, por lo que no se trata de un componente de hardware diferente, sino que como su nombre indica, es una solución por software. Su origen se encuentra en el concepto de transacciones utilizado en el campo de las bases de datos y se implementa como un conjunto de instrucciones ejecutadas en las unidades Load-Store de una CPU.
Es decir, el funcionamiento de la memoria transaccional o STM básicamente sigue estos pasos:
- Se crea una copia de la parte de la memoria a la que varios núcleos desean acceder, generando una copia privada para cada instancia.
- Cada instancia modifica su propia copia privada de manera independiente, sin afectar a las otras copias privadas.
- Si un dato ha sido modificado en una copia privada y no en las demás, entonces la modificación se propaga a las otras copias privadas.
- Si dos instancias realizan cambios en el mismo dato simultáneamente, lo que podría crear inconsistencias en los datos, entonces se eliminan ambas copias privadas y se copian las copias privadas de las otras instancias. Este punto es crítico, ya que en este momento se hace evidente la necesidad de serializar una parte específica del código. Esto significa que las otras instancias dejan de modificar sus copias privadas y permiten que una sola instancia realice las modificaciones. Una vez que esta instancia ha completado las modificaciones, se copian los resultados a las copias privadas de las otras instancias. Cuando se ha ejecutado completamente la parte del código marcada como transaccional y todas las copias privadas contienen la misma información, entonces los resultados se copian en las líneas de caché y direcciones de memoria correspondientes.
Ventajas y desventajas
La memoria transaccional de software, o STM, tiene algunas grandes ventajas por las que merece la pena implementarla, como:
- Mejora significativa en el rendimiento.
- Simplifica la comprensión conceptual de los programas multihilo a los programadores, es decir, hace que los programas sean más fáciles. Cada transacción se puede ver de forma aislada como una ejecución de un solo hilo. El bloqueo, la livelock y otros problemas son prevenidos o gestionados por un administrador de transacciones externo, lo que hace que el programador apenas tenga que preocuparse por ellos.
Y es que la programación basada en bloqueos tiene algunos serios problemas, frente a la memoria transaccional, como:
- El bloqueo requiere pensar en operaciones superpuestas y operaciones parciales en secciones de código distantes y aparentemente no relacionadas, una tarea que es muy difícil y propensa a errores.
- Exige que los programadores adopten una política de bloqueo para prevenir el bloqueo, la livelock y otros fallos en el avance del programa. Estas políticas a menudo se aplican de manera informal y son falibles. Cuando surgen estos problemas, son insidiosamente difíciles de reproducir y depurar.
- Puede dar lugar a la inversión de prioridades, un fenómeno en el que un hilo de alta prioridad se ve obligado a esperar a un hilo de baja prioridad que tiene acceso exclusivo a un recurso que necesita.
No obstante, la memoria transaccional tampoco es perfecta. No todo son ventajas, también tiene sus desventajas, como la necesidad de abortar transacciones fallidas también impone limitaciones en el comportamiento de las transacciones: no pueden realizar ninguna operación que no pueda deshacerse, incluyendo la mayoría de las operaciones de entrada/salida (I/O). Estas limitaciones suelen superarse en la práctica mediante la creación de buffers que almacenan las operaciones irreversibles y las posponen para ejecutarse en un momento posterior, fuera de cualquier transacción. En algunos casos esto se lleva a cabo en tiempo de ejecución, en otras se implementa mediante hardware, etc.
Implementaciones de memoria transaccional
Como he comentado anteriormente, no se trata de una solución de hardware en sí, sino que es algo de software. Concretamente, los diseñadores de CPUs lo que hacen es agregar unas instrucciones adicionales a su ISA y los programadores podrán usarlas a la hora de compilar su software. Por ejemplo, dos casos de tecnologías para memoria transaccional o implementaciones son:
Intel TSX (Transactional Synchronization eXtension)
La tecnología Intel TSX se refiere a un conjunto de instrucciones adicionales diseñadas para extender la ISA x86-64 y proporcionar soporte de memoria transaccional en las CPU de Intel. Estas instrucciones y sus mecanismos asociados permiten identificar secciones específicas del código como transaccionales, lo que permite a las CPU de Intel llevar a cabo el proceso descrito anteriormente. Sin embargo, la implementación de Intel en este aspecto es algo más compleja. Como se mencionó anteriormente, si se produce un conflicto entre dos datos, una de las instancias en ejecución abortará todo el proceso.
Hay que decir, que en la actualidad, las unidades de control de las CPU se basan en microcódigo, como he mencionado, lo que significa que la forma en que decodifican las instrucciones y la lista de estas instrucciones puede actualizarse mediante el firmware. De vez en cuando, Intel realiza actualizaciones remotas de sus CPU a través del Intel Management Engine (ME), que permite a Intel gestión remota de un PC sin que el usuario lo sepa, de forma transparente, otro foco de problemas de seguridad, si has estado al día de estos casos… Estas actualizaciones no son muy comunes, pero pueden incluir optimizaciones para la ejecución de ciertas instrucciones o incluso la eliminación del soporte para otras.
TSX/TSX-NI fueron implementadas por Intel en 2012, siendo la primer microarquitectura en usarlas la Haswell. Sin embargo, la alegría les duró poco, ya que dos años más tarde Intel anunciaría un bug relacionado con esta implementación, afectando tanto a las distintas versiones de Haswell como a las primeras Broadwell, por lo que tuvieron que actualizar el microcódigo para solucionarlo. Y ese no fue el único problema, más tarde, en 2016, también se encontró otra vulnerabilidad que podía explotarse mediante side-channel timing attack, rompiendo así el KASLR en el kernel de muchos sistemas operativos. En 2021, Intel finalmente lanzaría otra actualización del microcódigo para deshabilitar TSX/TSX-NI en la generación Skylake a Coffe Lake por los serios problemas de seguridad. No obstante, han vuelto a aparecer en la actualidad…
Esta implementación no implica modificar demasiado el hardware, ya que simplemente sería hacer que la unidad de decodificación las sepa interpretar. No obstante, también implica la incorporación de una nueva forma de caché llamada caché transaccional, donde se realizan diversas operaciones en los datos. La memoria transaccional tiene como objetivo reducir los conflictos de acceso a la memoria. A pesar de que las cachés pueden manejar un mayor número de solicitudes en comparación con la RAM en general, también tienen limitaciones, especialmente en los niveles más alejados de los núcleos. Además, se complementa con el uso de memorias internas y registros privados que sirven como soporte para las copias privadas realizadas por los distintos núcleos.
Las instrucciones de Intel TSX no son particularmente complejas. Incluyen la instrucción XBEGIN para indicar el inicio de una sección transaccional de memoria, la instrucción XEND para marcar su final y la XABORT, que se utiliza para salir de la transacción en caso de situaciones excepcionales.
AMD ASF (Advanced Synchronization Facility)
Advanced Synchronization Facility (ASF) es una extensión propuesta para la arquitectura de conjunto de instrucciones x86-64 que agrega soporte de memoria transaccional a nivel de hardware. Fue introducida por AMD, y la última especificación data de marzo de 2009. No obstante, es una proposición que ha estado latente, sin implementarse en los procesadores, quizás conocedores de los problemas que ha tenido Intel.
Sin embargo, ASF sí ha sido documentada, y sabemos que proporciona la capacidad de iniciar, finalizar y abortar la ejecución transaccional, así como marcar las líneas de caché de la CPU para el acceso protegido a la memoria en regiones de código transaccional.
Para que esto sea posible, se han creado cuatro nuevas instrucciones: SPECULATE, COMMIT, ABORT y RELEASE. Además, convierte las instrucciones MOVx, PREFETCH y PREFETCHW con el prefijo LOCK, que normalmente serían inválidas, en válidas dentro de las regiones de código transaccional. Se admite un máximo de 256 niveles de regiones de código transaccional anidadas.
Las instrucciones son bastante simples de entender, como las de Intel TSX. Por ejemplo, tenemos SPECULATE y COMMIT que marcarán el inicio y el final de una región de código transaccional respectivamente. Luego, la cancelación de transacción generada por hardware o solicitada explícitamente se harían mediante ABORT, deshaciendo las modificaciones en las líneas de la memoria cache protegidas y reiniciando la ejecución desde la siguiente instrucción a la instrucción SPECULATE…