viernes, 20 de junio de 2008

Buffers: por defecto y por exceso

Llevo 11 años en el mundo Java. A lo largo de ese tiempo ven viniendo como los maderos a la playa los mismos error repetidos en quizás 20 ocasiones por personas distintas, en momentos distintos.

Defecto

Por ejemplo este: cargar todo el contenido de un fichero o un blob en memoria, no para trabajar con sus datos (que sería hasta discutible) sino para derivarlo hacia otra colaboración/proceso/cliente.

Hace poco me he encontrado, de nuevo, con un código similar a este para enviarle un fichero al navegador (eliminadas las partes de content-disposition, content-type, excepciones, etc):

input = new java.io.FileInputStream(pdf);
int longitud = input.available();
byte [] datos = new byte [longitud];
input.read(datos);
response.getOutputStream().write(datos);
response.getOutputStream().flush();


Lo que podríamos catalogar de como mínimo "bestia" si no sabemos si puede tratarse de ficheros pequeños o grandes, o si vamos a tener un poquito de concurrencia. ¡Aparte de que vaya usted a saber qué valor devuelve el método available()!

Es cierto que cada vez la memoria es más "barata" y que tiende a pensarse que es infinita... pero también es cierto que el número de sesiones simultáneas, clientes concurrentes y tamaño de los ficheros / contenidos multimedia crece en una proporción similar. ¿Tanto cuesta utilizar un pequeño buffer?. Un código equivalente similar a este:

input = new java.io.FileInputStream(pdf);
byte [] buffer = new byte [16*1024]; // 16 KBytes
OutputStream output = response.getOutputStream();
int readed;
while ( (readed = input.read(buffer)) > 0 ) {
output.write(buffer, 0, readed);
}
out.flush();


Importantísimo: Sólo hay que escribir el tamaño real que el método de lectura ha metido en el buffer, lo que significaría que en la última escritura a la salida meteríamos basura detrás del final de lo que realmente teníamos que escribir, y eso puede dar problemas serios. Es decir me suelo encontrar con esta otra versión del bucle que es ABSOLUTAMENTE INCORRECTA (yo mismo la he cagao en alguna ocasión reciente, al loro):
while ( (fiPdf.read(buffer)) > 0 ) {
out.write(buffer);
}
Por cierto, en mi experiencia, en función de la arquitectura y sistema operativo, tamaños de entre 16 y 32 KBytes suelen ser los apropiados para este tipo de operaciones y para el uso de java.io.BufferedInputStream y java.io.BufferedOutputStream (por cierto esas clases usan por defecto un buffer 8KBytes si no especificas otra cosa en sus contructores).

Exceso

Por otro lado, también es común encontrarnos con el caso contrario: utilizar buffers cuando no hacen falta. ¿Cuántas veces hemos visto algo parecido a new GZIPOutputStream(new BufferedInputStream(new FileOutputStream(tempFile)))? ¿o ps.setBlob(1, new BufferedInputStream(new FileInputStream(tempFile),32*1024), tempFile.length()). Y el desarrollador, tan orgulloso de ser más listo que nadie.

Por regla general, si vamos a invocar a un método cuyo parámetro es un InputStream o un OutputStream, no hay que utilizar como intermediario un buffer porque por regla general el método invocado está optimizado. Es decir, debemos confiar en nuestros partners. Y por supuesto, si partimos de un array, no usemos un buffer como intermediario, ¡porque nuestro origen ya es un buffer!

En definitiva, tanto por exceso como por defecto, nos podemos encontrar con un sobre uso de la memoria con un efecto multiplicador más que curioso.

Muchas veces le doy vueltas a que deberían darle alguna que otra pensada a la implementación interna de los arrays en la máquina virtual; es decir que internamente la JVM hiciera swapping a disco de arrays grandes como uno de los pasos del recolector de basura, GC!!!

Hablando del tema... en su día yo hice una clase que era una especie de abstracción de todo esto: un "ByteArrayAdapter" que internamente decidía utilizar un byte array o un fichero temporal en función de tamaños y algunos que otros condicionantes, de forma transparente al desarrollador. El problema es el de siempre: difícil de explicar y difícil de recordar que existe si no eres el creador de la criatura, y una cosa más que mantener. Repito el autoconsejo de uno de los primeros posts: cuanto menos contenido tenga la biblioteca propia de clases de nuestra compañía / grupo de trabajo / producto, mucho mejor. (¿Por qué todos nos empeñamos en que tenemos que tener una biblioteca estándar en nuestra empresa? ¿por qué tenemos que creernos más listos que las comunidades opensource? ¿por qué terminamos teniendo cuatro versiones de lo mismo que encima los nuevos integrantes de los equipos terminan "olvidando"? Si siempre pasa lo mismo, no caigamos en lo mismo de siempre.

Bueno, que me he ido del asunto. Volvemos a lo de siempre: seguimos necesitando que el desarrollador sea un ser pensante y con experiencia. Afortunadamente, por cierto.

Nota: el tema de este post en realidad es la escalabilidad más que el rendimiento :-)

No hay comentarios: