Con el Z80 del Spectrum, había usado las instrucciones de repetición, que con un solo nemónico, era capaces de repetir un proceso varias veces: ldir, lddr, cpdr, cpir, … No es que en aquellos años 80 llegara a ser un experto en ensamblador, pero eran cómodas, y particularmente veloces y compactas.

Así que cuando me pasé a PC, y empecé a trastear con el ensamblador del x86, me sorprendió que no hubiera instrucciones que hicieran lo mismo. Es decir, lo hacían una vez, pero no un número determinado de veces. Lógicamente, aquello fue un malentendido por mi parte, por supuesto que si había forma de hacerlo, y naturalmente de manera mucho más flexible que en el Z80. Lo único es que no eran instrucciones, sino un prefijo opcional sobre las mismas.

Me refiero a rep (Repetir hasta que CX/ECX/RCX fuera cero), repe (repetir mientras ZF=0), repne (repetir mientras ZF<>0), etc. Estos prefijos, se aplicaban al juego de instrucciones que denominaban de cadena:
– Load String: lodsb lodsw lodsd lodsq.
– Store String: stosb stosw stosd stosq.
– Move String: movsb movsw movsd movsq.
– Compare String: cmpsb cmpsw cmpsd cmpsq.
– Scan String: scasb scasw scasd scasq.

Como vemos funciones de relativo alto nivel (CISC), y que además eran tremendamente rápidas de ejecución. Pero es que además, podían operar hacia delante (DF=0) o hacia atrás (DF=1), dependiendo de lo que nos interesase por medio de cld y std, y los puedes ver funcionando en el antiguo artículo FPS en ensamblador.

Pensadas para operar con strings, en realidad su potencia estribaba en el manejo de datos, precisamente aquellas operaciones que por defecto hace un programa con mayor frecuencia. Cargar un dato de memoria, Guardar un dato en memoria, Mover de un sitio a otro de la memoria, Comparar un bloque de memoria con otro, Buscar un valor en memoria, …

En arquitecturas de 16 bits como los PC originales, podíamos manejar 2 bytes de golpe, pero donde de verdad disfruté, fue con glib, que pese a funcionar en modo real, aprovechaba las capacidades de 32 bits del 386 y posteriores, para así manejar los bytes de 4 en 4, que lo hacía prácticamente el doble de veloz. Me viene a la memoria el truco de operar sobre los primeros 1-3 bytes individualmente en el caso de no ser múltiplos de 4, y luego operar con ellos de 4 en 4, a la velocidad del rayo, reduciendo de este modo a un 25% la cantidad de accesos a memoria, y por tanto, incrementando el rendimiento.

Desgraciadamente, con el enfoque a RISC de x86, a partir del 486, estas instrucciones de cadena empezaron a perder competitividad comparado con la implementación sin ellas, y con el pipeline del Pentium, cosa que sigue vigente en los últimos Core i7 actuales, y que he explotado con bastante profusión en el viejo Lamark Speed Test, pero que obligaron a reestructurar gran parte de código y de herramientas de desarrollo, para actualizarlos a las nuevas CPU, pues lo que antaño era mejor, ahora se ejecutaba más lento.

A pesar de todo, se han ido actualizando, con las mencionadas versiones de 64 bits para x64, que operan con quad-words (lodsq, stosq, movsq, cmpsq y scasq), pero de nuevo, siguen siendo más compactas, pero más lentas.

Son unas operaciones tan habituales, que en la mayoría de lenguajes de programación y sistemas operativos, se incluyen funciones que las facilitan: memset y memcpy en C/C++; FillChar y Move en Pascal; MEMORY FILL y MEMORY COPY en PowerBasic, o FillMemory y CopyMemory en Windows.

Sin duda unas maravillas poco usadas actualmente, con el encanto del paradigma CISC, y que con MMX, SSE, y AVX vuelve a estar de moda, después del repunte RISC.