16 - Temas avanzados (I)

Tutorial creado por Guillem Borrell i Nogueras. Extraido de: http://torroja.dmt.upm.es/%7Eguillem/matlab/
05 de Noviembre de 2006
Este capítulo nace de la necesidad de recojer todos los argumentos no necesariamente ligados al uso de Matlab. La mayoría de ellos están relacionados con la programación general o en cálculo numérico, sin embargo son de gran utilidad para escribir buenos programas. La teoría que contiene este capítulo es de un nivel mucho más elevado al resto, estais avisados; esto no significa que todo esté explicado del modo más sencillo posible.

7.1  Aumentar la calidad del código escrito en Matlab

Que el código funcione no suele ser suficiente. Debemos intentar en cualquier caso escribir código de calidad, debemos convertir en hábitos ciertas prácticas de programación orientadas a hacer más fácil el uso de las funciones y los scripts. No todo termina en escribir una pequeña ayuda en cada función. Hay estrategias muy útiles para aumentar significativamente la potencia del código escrito sin necesidad de aumentar el esfuerzo. Debemos entender que si Matlab es una plataforma de desarrollo rápido de aplicaciones dispondrá de funciones para escribir código de un modo más eficiente.

7.1.1  Vectorizar, la clave para aumentar la velocidad

Hay muchas maneras de asignar un argumento a una variable. Cuando se crearon los ordenadores y empezaron a surgir los lenguajes de programación casi todos los procesos eran escalares. Todo estaba gobernado por operaciones lógicas que operaban unidades muy pequeñas de memoria. A medida que los ordenadores iban creciendo en potencia y versatilidad se empezó a pensar en una manera más eficiente de calcular. Uno de los conceptos era la vectorización.1

Se dice que una operación es escalar cuando se hace elemento a elemento. Una suma escalar de dos vectores es tomar los elementos de cada uno de ellos, sumarlos y asignar el resultado a un tercer vector. Una operación es vectorial cuando se hace por bloques mayores en la memoria. Una suma vectorial de dos vectores sería tomar partes del los vectores o los vectores enteros y sumarlos de golpe.

Los compiladores modernos son capaces de vectorizar automáticamente. Advierten que dos bucles pueden combinarse perfectamente y realizan la operación por bloques ahorrando memoria y tiempo de cálculo. Como Matlab es un programa secuencial carece de esta capacidad de optimización. Si nosotros le pedimos un bucle con operaciones escalares lo va a realizar sin ningún tipo de optimización. Si en cambio asignamos operamos las matrices mediante la notación matricial y las submatrices Matlab sí va a ser capaz de vectorizar la operación.

En la sección
7.1.1.1 explicaremos la importancia que todas estas consideraciones tienen sobre la velocidad de ejecución.

7.1.1.1  El truco más importante de la programación en Matlab

El truco más importante para que nuestros scripts tengan una velocidad aceptable es evitar los bucles con contador. Es la estructura más lenta que existe en el lenguaje. El siguiente ejemplo nos ayudará a entenderlo perfectamente. Crearemos dos matrices de números aleatorios y las sumaremos creando una tercera matriz. Primero lo haremos mediante un bucle que sume con dos índices y luego utilizando el operador suma elemento a elemento. Utilizaremos la función rand para crear las matrices y la pareja tic y toc para calcular el tiempo de cálculo.
a=rand(66) #matriz de 66 x 66
 b=rand(66)
 >> tic;for i=1:66;for j=1:66;c(i,j)=a(i,j)+b(i,j);end;end;toc
 ans = 0.70488
 >> tic;c=a.+b;toc
 ans = 0.0022700
 
Donde el número que obtenemos como resultado es el tiempo transcurrido entre la llamada de tic y la de toc. La diferencia entre los dos métodos es de2:
>> 0.70488/0.00227
 ans = 310.52
 
Utilizar los operadores matriciales y las submatrices generará código del orden de 100 veces más rápido. Para una EDP esto es la diferencia entre un rato de espera y una semana de cálculos, sólo un contador mal puesto puede acabar con un código globalmente bien escrito.

La lentitud de los bucles llega hasta límites insospechados. Supongamos que queremos multiplicar todas las filas de una matriz por un escalar distinto. En un alarde decidimos convertir la serie de números en un vector y utilizar un bucle contador para operar la matriz por filas del siguiente modo:
>> a=1:66;
 >> b=rand(66);
 >> tic;for i=1:66;c(i,:)=a(i)*b(i,:);end;toc
 ans = 0.029491
 
Para eliminar este bucle tenemos que convertir la secuencia de números en una matriz de 66׶6 y luego multiplicarla por una matriz. Qué sorpresa nos llevamos cuando observamos que el tiempo de proceso es menor:
>> tic;c=a'*ones(1,66).*b;toc
 ans = 0.0023100
 
Eliminando un bucle que parecía completamente justificado acabamos de reducir el tiempo de proceso a la décima parte.

A partir de ahora nos lo pensaremos dos veces antes de escribir la palabra for. Si nos acostumbramos pensar con submatrices nos ahorraremos tiempo de cálculo y la engorrosa tarea de migrar código a Fortran inútilmente.

7.1.1.2  ¿Por qué son tan lentos los bucles?

Lo que hace que los bucles sean tan lentos no es únicamente la ausencia de vectorización en el cálculo. Los bucles escalares son muy rápidos sea cual sea la arquitectura y el lenguaje de programación. Si analizamos con un poco más de precisión el código de los ejemplos anteriores observamos que no sólo se están multiplicando dos matrices o dos escalares, además se está reservando la memoria correspondiente al resultado.

Imaginemos que queremos sumar dos vectores y asignar el resultado a un tercero y que para ello utilicemos un bucle. Primero tomaremos el los primeros índices de cada vector y los situaremos en una posición de memoria nueva. Esto sucederá a cada paso con lo que cada iteración implicará una operación de reserva de memoria al final de un vector.

Cada vez que ampliamos un vector llenando una posición vacía Matlab debe comprobar que el elemento no existe, ampliar la memoria reservada al vector para poder situar el nuevo elemento donde debe y rellenar el resto con ceros y finalmente almacenar los datos del nuevo vector.

Cuando sumamos dos vectores escalarmente el ciclo de verificación-reserva -asignación-cierre se realiza una sola vez. Podemos concluir entonces que la operación de ampliación de una matriz en Matlab es especialmente lenta. Aunque no estemos obligados a declarar las variables antes de inicializarlas es siempre una buena práctica comprobar que cada matriz se defina entera o mediante bloques lo suficientemente grandes.

Este comportamiento está ligado al funcionamiento de los arrays en C; un buen texto para comprenderlo mejor es [3] donde encontraremos un capítulo inicial sobre qué es verdaderamente un array y qué relación tiene con un puntero.

Como curiosidad diremos que mientras las operaciones de reserva y liberación de memoria son bastante lentas, las operaciones de manipulación de forma como la función reshape son especialmente rápidas. No debemos tener miedo a cambiar la forma de las matrices según nuestras necesidados pensando que estamos sacrificando tiempo de ejecución.

7.1.2  Control de las variables de entrada y salida en funciones.(+)

La necesidad de pasar una cantidad fija de argumentos a una función en forma de variables no es una limitación para Matlab. Uno de los puntos débiles de la definición de las cabeceras de las funciones es que no pueden definirse, tal como lo hacen otros lenguajes de programación, valores por defecto para las variables de entrada. Matlab cuenta con la siguiente serie de funciones dedicadas a manipular las variables de entrada y salida de las funciones:
nargin
Da el número de argumentos con el que se ha llamado una función
nargoun
Retorna el número de argumentos de salida de una función
varargin
Permite que las funciones admitan cualquier combinación de argumentos de entrada.
varargout
Permite que las funciones adimitan cualquier combinación de argumentos de salida.
inputname
Retorna el nombre de la variable que se ha pasado como argumento de entrada en una función.
Estas funciones son una ayuda esencial cuando escribimos funciones muy polivalentes. Los métodos nargin y nargout sirven para que las funciones se comporten de un modo distinto según la cantidad de argumentos que reciban, varargin y varargout hacen que no tengamos que preocuparnos de escribir largas cabeceras de funciones cuando estas reciben muchos argumentos, es como si recibiera una variable tipo celda de un modo automático.

7.1.3  Comunicación entre el entorno de ejecución global y el entorno de la función

En el léxico utilizado por Matlab se habla de dos entornos de ejecución o workspaces. Existen sólo dos workspaces en los que habitan varibles inicialmente independientes. El modo usual de comunicar los dos entornos es mediante variables globales, una vez definimos una variable como global en todos los workspace la hacemos visible para todas las unidades de programa. Matlab define dos workspace, base y caller. Base es el nombre del entorno de ejecución principal; sería el intérprete en una sesión interactiva. Caller es la función que se esté activa en algún momento de la ejecución. Los dos métodos siguientes son interfaces entre las variables en base y las variables en caller.
evalin
Evalua una variable o una expresión en cualquier entorno.
Por ejemplo, vamos a crear una función que intente capturar una variable del entorno de ejecución principal en una función. Para ello escribiremos la siguiente función:
function out=testfunc()
   out=evalin('base','dummy');
 
Ahora en una sesión del intérprete definiremos la variable var y veremos cómo queda capturada por la sentencia evalin sin que aparezca en la cabecera:
>> testfunc()
 error: `dummy' undefined near line 23 column 1
 error: evaluating assignment expression near line 2, column 4
 error: called from `testfunc'
 
Nos ha dado un error porque aún no hemos definido la variable dummy en el entorno base. Si ahora definimos la variable y llamamos la función:
>> dummy='hola'
 >> testfunc()
 ans = hola
 
Acabamos de comuncar de un modo bastante elegante los dos entornos de ejecución. Los programadores experimentados están acostumbrados a lidiar con los punteros. nos podemos imaginar esta función como una manera razonable de emular el comportamiento de un puntero3 y así añadir algo de potencia a nuestros algoritmos. No será literalmente un puntero porque en vez de apuntar una posición de memoria apuntará a una variable pero como es la manera normal de definir los punteros podemos hacer que se comporte del mismo modo. Por ejemplo, en el caso anterior hemos definido una función que extrae el valor out que ``apunta'' al valor contenido en la variable dummy. ¿Qué sucede si cambiamos la variable dummy? Pues que en tiempo de ejecución la variable out cambiará inmediatamente de valor:
>> dummy='adios'
 dummy = adios
 >> testfunc()
 ans = adios
 
Vemos que esto no es exactamente una asignación de una misma posición de memoria pero la ejecución emula el mismo comportamiento, es como hacer un out==dummy implícito.
assignin
Asigna un valor dado a una variable de cualquier entorno de ejecución.
Estas dos funciones es el método recomendado para establecer una comunicación entre el entorno de ejecución de las funciones y el entorno principal. Se sugiere sacrificar el uso de las variables globales en favor de la asignación y la evaluación entre workspaces. Sin embargo es una sutileza sujeta al estilo de programación. Personalmente encuentro las variables globales mucho más intuitivas.

7.1.4  La función clear

Más adelante en este capítulo, en la sección 7.4.3.2 y por si aún es ajeno a nosotros este concepto, hablaremos de la llamada por valor y la llamada por referencia. Matlab, como C, llama por valor. Esta característica unida a que podemos iniciar variables sin declararlas previamente hace que estemos acumulando memoria en uso. Se llama pérdida de memoria, memory leak al defecto que se presenta cuando una variable inútil no es destruida y la memoria que reserva liberada.

Algunos lenguajes disponen de un recolector automático de basura pero no es el caso de Matlab. Si creamos una función que utiliza variables internas Matlab no liberará la memoria que hayan utilizado cuando termine su ejecución. Si esta función reserva una gran cantidad de memoria es muy importante que nos acordemos de utilizar la función clear para liberar la memoria utilizada. Incluso Fortran, sin recolector de basura y que pasa los argumentos por referencia, no tolera estos errores puesto que manda la memoria reservada en las subrutinas a un stack o un heap; en el caso de desbordarlo da un error en tiempo de ejecución. Las variables verdaderamente grandes requieren una reserva de memoria estática.

En la mayoría de los programas esto no será necesario, hay que trabajar con matrices verdaderamente grandes para ocupar una parte significativa de la memoria de un ordenador moderno. Pero es siempre una práctica muy higiénica liberar la memoria al final de cada función. Al igual que en Fortran, la responsabilidad de que un programa no tenga pérdidas de memoria recae en el programador, no en el lenguaje o sus herramientas.

7.2  Manipulación de bits y Array Masking

En la sección 2.5.1.1 hemos visto que Matlab puede definir argumentos cuyos tipos son números enteros de distinta precisión. Es significativo que la manera de nombrarlos sea precisamente la cantidad de bits que ocupan en memoria; el tipo uint8 es un entero sin signo que ocupa exactamente ocho bits en memoria.

