Programación Avanzada: 7. Laboratorios Nicolas Thériault Departamento de Matemática y Ciencia de la Computación, Universidad de Santiago de Chile. N. Thériault (USACH) Presentación 1 / 28 Laboratorios (fechas tentativas) 1 Resolver sistemas lineales grandes, 23/10 2 Multiplicar matrices, 06/11 3 Calcular matrices inversas y resolver sistemas grandes, 20/11 4 Ordenar listados de puntos según sus coordenadas, 18/12 5 Resolver un problema de geometrı́a computacional, 15/01 6 Resolver un problema de programación dinamica, 22/01 N. Thériault (USACH) Presentación 2 / 28 Para los laboratorios 1, 2 y 3 Para evitar problemas de redondeo en la aritmética de matrices: se trabaja módulo p = 3781996138433268199 (número primo de 62 bits) aritmética de enteros (exacta, no hay problemas de redondeo) permite dividir entre cualquier número 6= 0 suma/restas con resultados en {0, 1, . . . , p − 1} producto con resta modular programados para long de 32 bits, long long de 64 bits deberı́a ser compatible con compiladores C99, C11 y C17 inverso (b −1 ): utiliza el algoritmo Euclidiano extendido división a/b: calcular como a · (b −1 ) operaciones especiales: long long SumaP( long long a , long long b ) ; long long RestaP( long long a , long long b ) ; long long MultP( long long a , long long b) ; long long InvP( long long a ) ; N. Thériault (USACH) Presentación 3 / 28 Para los laboratorios 4 y 5 Para evitar problemas de redondeo en la evaluación de distancias: se trabaja con enteros long aritmética de enteros para d 2 (exacta, no hay problemas de redondeo) I I I I I d1 > d2 si y solo si d1 2 > d2 2 Distancias al cuadrado en formato long long Calcular la raı́z cuadrada introduce un error, complica para comparar Riesgo: d + 1 y d − 2 parecen distintos aunque deberı́an ser iguales Riesgo: d1 + 1 > d2 − 2 aunque d1 < d2 programados para long de 32 bits, long long de 64 bits deberı́a ser compatible con compiladores C99, C11 y C17 I I para C11 y C17, se puede trabajar con listados más grandes para C99, empieza a producir repeticiones de puntos más temprano N. Thériault (USACH) Presentación 4 / 28 Objetivos Trabajar en equipos Seleccionar técnicas de programación adecuadas según el problema a resolver Aplicar las técnicas vistas en clases en situaciones prácticas Controlar la utilización de memoria dentro de los programas Producir código limpio y eficiente Hacer experimentos para estimar el comportamiento del algoritmo a grande escala N. Thériault (USACH) Presentación 5 / 28 Aspectos generales Utilizar un sistema de menu en la ejecución del algoritmo. Si debe leer un archivo (para trabajar los datos que contiene), debe pedir a los usuarios de escribir su nombre dentro de la ejecución del programa Testear todas sus funciones. Si suponen algunos limitantes en las entradas, documentarlo Siempre deben liberar la memoria utilizada antes de cerrar el programa (antes de hacer el “salir” final) I Si necesario, hacer una pausa del tipo: printf(“Listo para cerrar, entrar cualquier entero: ”); scanf(“%d”,&selecion); N. Thériault (USACH) Presentación 6 / 28 C vs C++ Ventajas del C++: Programación orientada a objetos I Manejo más flexible de los tipos de variables I Modelos (templates) I Métodos/funciones virtuales Manejo de excepciones (exception handling) Librerı́as estandares más amplias N. Thériault (USACH) Presentación 7 / 28 desventajas del C++ Algunas de las funcionalidades del C++ pueden afectar la eficiencia de los programas: Modelos (templates) I I Requiere cuidado para asegurar que no tiene mucho costo Hacer el trabajo especifico al caso permite tomar ventaja de algunas propiedades Métodos/funciones virtuales I Afecta la eficiencia Manejo de excepciones (exception handling) I I I Afecta la eficiencia Da seguridad para evitar errores, pero las verificaciones se pagan Mejor verificar solamente cuando es necesario Librerı́as estandares más amplias I I Requiere cuidado para asegurar que no tiene mucho costo Si se utiliza algo sin entenderlo, puede no ser lo más optimal para el caso N. Thériault (USACH) Presentación 8 / 28 Ventajas de C Lenguaje más pequeño, pero completo: Se puede conocer todo el lenguaje estandar (sin tener que utilizar referencias). Sencillo de leer. Hay pocas manejas de hacer las cosas, por lo que es más sencillo entender el código escrito por otros/otras. Facilita incorporar nuevas personas en un grupo de trabajo. Obliga a ser más cuidadoso Los programas en C++ pueden ser igual de eficientes de los en C, pero es más fácil de “descuidarse” en C++, y los descuidos se pueden acumular unos sobre otros. Para trabajos conjuntos: TODO el equipo debe mantener el cuidado N. Thériault (USACH) Presentación 9 / 28 Entradas y salidas en C formato básico: printf (equivalente de cout) printf(“Listado de %d entradas.\n”,valor); scanf (equivalente de cin) scanf(“%d”,&valor); Debe incluir los tipos de variables en la parte de “texto”: c (char), s (string) d (int), u (unsigned), ld (long), lld (long long) f (float - decimal), e (float - exponencial), lf (double), llf (long double) En printf, se puede inticar el número de posiciones (no utilizar en scanf): %5d → entero de ≥ 5 espacios decimales %6.3f → número decimal de 6 posiciones (con el “.”), hasta los milésimos. Con archivos: fprintf, fscanf (formato parecido, pero incluye el nombre del archivo) Empezar con fopen, terminar con fclose Tipo de variable del archivo: FILE (en mayusculas) N. Thériault (USACH) Presentación 10 / 28 Punteros Los punteros corresponden a la posición (bloque de memoria) donde se encuentran unos datos Se definen como las estructuras a las cuales apuntan, con una ∗ antes del nombre tipo *nombre de la variable; float *puntoflotante; long *enterogrande; Una vez definido, no se escribe el ∗ puntoflotante = NULL; Para conocer el puntero asociado a una variable, se pone & frente al nombre puntoflotante = &abc; Necesarios para trabajar las entradas y salidas de muchas funciones I Por ejemplo si una función requiere cambiar el valor de una de sus entradas N. Thériault (USACH) Presentación 11 / 28 Arreglos Bloques continuos de memoria de un mismo tipo de estructura. Se debe conocer el tamaño (cantidad de entradas) al momento de definir Se definen como sus estructuras, con un [tamaño] después del nombre I El tamaño puede ser un valor fijo o una variable (int/long) con un valor dado tipo nombre[tamaño]; float puntosflotantes[7]; long enterosgrandes[n]; La j-ésima entrada (empezando a contar posiciones en 0) se obtiene como: nombre[j]; puntosflotantes[4]; En efecto, un arreglo es un puntero a la primera entrada I I I Para pasar un arreglo como entrada de una función, se utiliza solo el puntero El procesador calcula la posición de cada entrada sumando su indice al puntero inicial Las funciones que reciben arreglos, no conocen su tamaño N. Thériault (USACH) Presentación 12 / 28 Memoria dinámica en C Permiten reservar espacios de memoria que seguirán disponibles fuera de la función donde fueron definidos, en general como arreglos, sin restricción de tamaño total (dentro de lo que permite el procesador). malloc: reserva un bloque de espacios de memoria, devolviendo la dirrección inicial int *arreglo = (int *) malloc( 20 * sizeof(int) ) ; calloc: reserva un bloque de espacios de memoria, inicializando en 0 int *arreglo = (int *) calloc( 20 , sizeof(int) ) ; realloc: ajusta el espacio de memoria asociado a una variable (puede cambiar la dirección inicial). Libera el espacio ya no utilizado y copia los valores si la dirección cambia. arreglo = (int *) realloc( arreglo , 20 * sizeof(int) ) ; free: libera (todo) el espacio asociado a la variable free( arreglo ) ; I I No libera los espacios apuntados dentro la variable Necesario para evitar que la memoria queda reservada mientrás funciona el programa N. Thériault (USACH) Presentación 13 / 28 Estructuras en C Definición (ejemplo: nodo de árbol binario): typedef struct nodoarbol { long valor ; nodoarbol *ramaizquierda ; nodoarbol *ramaderecha ; } nodo ; I I I podemos poner cuantos elementos como queremos, de cualquier tipos el nombre inicial (temporario) sirve para poner punteros a la estructura dentro de ella el nombre final será el oficial de la estructura, puede ser distinto o repetir el inicial Definir una variable/puntero: nodo bloque = { 3 , izq , der } ; nodo *dondebloque ; Llamado a un elemento: bloque.valor ; dondebloque->ramaizquierda ; (*dondebloque).ramaizquierda ; En malloc, calloc y realloc: sizeof( nodo ) ; N. Thériault (USACH) Presentación 14 / 28 Matrices y arreglos dobles El lenguaje C permite definir arreglos dobles (matrices), pero su uso es limitado: Se define como tipo nombre[tamaño1][tamaño2]; En realidad es un arreglo simple de largo tamaño1 × tamaño2, donde los dados están organizados en “filas” de largo tamaño2. La posición de del bloque nombre[i][j] es i × tamaño2 + j Para pasar a una función se hace como para un arreglo simple, con el puntero del bloque inicial Se pierde el formato “arreglo doble” para la función, se ve como un arreglo simple La función no puede utilizar directamente la organización interna Como consecuencia, los arreglos dobles son poco prácticos para ser utilizados en sub-funciones: Riesgos de trabajo fuera del arreglo (simple o doble), que puede producir un segmentation fault o corromper los valores de otras variables Se deben enviar todos los tamaños de los arreglos como entradas de la función Se debe calcular la posición como i × tamaño2 + j para todas las utilizaciones N. Thériault (USACH) Presentación 15 / 28 Matrices, alternativa 1 una forma manera de reducir los cálculos consiste en utilizar punteros dobles: Se define como tipo **nombre; Es un arreglo de punteros, cada entrada corresponde a la posición inicial de una fila (arreglo de variables tipo) En vez de hacer el cálculo “i × tamaño2 + j”, se sigue el i-ésimo puntero y se toma la j-ésima entrada de este Si se utilizan varias entradas de una misma fila, se puede copiar el puntero del arreglo para reducir el trabajo Trabajar con memoria dinámica: I I Se crea el espacio para el arreglo de punteros primero, luego se crea el espacio para cada fila y se guarda su ubicación en el arreglo de punteros. Para liberar el espacio de memoria, primero liberar el espacio de cada filas, y luego el espacio del arreglo de punteros Más flexible para la memoria: no requiere un espacio continuo para toda la matriz, solamente un espacio continuo para cada filas (que pueden ser separadas una de la otra) y un espacio continuo para el listado de filas. Todavı́a hay que entregar las dimensiones como entradas de funciones que utilizan la matriz. N. Thériault (USACH) Presentación 16 / 28 Matrices, alternativa 2 Definir estructuras especiales para contener la matriz: Una estructura que contiene las dimensiones (tamaños) de la matriz y un puntero para el arreglo (o puntero doble) Ejemplo: typedef struct matriz { long filas ; long columnas ; double **listado ; } matriz ; Para enviar a funciones, se necesita solamente la estructura (o su puntero) como entrada en vez de tener las dimensiones como entradas a parte I I I Código más compacto Más facil de seguir y verificar Ofrece más seguridad (menos riesgo de inconsistencias de tamaños por errores de redacción o confusiones) Se puede considerar estructuras especiales para las filas también, que contienen el tamaño y el puntero para el arreglo I Puede facilitar operaciones de filas Desventaja: hay que manejar la estructuras para seguir el código N. Thériault (USACH) Presentación 17 / 28 Funciones en C Incluyen los tipos (estructura) de cada variable void Invertir Matriz( matriz A , matriz *B ); // B = A∧(-1) I void: sin entrada o salida (según donde se escribe) Dos funciones no pueden tener el mismo nombre, aun si el tipo de variables cambia int maximo int( int a , int b ); float maximo float( float a , float b ); El compilador puede permitir algunos ajustes de tipos, pero no es recomendable I Ejemplo: si se manda un int de entrada cuando se pide un long Se debe incluir las declaraciones de funciones antes de utilizarlas I Esencial para funciones recursivas Funciones con resultado único I I Utilizar return(variable), lo que termina la ejecución de la función. Utilizar puntero como entrada (escribir el resultado en la dirección asociada). Funciones con varios resultados I I I Utilizar punteros como entradas (escribir los resultados en las direcciones asociadas). Utilizar un arreglo (si todos los resultados son del mismo tipo). Utilizar una estructura especial. N. Thériault (USACH) Presentación 18 / 28 Algunas funciones y constantes comunes en C clock(): tiempo del procesador asociado al programa, en tipo de variable “clock t” CLOCKS PER SEC: constante (de tipo clock t) para convertir a segundos rand(): generador aleatorio, devuelve enteros entre 0 y RAND MAX srand(semilla): inicializa la “semilla” del generador aleatorio arc4random(): generador aleatorio con mejores propiedades, devuelve enteros entre 0 y UINT32 MAX. No disponible en versiones más antiguas de C abs(int): valor absoluto; labs(long), fabs(float), fabsf(double), fabsl(long double) log2(float): logaritmo en base 2; log2f(double), log2l(long double) ceil(valor): parte entera (redondeada hacı́a arriba) floor(valor): parte entera (redondeada hacı́a abajo) NULL: puntero “vacio” N. Thériault (USACH) Presentación 19 / 28 Algunos aspectos a considerar al programar Orden de las operaciones Operadores lógicos y acciones condicionales Bucles while y for Matrices y arreglos dobles Sub-funciones e inlining Funciones recursivas Testeo de programas N. Thériault (USACH) Presentación 20 / 28 Orden de las operaciones Siempre verificar si el orden de las operaciones importan: I Para sumas y productos de enteros: conmutativos y asociativos (el orden no importa) I Para sumas de matrices: conmutativas y asociativas I Para productos de matrices: asociativos pero no conmutativos I Para productos de floats: conmutativos y asociativos I Para sumas de floats: conmutativas pero no asociativos A · (B · C ) = (A · B) · C F A · B 6= B · A Problemas de redondeos (estudio de análisis númerico, etc) −70 20 + 3 · 10 −70 3 · 10 −70 + . . . + 3 · 10 −70 + . . . + 3 · 10 = 20 + 20 = 20.0000000006 Algunas funciones conmutativas pueden no considerar ambas entradas como equivalentes: I “acumular” (suma que pone el resultado en un espacio de entrada) podrı́a ser permitido para a ← a + b pero no para b ← a + b I Se recomienda utilizar const en la definición de la función si se considera que el bloque asociado al puntero de entrada no puede ser afectado (seguridad) N. Thériault (USACH) Presentación 21 / 28 Operadores lógicos y acciones condicionales Comparaciones: I I I a == b (¿a es igual a b?), a < b (¿a es menor que b?), a > b (¿a es mayor que b?), a! = b (¿a es distinto de b?), a <= b (¿a es menor o igual que b?), a >= b (¿a es mayor o igual que b?), Operadores lógicos: I I I !P (negación lógica) P&&Q (P y Q) PkQ (P o Q) Acciones condicionales: if e if-else I Formato básico: if (condicones) { acciones A; } else { acciones B; } I Cuidado: un “;” después de la condición (antes de los “{” y“}”) es equivalente a poner una secuencia de acciones vacı́a. N. Thériault (USACH) Presentación 22 / 28 Bucles while y for Bucles while: I Formato básico: inicialización; while (condicones) { acciones; } I I Más flexibles, no es necesario saber cuantas iteraciones se harán Si hay incrementos, se pueden hacer antes o después de las acciones Bucles for: I Formato básico: for ( inicialización ; condiciones ; incremento ) { acciones; } I I I Más facı́l de seguir para entender el programa y verificar su validez Se sabe cuantas iteraciones hay Siempre hay un incremento, después de las acciones N. Thériault (USACH) Presentación 23 / 28 Sub-funciones e inlining En programas más complejos (de escribir), se puede utilizar sub-funciones o inlining: Crear sub-funciones: I I I Más sencillo de seguir el código Más sencillo para la verificación y el testeo El llamado a funciones aumenta un poco el tiempo de ejecución Inlining manual: I I I I Las operaciones están detalladas a cada utilización Evita llamados a funciones (más eficiente) y “saltos” dentro del programa Aumenta en tamaño del código redactado Requiere cuidado con los nombres de variables, pero puede reutilizar las mismas variables temporarias Inlining automatico: I I I I Recomienda al compilador de hacer el reemplazo automáticamente Se agrega “inline” o “static inline” antes dedefinir/declarar la función Más seguro (evita errores de nombres de variables) Poco control sobre el aumento de tamaño del programa Lo ideal es llegar a un equilibrio entre sub-funciones y inlining: crear funciones cuando se necesitan, sin sub-dividir demasiado. I Se recomienda empezar con sub-funciones para el testeo inicial N. Thériault (USACH) Presentación 24 / 28 Funciones recursivas Una función recursiva es una función que se llama a si misma (una o más veces). Se debe declarar la función primero (sin excepción) No se puede utilizar “inline” Puede ser una herramienta esencial para obtener mejor eficiencia I I Formato natural para algoritmos Reducir-y-Conquistar y Dividir-y-Conquistar Común en algoritmos de Progración dinámica Se deberı́a pensar como si se trataba de una demostración por inducción Debe incluir unas condiciones para evitar recursiones infinitas I I I I Una función recursive siempre tendrá a lo menos un “if” para elegir entre el caso base (sin recursión) y el caso recursivo Los emphcasos recursivos corresponden al paso inductivo en la demostración por inducción Los emphcasos bases corresponden a la eituación bases en la demostración por inducción (donde se inicia la inducción) Se llama a la función misma solamente si se cumple la hipótesis de inducción N. Thériault (USACH) Presentación 25 / 28 Testeo de programas Aunque un algoritmo puede ser bien desarrollado (preparado), con un pseudo-código claro y verificado, varias cosas pueden salir mal al programarlo: Errores de escritura Errores de signos Problemas con el manejo de memorı́a dinámica Conflictos con sub-funciones y/o funciones provenientes de otras librerı́as Interpretación equivocada de de los indices, etc Detalles no considerados en el pseudo-código En algunos casos, la complejidad conceptual (dificultad de entender todos los detalles del programa y tenerlos todos en mente al mismo tiempo), especialmente con programas desarrollados en equipos de trabajo grandes, puede ser muy dificil de asegurar que todo está bien con el programa. Por eso, es importante testear un programa antes de hacerlo oficial. N. Thériault (USACH) Presentación 26 / 28 Consejos para el testeo Empezar con ejemplos pequeños primero: I I Más rápido Idealmente comparar con valores calculados a mano o obtenidos por otros algoritmos (previamente testeados) Testear las (sub-)funciones una por una antes de testearlas juntas I I I Idealmente testear cada función justo después de programarla y antes de empezar a trabajar la próxima Juntar las funciones de a poco Si se detectan errores pero no se sabe donde, mostrar resultados parciales Antes de testear una función recursiva de forma general, se recomiendo testear una versión que hace a lo más un paso de recursión. I I I I I Utilizar una versión de la función que parra después de aplicar el proceso de inducci’on una sola vez Comparar con un algoritmo más básico (e.g. fuerza bruta) Puede ayudar a optimizar la función también (determinar cual es el caso base práctico) Evita entrar en bucles infinitos por errores en en proceso de los pasos Si hay un(os) error(es), es más sencillo identificar donde antes de que las recursiones hagan una difusión del error N. Thériault (USACH) Presentación 27 / 28 Mediciones de tiempo Utilizar clock para medir tiempo (tiempo cpu), no time (tiempo reloj). Según la instalación de C, podrı́an haber otras funciones de medición del tiempo cpu (más precisas), pero no siempre están disponibles. Solo utilizar clock. Hay un nivel mı́nimo de precisión de la medición, no considerar debajo de µs. Para cálculos debajo del minuto, en general el proceso puede compartir el procesador, lo que produce ruido en la medición del tiempo cpu. Para mediciones de mayor precisión, se recomienda repetir el proceso hasta que requiere a lo mı́nimo un minuto, medir el tiempo total y dividir por la cantidad de repeticiones. I Ejemplo: 10, 100, 1000, 104 , ... repeticiones. Idealmente repetir con entradas (aleatorias) distintas, pero generar las entradas aleatorias ANTES de empezar a medir el tiempo. I I Si el algoritmo requiere dos (o más) entradas, preparar un conjunto de valores para cada una y ejecutar para las diferentes combinaciones (requiere menos tiempo y memoria de preparación). Asegurar que las entradas preparadas no agotan el RAM. N. Thériault (USACH) Presentación 28 / 28