Responsabilidad del programador/compilador-S.O. en la eficiencia de la lectura/escritura de datos en disco

Iniciado por fzp, 3 Diciembre 2021, 12:31 PM

0 Miembros y 1 Visitante están viendo este tema.

fzp

Tengo entendido que la rapidez de las operaciones de lectura/escritura en disco dependen de si se hacen varias en bloque o como datos aislados uno a uno. No sé seguro si ésto es así -y menos en los SSD-, es lo que yo tenía entendido; pero si no es así se puede ahorrar la lectura de lo que sigue.

Dado por cierto lo anterior lo que quiero saber es si el compilador o el Sistema Operativo adaptan esas operaciones a la memoria disponible -de una forma transparente para el programador- o debe ser éste quien programe esas operaciones de la forma más eficiente.

Lo aclaro con un ejemplo concreto. Supongamos que un programa hace una multiplicación de matrices que están almacenadas en archivo en disco y que, por su dimensión, no caben en la memoria del ordenador y, por tanto, no pueden leerse de una tacada y operar con ellas en memoria, guardar el propducto también en memoria y al final grabar en disco el resultado de una vez.

Lo más fácil para el programador -pero quizá no lo más eficiente en tiempo de ejecución- es mediante bucle leer un dato de fila de una matriz, leer un dato de columna de la otra, multiplicar y acumular el producto, proceder igual con el siguiente elemento de fila, el siguiente de columna, volver a acumular el producto y cuando se haya terminado el producto fila x columna grabar el resultado en disco; y volver a proceder con fila-columna.

La pregunta es: ¿si se programa de esa forma, en tiempo de ejecución, se ejecuta también así o bien al compilar, o al ejecutar el S.O. realizan las operaciones E/S (disco) en bloques según la RAM disponible y hacen esas operaciones en pequeños bloques en RAM?

¿O bien es responsabilidad del programador -si quiere mejorar la eficiencia- el prever más bucles adicionales y más arreglos adicionales que realizen, por ejemplo, lectura de 100 datos de fila, 100 de columna (el 100 es un ejemplo, podría ser otro), los almacenen en en RAM, operen con ellos, realicen otras 100 lecturas, etc., hasta terminar con fila-columna, almacenar en otro arreglo (de otros 100 datos), proseguir multiplicando, cuando ya tenga 100 resultados escribirlos de golpe en el disco... y proseguir hasta acabr el producto de las matrices?

En resumen: que si el programador para mejorar la efciencia tiene que programar (valga la redundancia) las operaciones de lectura/escritura en bloque adaptados a la RAM, o puede confiar esa labor al compilador o al S.O.: en tiempo de ejecución de forma transparente para él.

Serapis

El compilador no hace según qué optimizaciones... pero de modo general pueden hacerse 3 o 4 fases de optimizaciones.

- Cuando se completa el análisis semántico, hace una optimización de código entre operaciones sencillas, del tipo si se suman dos constantes, remplazarlas por una sola constante ya sumada. Si hay una cascada de sumas, utilizar un registro destino (acumulador), para ir almacenando (algo como x= b +c) ; y= (x + v): z= (y +s), se convertiría en: mov regz b; add regz c; add regz v; add regz s, si se hace el a´rbol sintáctico entre uno y otro se verá la cantidad de instrucciones que se evitan), también por ejemplo cosas como si se multiplica o divide por una potencia de 2, cambiarlo por un desplazamiento a derecha o izquierda, etc...  Esta optimización suele ser a nivel de expresiones.

- Tras esa optimización hay una más global, a un nivel mayor, que suele consistir por ejemplo, en sacar de un bucle ciertas operaciones que bastan ser calculadas una vez antes del bucle...bifurcaciones para las que puede calcularse una dirección de desplazamiento relativo (casos de Select case (switch) o if...elseif..elseif... Así, se hace un cálculo inicial, se suma a la dirección actual y se salta al punto exacto, en vez de comprobar caso por caso (cuando esto e sposible, lo que queda recogido en el árbol sintáctico cuabndo se realiza dicho análisis)), múltiples llamadas a una rutina que pueden yacer como una única llamada, etc...