¿Qué tienen los enteros que no tengan los reales? Esta pregunta suele formularse al revés; ahora nos interesa para qué puede servir un entero y no es capaz de hacer un real. Los enteros pueden servir para almacenar cantidades de bits de un modo ordenado. Esto abre la puerta a el uso de funciones que no operan según la aritmética decimal usual sino que manipulan bits de enteros de distinto tipo. Ya no debemos pensar en un entero como la expresión de un número decimal sino que podemos utilizarlo para la descripción de máscaras lógicas, algoritmos genéticos, probabilidad...

Las funciones de manipulación de enteros por bits son las siguientes:
bitand
Adición lógica de dos cadenas de números binarios en su expresión como números enteros
bitor
Sustracción lógica de dos números enteros
bitxor
Operación de ``or'' exclusivo
Ahora supongamos que tenemos una condición lógica compuesta por ocho elementos. Para representarla como un número entero utilizaremos ocho bits. El número total de combinaciones sin repetición de una serie ordenada de ocho con dos opciones es precisamente de 256, el número máximo al que se llega con la representación de entero de ocho bits.

7.2.1  Array masking(+)

Cuando necesitamos controlar el flujo de ejecución de un programa y este flujo necesita ciertas condiciones lógicas solemos utilizar una estructura condicional (if). Cuando dichas condiciones lógicas adquieren un alto grado de complejidad, con más de seis o siete opciones que pueden ser complementarias entre ellas, la implementación de la estructura suele ser harto complicada.

En programación suelen evitarse este tipo de estructuras, son lentas, difíciles de programar, difíciles de entender y propensas a generar errores en tiempo de ejecución que cuesta bastante resolver. En otros lenguajes de programación los defectos de forma suelen ser los más importantes pero ya hemos aprendido que en Matlab es una buena práctica programar con la velocidad en mente.

Una solución especialmente eficiente es ver que los grupos de números binarios son un conjunto no intersectado en los que las operaciones de adición, sustracción y combinación lógicas son triviales. Esto sirve para constuir encima de cada matriz una ``máscara lógica'' que permite asignar una etiqueta a cada elemento de la matriz.

7.3  Introducción al debbugging

La traducción de la palabra debugging es ``quitar los bichos''. Un bug o bicho es un error en el código, sus efectos pueden ser evidentes o sutiles y la tarea de encontrarlos es mucho más complicada que eliminarlos. Los debuggers son programas especiales para esta tarea de uso en lenguajes compilados. Lo que hacen es ejecutar los procesos paso a paso para conocer su funcionamiento de un modo más interactivo. Matlab ya es en sí mismo interactivo pero algunas herramientas del debugging clásico serán útiles en programas muy grandes.

El debugging se basa en los breakpoints que no son más que puntos en los que podemos detener la ejecución del programa para analizar su estado actual. La posición de los breakpoints es más una labor de experiencia que una ley tanto en los lenguajes compilados como interactivos. Solemos poner uno antes de llamar una función y unos cuantos antes de que aparezca el error.

El editor de Matlab es además el interfaz para el debugger. Podremos poner y quitar los breakpoints con el ratón y recuperar el control del proceso con la consola. Pero cuando uno se siente cómodo con el debugging prefiere realizar todo el proceso manualmente mediante las funciones propias. Estas funciones son casi las mismas en Matlab y Octave.
keyboard
Esta palabra clave no es parte del debugging en sentido estricto pero puede ser muy útil para resolver errores del código. Si introducimos esta función en un programa pararemos su ejecución y pasaremos a tener el control en el punto de ejecución donde nos encontremos. Se abrirá un intérprete mediante el cual accederemos al estado actual del programa para poder acceder a las variables de modo interactivo. Una vez salgamos del intérprete continuaremos la ejecución conservando los cambios que hayamos introducido. Este es el modo más sencillo de hacer debugging en scripts porque las funciones para debugging clásicas sólo operan dentro de funciones.
echo
Traducido eco. Controla si los comandos en archivos aparecen o no en pantalla. Esta sentencia sólo tiene efecto dentro de un archivo o de una función. Normalmente los comandos ejecutables de funciones y scripts no aparecen en pantalla, sólo aparece su resultado si no hemos puesto un punto y coma al final de la línea. Con echo on los comandos de los scripts se escriben como si los hubieramos introducido a través del intérprete.

on Activa el eco en los scripts

off Desactiva el eco en los scripts

