Está en la página 1de 8

De Vuelta a las bases

Por Joel Spolsky


Traducido por Jos Manuel Navarro
Editado por Jerry Elizondo
14/04/2003
En la web dedicamos mucho tiempo a hablar sobre temas grandiosos como ".NET vs. Java", la
estrategia del XML, bloqueos, estrategia competitiva, diseo de software, arquitectura, y as
sucesivamente.
Todos estos temas son, de alguna manera, son como un pastel hecho de capas. En la capa
superior, tenemos la estrategia del software. Por debajo de esto, reflexionamos sobre
arquitecturas como .NET, y por debajo, estn los productos individuales: productos de desarrollo
de software como Java o plataformas como Windows.
Vayamos ms abajo en el pastel, por favor. DLLs? Objetos? Funciones? No! Ms abajo! En
algn momento estars pensando en lneas de cdigo escritas en lenguajes de programacin.
An no bajaste lo suficiente. Hoy quiero reflexionar sobre las CPUs: un pequeo pedazo de
silicio moviendo bytes a su alrededor. Finge que eres un programador principiante. Olvdate de
todo el conocimiento que has adquirido sobre programacin, software, gestin, y regresa al nivel
ms bajo de los temas fundamentales de Von Neumann. Saca al J2EE de tu cabeza por un
momento. Piensa en los bytes.
Por qu estamos haciendo esto? Creo que muchos de los
mayores errores que la gente comete incluso en los niveles
ms altos de la arquitectura, vienen de tener un conocimiento
muy dbil o nulo de unas pocas cosas sencillas, en los niveles
ms bajos. Hemos construido un maravilloso palacio, pero los
cimientos son un desastre. En vez de una buena base de
cemento, tienes escombros ah abajo. As que el palacio
parece bueno, pero a veces la baera se desliza por el suelo
del cuarto de bao y no tienes ni idea de lo que est pasando.
As que hoy, tmate un buen respiro. Camina conmigo, por favor, a travs de un pequeo
ejercicio, que guiar usando el lenguaje de programacin C.
Recuerda el modo en que trabajan las cadenas en C: consisten en un manojo de bytes seguidos
por un carcter nulo, que tiene el valor 0. Esto tiene dos implicaciones obvias:
1. No hay ningn modo de saber dnde termina la cadena (es decir, su longitud) sin
moverse a travs de ella, buscando el carcter nulo del final.
2. Tus cadenas no pueden contener ceros. As que no podrs almacenar cualquier valor
binario, como una imagen JPEG, en una cadena de C.
Por qu las cadenas de C trabajan de este modo? Esto es debido a que el microprocesador
PDP-7, en el que se inventaron el sistema operativo UNIX y el lenguaje de programacin C, tiene
un tipo de dato llamado ASICZ. ASICZ significa ASCII con un Cero al final.
Es este el nico modo de almacenar cadenas? No, de hecho, es uno de los peores mtodos de
almacenar cadenas. Para programas no-triviales, APIs, sistemas operativos, libreras de clases,
etc., debes evitar el uso de cadenas ASICZ como una plaga. Por qu?
Comencemos escribiendo una versin del cdigo de strcat, la funcin que aade una cadena a
otra.
void strcat( char* dest, char* src )
{
while (*dest) dest++;
while (*dest++ = *src++);
}
Estudia el cdigo un poco y observa qu es lo que estamos haciendo. Para empezar, recorremos
la primera cadena buscando su carcter terminador nulo. Cuando lo encontramos, recorremos la
segunda cadena copiando un carcter a la segunda cadena cada vez.
Este tipo de manipulacin y concatenacin de cadenas fue suficientemente bueno para
Kernighan y Ritchie, pero esto tiene sus problemas. Aqu est el problema. Supn que tienes un
manojo de nombres que quieres concatenar juntos en una gran cadena.
char bigString[1000]; /* Nunca s cuanto tengo que reservar... */
bigString[0] = '\0';
strcat(bigString,"John, ");
strcat(bigString,"Paul, ");
strcat(bigString,"George, ");
strcat(bigString,"Joel ");
Esto funciona verdad? S. Y parece correcto y elegante.
Y cmo va de rendimiento? Es tan rpido
como podra llegar a ser? Se puede ampliar
bien? Si tenemos un milln de cadenas que
concatenar, sera un buen modo de hacerlo?
No. Este cdigo usa el algoritmo de "Shlemiel
el Pintor". Quin es Shlemiel? Pues el chaval
de este chiste:
Shlemiel consigui un trabajo como pintor de
calles, pintando la lnea discontinua de las
carreteras. El primer da cogi su cubo de
pintura y acab 300 yardas de carretera. "Eso
est realmente bien!" le dijo su jefe. "Eres un trabajador muy rpido" y le dio una moneda.
El da siguiente, slo consigui hacer 150 yardas. "Bueno, no ha estado tan bien como ayer pero
todava eres un trabajador rpido. 150 yardas es una cantidad muy respetable". Y le da una
pequea moneda.
Al da siguiente, Shlemiel complet 30 yardas de carretera. "Slo 30 yardas!" le grit su jefe.
"Esto es inaceptable!. El primer da hiciste 10 veces ms distancia Qu est pasando aqu?"
"No puedo hacerlo mejor", dijo Shlemiel, "cada da estoy ms y ms lejos del bote de pintura."
Este chiste malo ilustra exactamente lo que ocurre cuando usas la funcin strcat tal y como yo lo
hice. Mientras que la primera parte del strcat tiene que escanear la cadena destino cada vez,
buscando el maldito carcter nulo una y otra vez, esta funcin es ms y ms lenta de lo que
necesita ser, y no se ampla del todo bien.
Montones de cdigo que usas cada da tienen este problema. Muchos sistemas de archivos
estn implementados de un modo en el que no es buena idea poner muchos archivos en el
mismo directorio. Para ver este efecto, intenta abrir la Papelera de Reciclaje de Windows cuando
est a rebosar -- te llevar horas que se abra, lo que tiene claramente un rendimiento no lineal al
nmero de archivos que contiene. Ah seguro que est el algoritmo de "Shlemiel el Pintor" por
algn lado. Cada vez que algo parezca que debe tener un rendimiento lineal, pero parezca que
tiene un rendimiento exponencial, busca a los Shlemiels ocultos. A menudo estn por tus
libreras. Mirando en un grupo de "strcats" o en un strcat dentro de un bucle, puede que no
parezca tener un rendimiento exponencial, pero eso es lo que est pasando.
Cmo puedo corregir esto? Algunos programadores espabilados de C, implementaron su
propia funcin mistrcat del siguiente modo:
char* mistrcat( char* dest, char* src )
{
while (*dest) dest++;
while (*dest++ = *src++);
return --dest;
}
Qu hemos hecho ah? Con un pequeo coste extra, retornamos un puntero al final de la nueva
cadena, que es ms larga. De ese modo, el cdigo que llama a esta funcin puede decidir aadir
al final sin tener que volver a recorrer la cadena:
char bigString[1000]; /* Nunca s cuanto tengo que reservar... */
char *p = bigString;
bigString[0] = '\0';
p = mistrcat(p,"John, ");
p = mistrcat(p,"Paul, ");
p = mistrcat(p,"George, ");
p = mistrcat(p,"Joel ");
Esto tiene, por supuesto, un rendimiento lineal, no exponencial., as que no sufre ninguna
degradacin cuando tengas un montn de cadenas para concatenar.
Los diseadores de Pascal se dieron cuenta de este problema y lo solucionaron almacenando el
nmero de bytes en el primer byte de la cadena. Estas se llaman Cadenas Pascal. Pueden
contener ceros, y no estn terminadas por nulo. Debido a que un byte slo puede almacenar
nmeros entre 0 y 255, las cadenas Pascal estn limitadas a 255 bytes de longitud, pero debido
a que no estn terminadas por el carcter nulo, ocupan la misma cantidad de memoria que las
cadenas ASCIZ. Lo mejor de las cadenas Pascal es que nunca tienes que hacer un bucle para
averiguar la longitud de la cadena. Buscar la longitud de la cadena es una instruccin en
ensamblador, en vez de un bucle. Es monumentalmente ms rpido.
El viejo sistema operativo de Macintosh usaba cadenas Pascal por todos los lados. Muchos
programadores de C en otras plataformas usaban cadenas Pascal para acelerar los programas.
Excel usa cadenas Pascal internamente, lo que es la razn por la que las cadenas, en muchos
lugares en Excel, estn limitadas a 255 bytes, y es tambin una de las razones por las que Excel
es brillantemente rpido.
Durante mucho tiempo, si queras poner un literal como cadena Pascal es tu cdigo C, tenas
que escribir:
char* str = "\006Hello!";
Pues si, tienes que contar el nmero de bytes a mano, t mismo, y codificarlo en el primer byte
de tu cadena. Los programadores perezosos solan hacer esto, para sus programas lentos:
char* str = "*Hello!";
str[0] = strlen(str) - 1;
Fjate que en este caso, tienes una cadena que est terminada en nulo (esto lo hace el
compilador) as como una cadena Pascal. Yo sola llamarlas jodidas cadenas, porque es ms
fcil que llamarlas cadenas Pascal terminadas en nulo, pero este es un canal para nios, as
que t tendrs que llamarlas por su nombre largo.
Antes he aludido a una cuestin importante. Recuerdas esta lnea de cdigo?
char bigString[1000]; /* Nunca s cuanto tengo que reservar... */
Como hoy estamos dedicando atencin a los bytes, no debera ignorar esto. Tendra que haber
hecho esto correctamente: averiguar cuantos bytes necesito y reservar la cantidad necesaria de
memoria.
Debera?
Porque de otro modo, como ves, un hacker avispado leer mi cdigo y se dar cuenta que estoy
reservando slo 1000 bytes y esperando que sean suficientes, as encontrar algn modo fcil
de burlarme y hacerme concatenar una cadena de 1100 bytes en mi memoria de 1000 bytes, as
que sobrescribiendo el marco de pila y cambiando la direccin de retorno, se ejecutar algn
cdigo que el hacker haya escrito. De esto es de lo que hablan cuando dicen que un programa
en particular es susceptible al desbordamiento de buffer. Esta fue la causa nmero uno de
intrusiones y gusanos en los viejos das, antes de que el Microsoft Outlook hiciera el pirateo lo
suficientemente fcil para que los adolescentes lo practicaran.
De acuerdo, as que todos esos programadores son un poco torpes. Deberan averiguar cuanta
memoria reservar.
Pero en realidad, el C no nos lo pone fcil. Volvamos a mi ejemplo de los Beatles:
char bigString[1000]; /* Nunca s cuanto tengo que reservar... */
char *p = bigString;
bigString[0] = '\0';
p = mistrcat(p,"John, ");
p = mistrcat(p,"Paul, ");
p = mistrcat(p,"George, ");
p = mistrcat(p,"Joel ");
Cuanto debo reservar? Intentemos hacerlo por el mtodo correcto:
char* bigString;
int i = 0;
i = strlen("John, ")
+ strlen("Paul, ")
+ strlen("George, ")
+ strlen("Joel ");
bigString = (char*) malloc (i + 1);
/* recuerda reservar espacio para el terminador nulo */
...
No puedo creerlo. Probablemente ya ests a preparado para cambiar de canal. No te voy a
echar las culpas, pero aguntame un poco porque esto se pone realmente interesante.
Tenemos que escanear a travs de todas las cadenas una vez slo para averiguar lo largas que
son, y despus, escanearlas otra vez para concatenarlas. Al menos si usas cadenas Pascal, la
operacin strlen es rpida. Quiz podemos escribir una versin de strcat que redireccione la
memoria por nosotros.
Eso nos abre un nuevo agujero para los gusanos: las reservas de memoria. Sabes cmo
funciona malloc? Por la naturaleza de la funcin malloc, tiene una lista enlazada muy larga de
bloques de memoria disponible, llamada "cadena de libres" (free chain). Cuando llamas a
malloc, se recorre la lista enlazada buscando un bloque de memoria que sea lo suficientemente
grande para tu peticin. Entonces, corta ese bloque de memoria en dos trozos: uno del tamao
que has pedido y el otro con los bytes que sobran, te da el bloque que pediste y pone el bloque
sobrante (si hay) de nuevo en la lista enlazada. Cuando llamas a la funcin free, aade el bloque
que ests liberando en la cadena libre. Eventualmente, la cadena libre cambia continuamente
hasta slo contener pequeas piezas, y si pides una pieza grande, no hay ninguna disponible del
tamao que queras. As que malloc hace una espera, y comienza a rumiar alrededor de la
cadena de libres, ordenando cosas y juntando pequeos bloques adyacentes en bloques ms
grandes. Esto tarda 3 das y medio. El resultado final de todo este lo es que el rendimiento de
malloc nunca es muy bueno (siempre debe recorrer la cadena de libres) y, a veces, es
impredecible y espantosamente lento mientras hace esta limpieza. (Esto es, dicho sea de paso,
el mismo rendimiento que los sistemas de recoleccin de basura, as que todas las aclamaciones
de la gente acerca de cmo los recolectores de basura imponen una penalizacin en el
rendimiento no son del todo ciertas, mientras que las implementaciones tpicas del malloc tienen
el mismo tipo de inconvenientes. De todas formas, hay una menor prdida de rendimiento en el
caso del malloc que en caso de los recolectores de basura.)
Los programadores espabilados minimizan los inconvenientes potenciales de malloc,
reservando siempre bloques de memoria que son potencias de 2. Ya sabes, 4 bytes, 8 byes, 16
bytes, 18446744073709551616 bytes, etc. Por razones que deberan ser intuitivas para todo el
mundo que juegue con Lego, esto minimiza la cantidad de la fragmentacin que ocurre en la
cadena de libres. Aunque pueda parecer que esto desperdicia espacio, es tambin fcil de ver
cmo nunca se desperdicia ms del 50% del espacio. As que tu programa usa, no ms de dos
veces la cantidad de memoria que necesita, lo que no es nada del otro mundo.
Supongamos que escribes una funcin strcat, que redirecciona el buffer de destino
automticamente. debera redireccionar exactamente a la nueva cantidad necesitada? Mi
profesor y mentor Stan Eisenstat sugiere que cuando llames a realloc, deberas duplicar el
tamao de la memoria que previamente ha sido reservada. Esto significa que nunca tienes que
llamar a realloc ms de log n veces, lo cual tiene un rendimiento aceptable incluso para
cadenas gigantescas, y nunca desperdiciars ms del 50% de tu memoria.
De cualquier modo, la vida se vuelve ms y ms complicada aqu abajo en bytelandia. No ests
contento de no tener que escribir en C nunca ms? Tenemos todos esos magnficos lenguajes
como Perl, Java y VB, y XSLT que nunca te hizo pensar de un modo como este, slo lo
resuelven, de algn modo. Pero en ocasiones, la infraestructura de caeras sobresale en el
medio de la sala de estar, y tenemos que pensar si debemos o no utilizar la clase String o
StringBuilder, o alguna otra distincin, debido a que el compilador no es lo suficientemente
inteligente para entender todo sobre lo que estamos intentando conseguir, y nos intenta ayudar a
que no escribamos algoritmos de Shlemiel inadvertidos.
La semana pasada escrib que no puedes implementar la instruccin SQL SELECT autor FROM
libros de un modo rpido cuando tus datos estn almacenados en XML. Slo en el caso en que
nadie entienda de qu estuve hablando, y ahora, que ya hemos estado rondando alrededor de la
CPU durante todo el da, tiene ms sentido.
Cmo implementa una base de datos relacional la instruccin SELECT autor FROM libros? En
una base de datos relacional, cada fila de la tabla (p.e. la tabla libros) tiene exactamente la
misma longitud en bytes, y cada campo est siempre situado a la misma distancia del principio
de la fila. As, por ejemplo, si cada fila de la tabla libros tiene 100 bytes de longitud, y el campo
autor est a una distancia de 23 desde el principio de la fila, entonces habr autores
almacenados en los bytes 23, 123, 223, 323, etc. Cul es el cdigo para moverse al siguiente
registro en el resultado de una consulta? Bsicamente, este:
puntero += 100;
Una instruccin del procesador. Raaaaaaapido.
Ahora, echemos in vistazo a la tabla de libros en XML
<?xml bla bla>
<libros>
<libro>
<titulo>UI Design for Programmers</titulo>
<autor>Joel Spolsky</autor>
</libro>
<libro>
<titulo>The Chop Suey Club</titulo>
<autor>Bruce Weber</autor>
</libro>
</libros>
Pregunta rpida: Cual es el cdigo para moverse al siguiente registro?
Estoooo....
Llegados a este punto, un buen programador dira: bien, lemos a memoria el rbol XML para
que podamos operar en l razonablemente rpido. La cantidad de trabajo que tiene que hacer la
CPU en este caso, para hacer el SELECT autor FROM libros te aburrira hasta que se te salten
las lgrimas. Como todo programador de compiladores sabe, el anlisis lxico y sintctico son
las operaciones ms lentas de la compilacin. Basta decir que esto conlleva manipulacin de
cadenas, que hemos descubierto que es lenta, y montones de operaciones de reserva de
memoria, que hemos descubierto que son lentas, para analizar sintcticamente, leer y construir
el rbol en memoria. Todo esto suponiendo que tendrs suficiente memoria para cargar todo a la
vez. Con las bases de datos relacionales, el rendimiento de desplazarse de registro en registro
es constante, y es, de hecho, una instruccin del procesador. Esto es as por su diseo. Y
gracias a los archivos proyectados en memoria, slo tienes que cargar las pginas de disco que
realmente vayas a utilizar. Con el XML, si haces un pre-anlisis, el rendimiento de desplazarse
de registro en registro es fijo, pero es un tiempo de inicio enorme, y si no haces ese pre-anlisis,
el rendimiento de moverte entre registros vara dependiendo de la longitud del registro y es
todava cientos de instrucciones del procesador.
Lo que esto significa para mi es que no puedes usar XML si necesitas un buen rendimiento y
tienes montones de datos. Si tienes muy pocos datos, o si lo que ests haciendo no tiene por
qu ser rpido, el XML es un buen formato. Y si realmente quieres lo mejor de ambos mundos,
tienes que idear un modo de almacenar metadatos junto con tu XML, algo parecido a la cuenta
de bytes de las cadenas Pascal, que te proporciona consejos acerca de donde estn las cosas
en el archivo, de modo que no tengas que analizarlo y escanearlo para ello. Pero, por supuesto,
en ese caso no puedes usar un editor de textos para modificar el archivo, porque eso echara a
perder los metadatos, as que no es realmente XML.
Llegados a este punto, para aquellos tres simpticos miembros de mi audiencia que estn an
conmigo, espero que hayis aprendido o reflexionado algo. Espero que haber pensado en los
temas aburridos de primero de carrera, como el modo de funcionar de strcat y malloc, te haya
dado una nueva herramienta para pensar sobre los ltimos y ms altos de los niveles,
estrategias y decisiones que tomas sobre la arquitectura, tratando con tecnologas como XML.
Como trabajo para casa, puedes pensar sobre cmo los chips Transmeta siempre parecern
lentos, o porqu las especificaciones originales para las tablas de HTML fueron tan mal
diseadas que tablas grandes en pginas web no se podan ver rpidamente por las personas
que usaban mdem. O piensa acerca de por qu la arquitectura COM es tan rpida, aunque deja
de serlo cuando atraviesas las fronteras de tu proceso. O sobre porqu la gente del NT puso el
controlador de vdeo en el espacio del kernel en vez del espacio de usuario.
Todas estas cosas requieren que pienses en los bytes, y afectan a las capas ms altas de
decisin que hacemos en todos los tipos de arquitectura y estrategia. Este es el por qu, desde
mi punto de vista, la enseanza en las carreras informticas debe comenzar desde las bases,
usando C y construyendo desde el procesador. En estos momentos estoy muy disgustado
porque muchos programas de enseanza creen que Java es un buen lenguaje inicial, porque es
"fcil" y no te confunde con todos los temas aburridos sobre cadenas y malloc, pero puedes
aprender una buena POO que har tus programas incluso ms modulares. Esto es un desastre
pedaggico que acabar por ocurrir. Generaciones de graduados estn llegando a nosotros y
creando algoritmos de Shlemiel, y ellos ni siquiera se dan cuenta, porque no tienen ni idea de lo
qu son las cadenas en un nivel profundo, difcil, incluso si no puedes ver eso dentro de tu script
en Perl. Si quieres ensear a alguien alguna cosa bien, debes empezar en los niveles ms bajos.
Como en "Karate Kid". Limpiar, Encerar. Limpiar, Encerar. Haz esto durante tres semanas.
Despus, tumbar a otros karatekas es fcil.


Joel Spolsky es el fundador de Fog Creek Software, una pequea empresa de software en Nueva York. Es
titulado por la Universidad de Yale y ha trabajado como programador y gerente en Microsoft, Viacom, y
Juno.