- Cuando se convierte a código objeto, también suelen hacerse optimizaciones. Esta suelen utilizar mucho la notación polaca inversa a la que el código se ha ido convirtiendo y que permite encontrar fácilmente ciertas optimizaciones basado en lo que permita el diseño del tipo de código objeto generado.

- Finalmente cuando se compila para la plataforma destino, conforme a las instrucciones que admita, suelen hacerse optimizaciones que son dependientes del hardware destino. En generla aquí se decide en muchas ocasiones si ciertas construcciones son más efectivas utilizando una pila o los registros del sistema. Sin conocer la plataforma destino, estás optimizaciones no pueden asegurarse que lo sena, por eso se demoran hasta dicho momento.




El S.O. mantiene búferes de archivos, en general a veces lejos de ser efectivos, suponen un lastre. Lo ideal es que un S.O. base la decisión de utilizar o no búferes en base a si el usuario abre un fichero para lectura, escritura, lectura y escritura, y aún así si no vas a hacer múltiples lecturas al mismo punto, puedne no ser efectivos. En general el S.O. no tiene mucha libertad de elección, o mejor dicho, si hace ciertas elecciones no tiene garantías de que sena acertadas, después de todo no es un compilador y no tiene que procesar nada al respecto, son decisiones 'generales' que no pueden personalizarse para cada programa.

En cambio el driver del disco (el código interno del fabricante, no el que proporciona el S.O.)... en general leen como mínimo un sector, así tu le digas que solo necesitas un byte, y luego otro y otro... pero pueden hacer acopio de búferes mucho mayores, los detalles exactos quedan a cubierto porque queda dentro del hardware del propio disco, y en general sirven de caché, de modo que si luego solicitas un acceso de lectura al mismo sector, lo provee del buffer en memoria que mantiene, en vez de leer estúpidamente de nuevo otra vez del disco. Esto actualmente ha aumentado dado que el precio de chips de memoria han bajado y las velolcidades de RAMs son mayores que las lecturas mecánicas (hoy puede afirmarse que los discos duros son híbridos entre esa tecnología cierta cantidad de RAM en froma SSD oculta, eso mismo lo hace bastante frágil a fallos ante determinados problemas), así un disco duro, puede tener una velocidad mucho mayor que la que obtendría si con cada petición tuviera que leer o escribir en el disco físicamente. Proporcionan acceso al S.O. a ciertos búferes como una caché, pero no hay forma de saber si dicho búffer será mucho mayor que lo que el S.O. solicite...

Dicho de otro modo, si el programador no provee cierta soltura con búferes propios, no será igual de íptimo, porque incluso auqnue el S.O. mantenga una caché y el propio disco lo provea máyor, todavía el tiempo de verificar si una dirección solicitada consta en la caché, consume algo de tiempo.

En consecuencia, el programador debiera para manejo de grandes volúmenes de datos proveer un array de tamaño múltiplo de un sector, y tirar de bucles (aunque luego lo limites, si resulta que el volumen leído supera el tamaño actual del fichero (pudiera estar relleno de 'urliburly').
Por supuesto si un fichero ocupa menos de un sector, no tiene sentido proveer un array de mayor tamaño, salvo que haya un bucle que lean un fichero y opere en él y al término otro, etc... en este caso, proveer un array de cierto tamaño, permite que sea memoria reservada para todo ese bucle externo, de otro modo será un array que se genera de nuevo para cada fichero con que se va a operar...

Aunque un S.O. el disco y los compiladores intenten ser efectivos, no realizan nunca manipulación del programa al nivel que tu esperas, el compilador JAMÁS debe modificar la semántica que el programador ha provisto (salvo corrección de errores corregibles de los que además debe avisar). El S.O. no está obligado a más que proveer un sistema de caché (que muchas veces es inefectivo, si lo que tratas de hacer es leer no más de una vez cada dato) y lo que el disco hace queda alejado del programa, por tanto se limita a proveer algortimos basado en heurísticas de frecuencia de uso, para decidir el tamaño de los búferes internos por encima de lo que el S.O. demande... por supuesto deberá descartar, cuando yerre.

El peso finalmente de la optimización depende básicamente del diseño del programador, procura ceñirte a ello.

fzp

Comprendido. Gracias. Explicación detallada y pormenorizada. Es muy de agradecer el tiempo dedicado a ella.