Cuando publiqué Watcom C++ y Sieve para DOS pareció que el rendimiento del compilador Watcom C/C++ aún siendo muy bueno, no llegaba a cumplir aquella promesa del pasado en la que se decía que Watcom C++ con aplicaciones de 32 bits generaba un código que era entre 2 y 5 veces más rápido que cualquier otro compilador de C de 16 bits. Era una leyenda urbana, una exageración de la que los iniciadores eran conscientes.
Así que después de Modula-2 (Fitted y TopSpeed), voy a intentar dar respuesta a la pregunta: ¿Era Watcom el doble de rápido que otros compiladores de C?
Para ello el «backportado» mi código de cálculo del CRC32 en FileOptimizer, una rutina que en los tiempos de DOS era intensiva en cuanto a cálculos, y que además ofrece una brillante implementación con tablas precalculadas para evitar los cálculos polinómicos. He pensado que siendo un CRC de 32 bits, y usando operaciones de 32 bits, la mejoría con un compilador también de 32 bits sería notable.
Sin más este es el código. Un sencillo programa que hacer la inútil tarea de calcular el CRC sobre un bloque de memoria vacío de 16 K repitiéndolo 100 veces. Es decir, un bloque de memoria total de algo más de 1,5 MB.
// -------------------------------------------------------------------------------------------------- #include#include #include #include // -------------------------------------------------------------------------------------------------- #define BUFFER_SIZE 16384 // -------------------------------------------------------------------------------------------------- unsigned long crc32 (const void *pacBuffer, unsigned int piLen, unsigned long plOldCrc) { unsigned int iCont; unsigned long lCrc; const unsigned long alTable[] = { }; lCrc = plOldCrc; for (iCont = 0; iCont < piLen; iCont++) { unsigned char cByte = (unsigned char) lCrc ^ ((const unsigned char *) pacBuffer)[iCont]; lCrc = (lCrc >> 8) ^ alTable[cByte]; } return (lCrc ^ 0xFFFFFFFF); } // -------------------------------------------------------------------------------------------------- int main (void) { unsigned int iCont; unsigned long lCrc32 = 0xFFFFFFFF; void *pvBuffer; clock_t start, end; pvBuffer = malloc(BUFFER_SIZE); memset(pvBuffer, 0, BUFFER_SIZE); if (pvBuffer) { start = clock(); for (iCont = 0; iCont < 100; iCont++) { lCrc32 = crc32(pvBuffer, BUFFER_SIZE, lCrc32); } end = clock(); printf("%lX CRC32 %d iterations in %ld ms.", lCrc32, iCont, (long) (end-start) * 1000 / (int) CLOCKS_PER_SEC); free(pvBuffer); } return((int) lCrc32); }
Antes de seguir, os dejo en 40 KB. en formato ZIP todo el código fuente y sus ejecutables. Para las pruebas he usado Borland C++ 3.1, una herramienta de desarrollo que conozco bien y que toqué durante muchos años, y OpenWatcom C/C++ 2.0.
Borland C++ 3.1 (16 bit) | Openwatcom C++ 2.0 (16 bit) | Openwatcom C++ 2.0 (32 bit) | |
Tiempo de ejecución (ms) | 9.333 | 5.712 | 2.417 |
Tamaño del ejecutable (bytes) | 8.460 | 10.300 | 49.580 |
Vemos que se cumple la afirmación de marketing sobre Watcom, y es que el resultado es 4 veces más veloz que con el de Borland. Hemos pasado de más de 9 segundos de ejecución a sólo 2. Me ha parecido tan sorprendente, que conociendo además la mala calidad del código generado por BC he decidido también compilar una versiónd de 16 bits con Watcom. Nueva sorpresa, porque con Watcom ha sido casi el doble de rápida que Borland.
En esta situación, Watcom gana de goleada, sin embargo si comparamos Watcom de 16 bits contra Watcom de 32 bits, la mejoría se acerca al doble, pero no llega. Es decir, Watcom C no era el doble de rápido que cualquier otro complicador de C de 16 bits, al menos si éste era de calidad como el de Watcom, y me temo que con GCC, que ahora permite generar también aplicaciones para DOS de 16 bits tampoco lo sería. Inevitablemente, el poder manejar números de 32 bits (long y unsigned long) de manera nativa es una interesante capacidad.
En cuanto al tamaño del ejecutable, todos ellos usando las optimizaciones para velocidad y con el modelo de memoria tiny para 16 bits nos da que el de Watcom es ligeramente mayor, y en 32 bits todavía más debido a tener que cargar con el extensor DOS, aunque sea ligero como aquí que he elegido a PMODE/W.
BCC.EXE
Comencemos analizando a Borland C++ 3.1 con su BCC.EXE 3.1 que me ha parecido tremendamente lento. Este es el código que ha generado.
_crc32 proc near push bp mov bp,sp sub sp,1030 push si push di ; ; { ; unsigned int iCont; ; unsigned long lCrc; ; const unsigned long alTable[] = ; { ; }; ; lea ax,word ptr [bp-1030] push ss push ax mov ax,offset DGROUP:d@w+0 push ds push ax mov cx,1024 call near ptr N_SCOPY@ ; ; ; lCrc = plOldCrc; ; mov ax,word ptr [bp+10] mov dx,word ptr [bp+8] mov word ptr [bp-2],ax mov word ptr [bp-4],dx ; ; for (iCont = 0; iCont < piLen; iCont++) ; xor di,di mov si,word ptr [bp+4] cmp di,word ptr [bp+6] jae short @1@226 @1@114: ; ; { ; unsigned char cByte = (unsigned char) lCrc ^ ((const unsigned char *) pacBuffer)[iCont]; ; mov al,byte ptr [bp-4] xor al,byte ptr [si] mov byte ptr [bp-5],al ; ; lCrc = (lCrc >> 8) ^ alTable[cByte]; ; mov dx,word ptr [bp-2] mov ax,word ptr [bp-4] mov cl,8 call near ptr N_LXURSH@ mov bl,byte ptr [bp-5] mov bh,0 mov cl,2 shl bx,cl lea cx,word ptr [bp-1030] add bx,cx xor ax,word ptr [bx] xor dx,word ptr [bx+2] mov word ptr [bp-2],dx mov word ptr [bp-4],ax inc si inc di cmp di,word ptr [bp+6] jb short @1@114 @1@226: ; ; } ; return (lCrc ^ 0xFFFFFFFF); ; mov dx,word ptr [bp-2] mov ax,word ptr [bp-4] xor ax,-1 xor dx,-1 ; ; } ; pop di pop si mov sp,bp pop bp ret _crc32 endp
A simple vista chirría el fragmento:
mov bl,byte ptr [bp-5] mov bh,0 mov cl,2 shl bx,cl
Apenas 4 instrucciones que van a necesitar de 36 ciclos de ejecución, y que yo habría escrito como:
mov bl,byte ptr [bp-5] xor bh,bh shl bx,1 shl bx,1
Resultante en sólo 19 ciclos de tiempo de ejecución, el doble de rápido que el que generó Borland, y que explica porque Watcom que es más listo, ha conseguido hacerlo de forma parecida.
No entraré mucho más en detalles porque queda evidenciado que el código es nefastamente malo. Para salir de dudas lo he generado también con Borland C++ 5.02, el último disponible (BCC 5.2) y el rendimiento ha sido parecido con un código casi idéntico, en cambio el ejecutable creció hasta los 13.896 bytes.
WCC386.EXE
A simple vista vemos que el código es mucho más corto, usa registros de 32 bits para realizar las operaciones, e incluso optimiza algunos procesos usando rep movs. ¿Está claro el porqué es más rápido no?
crc32_: push ecx push esi push edi push ebp sub esp,0x00000400 mov ebp,edx mov edx,ebx mov ecx,0x00000100 mov edi,esp mov esi,offset L$1 xor ebx,ebx rep movsd test ebp,ebp jbe L$6 L$5: mov cl,[eax] xor cl,dl inc eax movzx ecx,cl shr edx,0x08 inc ebx xor edx,[esp+ecx*4] cmp ebx,ebp jb L$5 L$6: mov eax,edx not eax add esp,0x00000400 pop ebp pop edi pop esi pop ecx ret
WCC.EXE
Aquí vemos el correspondiente para Watcom de 16 bits. El galimatías de realizar operaciones de 32 bits usando sólo 16 sigue presente, aunque vemos cosas que están muy bien. Hay un rep movsw de 16 bits para copiar el contenido rápidamente. Fijaros también que directamente Watcom general el código xor ah, ah para poner un registro a cero.
crc32_: push si push di push bp mov bp,sp sub sp,0x0402 push ax push dx mov dx,cx mov cx,0x0200 mov ax,ds lea di,-0x402[bp] mov es,ax mov si,offset DGROUP:L$2 rep movsw mov -0x2[bp],bx xor di,di cmp word ptr -0x406[bp],0x0000 jbe L$6 mov bx,-0x404[bp] L$5: mov al,-0x2[bp] xor al,[bx] xor ah,ah mov si,ax inc bx shl si,0x01 mov ax,-0x2[bp] shl si,0x01 mov cl,0x08 shr ax,cl ror dx,cl xor ax,dx and dx,0x00ff xor ax,dx inc di mov cx,-0x402[bp+si] xor dx,-0x400[bp+si] xor cx,ax mov -0x2[bp],cx cmp di,-0x406[bp] jb L$5 L$6: mov ax,-0x2[bp] not dx not ax mov sp,bp pop bp pop di pop si ret
Recuerdo que se comentaba que poner un registro a cero con el xor estaba patentado y no se podía hacer.. aunque supongo que nadie hacía caso de tal absurdez.
¡Es verdad Fernando! Yo también escuché algo así. Lo piensas ahora y es un absurdo. ¿Patentar una instrucción? ¿Y entonces porque la implementaban las CPU? Es curioso como en esa época donde mucha de la información te venía por el boca a boca había cantidad de bulos.
No es que no pudieras usar el xor, es que lo que «estaba patentado» era su uso particular para poner un registro a cero. Para comprender esto hay que tener en cuenta que en aquellos tiempos las CPU no tenían paralelización y poner un registro a cero, operación muy común, requería varios ciclos de reloj, muy caros con aquellas CPU «tan lentas». Hacer un «xor ax, ax» ponía el registro ax a cero en un solo ciclo de reloj. De ahí la importancia del método y la significación de «la patente».
Sin embargo yo no terminé de creérmelo del todo, aunque di por buena la historia. Es de esas historias que es más beneficioso creérselas, y me explico: era el ejemplo perfecto de porqué en el mundo de la programación es tan perverso el sistema de patentes del mundo industrial de los artículos tangibles. La lucha contra las patentes de software ya había comenzado en aquellos años.
Además hay un motivo por el cual ahora me parece claro que era más un ejemplo contra las patentes de software que algo real. Si yo hubiera sido un fabricante de CPU CISC habría inventado una nueva instrucción para poner a cero un registro, llamémosla «clear», que en microcódigo fuera exactamente un xor. Listo, a los programadores de mi plataforma ya les doy una forma igual de rápida para poner a cero un registro que «el patentado» xor.
En mi caso ese tipo de microoptimizaciones me salían de manera natural al escribir código Fernando. El Xor para poner a cero, el Test para comprobar si valía cero… No sólo eran más veloces, sino también más compactas. De hecho con la llegada de los 486 y Pentium, inclusive paralelizaban mejor y seguían siendo más rápida.
Totalmente, en filosofía CISC el concepto es especializar. Yo habría agregado poner a -1 (sbb ax, ax)…
Pues el que lo patentó fue un tonto. Me explico: patentas el MOV que es mucho mejor, y lo fundes XD O ya puestos a patentar, patentas todo el ensamblador y que se inventen otro. No creo que nadie se creyera eso, es de estúpidos, es como si hoy alguien te dice que ha patentado un while.
Aunque bueno, si Casio patentó F-91W, por qué no iban a patentar un XOR
Berta Snider precisamente porque la historia decía que se había patentado el uso de xor para poner a cero un registro era creíble. El motivo es que «xor ax,ax» es una optimización, no se patentó la instrucción sino un uso muy concreto que evitaba hacerlo de la manera «correcta» (sintácticamente hablando) con un «mov ax,0».
Nadie intentó patentar el ensamblador, hubiera sido un arma de doble filo. Por definición el ensamblador es propio de cada CPU, sin embargo los nombres de las instrucciones muchas veces eran los mismos o muy similares y esto se hacía así por motivos obvios: los programadores podían fácilmente programar con varios lenguajes ensamblador sin problemas. Yo copiaba en mi ensamblador las instrucciones de la competencia, por eso no intentaba patentar y la competencia no me denunciaba o patentaba su ensamblador porque también se copiaba de mi ensamblador cuando yo introducía nuevas instrucciones.
En electrónica digital para ahorrar puertas muchas veces se utilizaban xor, nand, nor como negadores o inversores y nadie patentó estos diseños básicos (al menos no corrió ninguna historia al respecto).
Como digo la historia sobre el «xor ax,ax» tiene su lógica como lucha contra las patentes de software, no como algo real. Yo recuerdo pensar «que les den, que me denuncien» (cosas de la adolescencia). Y aún así, siendo una optimización tan obvia y evidente resulta muy curioso como unos compiladores la hacían y otros no. Esto, y no otra cosa, hacía pensar si de verdad había algo más que una historia curiosa sobre los peligros de las patentes en el mundo del software.
No me acordaba de la optimización hardware, teniendo en cuenta que durante mis prácticas de lógica digital tuve que hacerlas porque tenía a mi disposición un muy limitado surtido de chips.
La verdad que si eso hubiera sido patentable habría sido un negociazo Berta Snider. Ahora en serio, recuerdo cuando estas cosas no estaban tan claras, había dudas de hasta que punto tu podías compilar un programa con una herramienta de desarrollo y venderlo. Al fin y al cabo tu ejecutable tenía código runtime propiedad de la empresa del compilador. Muchos anuncios te decían «royalty free» para aclararlo. Si no estoy equivocado, había algunos que sí te cobraban.
Ya decía que es una optimización que a mi me encantaba Fernando. Personalmente tenía dos formas de valorar cómo de bueno era el código de un compilador: Cuando siempre que se podía usaba xor reg, reg para poner a cero (algo que sólo vi en Watcom MSC), y cuando usaban rep lods/stos/movs para acceder a bloques de memoria.
No es mi fuerte, pero estoy aprendiendo, Guti, Fernando, Berta, les mando un abrazo desde México.
Tranquilo RICARDO, es sólo una forma de variar. Gracias igualmente.
Los mejores compiladores fueron los de ms visual c y masm, así programamos los hombres de verdad
Bienvenido Panzer. Yo era más de TASM, pero reconozco que MASM era superior y se mantuvo más tiempo. En cuanto a Visual C, discrepo, hasta versiones muy recientes, entorno a la 5, Watcom le superaba.
Puedes probar el editor del Microsoft c comoiler 7 para dosmy ver si te permite introducir las llaves de las funciones?
Lo usé en su día lord Soth y ningún problema con las llaves. Entiendo que a ti no te deja meter { ni }, pero no das muchos detalles.
Creo que es un problema de pcem y ciertas bios ! Gracias !
Bienvenido Lord Soth. Es poco probable que tenga que ver con las BIOS puesto que el código simplemente hace cálculos sobre un bloque de memoria sin utilizar ningún servicio BIOS.