¿Era Watcom C el doble de rápido?

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.

¿Era Watcom C el doble de rápido?

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

12 comentarios en “¿Era Watcom C el doble de rápido?”

  1. 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.

  2. Javier Gutiérrez Chamorro (Guti)

    ¡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.

  3. 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.

  4. 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.

  5. Javier Gutiérrez Chamorro (Guti)

    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)…

  6. 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

  7. 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.

  8. 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.

  9. Javier Gutiérrez Chamorro (Guti)

    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.

  10. Javier Gutiérrez Chamorro (Guti)

    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.

Deja un comentario