on all Activa el eco en scripts y funciones

off all Desactiva el eco en scripts y funciones

type
Saca por pantalla el texto correspondiente a cualquier función que esté en el árbol de directorios dentro de un archivo .m. Es útil cuando disponemos de una colección propia de funciones bastante extensa y preferimos no abrir el archivo con el editor.
Como ejemplo del uso de las funciones de debugging utilizaremos el script polyadd.m que implementa la suma de dos polinomios. Las rutinas básicas para el debugging de funciones son:
dbtype
Muestra la función con los números de línea para facilitar la inclusión de breakpoints
Para usarlo con nuestra función sería
>> dbtype polyadd
 1       function poly=polyadd(poly1,poly2)
 2         if (nargin != 2)
 3           usage('polyadd(poly1,poly2)')
 4         end
 5         if (is_vector(poly1)  & & is_vector(poly2))
 6           if length(poly1)<length(poly2)
 7             short=poly1;
 8             long=poly2;
 9           else
 10            short=poly2;
 11            long=poly1;
 12          end
 13          diff=length(long)-length(short);
 14          if diff>0
 15            poly=[zeros(1,diff),short]+long;
 16          else
 17            poly=long+short;
 18          end
 19        else
 20          error('both arguments must be polynomials')
 21        end
 
Ahora queremos colocar dos breakpoints, uno en la línea 14 y otro en la línea 16. Para ello usaremos la siguiente función:
dbstop(func,line)
Introduce un breakpoint en una función.
>> dbstop('polyadd','14')
 ans = 14
 >> dbstop('polyadd','16')
 ans = 17
 
Fijémonos que la función no nos ha dejado poner el breakpoint en la línea 16 porque no es ejecutable. Para comprobar el estado de la función:
dbstatus
Devuelve un vector cuyos elementos son las líneas con breakpoints.
>> dbstatus polyadd
 ans =
   14  17
 
Ahora utilizamos la función del modo usual. La ejecución avanzará hasta que encuentre un breakpoint, entonces se abrirá una consola que nos dará el control de la ejecución.
>> polyadd([3,2,1,3],[3,2,0])
 polyadd: line 14, column 8
 diff
 debug>
 
La consola debug es local, es decir, sólo contiene las variables de la ejecución de la función. Lo más lógico en este punto es utilizar la función who para saber qué variables han sido iniciadas:
debug> who
 *** local user variables:
 __nargin__   argn         long         poly1        short
 __nargout__  diff         poly         poly2
 
Aprovechamos para conocer algunas de ellas:
debug> long
 long =
   3  2  1  3
 debug> poly1
 poly1 =
   3  2  1  3
 debug> poly2
 poly2 =
   3  2  0
 debug> __nargin__
 __nargin__ = 2
 
__nargin__ es el número de argumentos de entrada. Si salimos de la consola avanzaremos hasta el siguiente breakpoint o finalizaremos la ejecución. En este caso llegaremos hasta la línea 17.

Para eliminar alguno de los breakpoints:
dbclear(func,line)
Elimina el breakpoint de la línea solicitada.
>> dbclear('polyadd','17')
 polyadd
 symbol_name = polyadd
 >> dbstatus polyadd
 ans = 14
 
Hay más comandos para debugging pero estos son los más importantes.

11 opiniones

Enzo

buenazo
Introducción informal a Matlab y Octave

un excelente materaial
^^ bueno el tutorial >_<

es bueno para uno q esta aprendiendo a aprender a manejarlo
Esta bien bueno y claro ese tutorial.

De verdad es una ayuda para el que empieza a manejar matlab.
Ing. Civil.

Bastante completo y didactico.
1 2 3 | siguiente >

Tutoriales relacionados con 'Introducción informal a Matlab y Octave'

Hay muchos libros de Matlab, algunos muy buenos, pero en ninguno es tratado como un... Más »

Autor y licencia de 'Introducción informal a Matlab y Octave'


Tutorial de Guillem Borrell i Nogueras. Extraido de: http://torroja.dmt.upm.es/%7Eguillem/matlab/ CopyLeft
Este contenido ha sido recopilado por el equipo de Wikilearning. Todo el contenido recopilado se ha obtenido respetando y comunicando en nuestro site la licencia de cada fuente.
Wikilearning tiene permiso expreso por escrito de los autores para publicar los contenidos que ha extraído de otras webs, incluyendo su uso comercial.