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.