Depuración y Optimización de Programas

Anuncio
Depuración y Optimización de Programas
Alex Sánchez
12 de marzo de 2008
Índice
1. Introducción
1
2. Depuración y control de errores
2.1. Trazado de la ejecución. Pila de llamadas
2.2. Depuración en R . . . . . . . . . . . . . .
2.2.1. Iniciar el navegador con el error . .
2.2.2. La función trace . . . . . . . . . .
2.3. Manejo de excepciones . . . . . . . . . . .
2.4. Extensiones al sistema básico . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
2
4
5
6
6
8
3. Programación eficiente
3.1. Estilos de programación . . . . . . . . . . . . . . . .
3.2. Programación vectorial y bucles . . . . . . . . . . . .
3.2.1. Uso razonable de los bucles . . . . . . . . . .
3.2.2. Programación vectorial: apply() y compañı́a
3.3. Herramientas para el análisis de la eficiencia . . . . .
3.3.1. Medida del tiempo de ejecución . . . . . . . .
3.3.2. Uso de la memoria . . . . . . . . . . . . . . .
3.4. Analisis del tiempo de ejecución o Profiling . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
8
9
9
9
10
11
11
11
12
1.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Introducción
Como norma general deberı́amos intentar escribir código correcto, limpio y
eficiente desde la primera versión de un programa.
Para ello es conveniente estructurar y encapsular el código, ya sea mediante la modularización, es decir funciones que llaman a otras funciones, o bien
recurriendo a la OOP.
Sin embargo a pesar de todo nuestro código puede contener errores o estar
realizado de forma ineficiente.
En este documento se consideran tres aspectos relacionados con la mejora
de los programas escritos en R .
1
Es muy probabe que nuestro código contenga errores. El proceso de encontrarlos y eliminarlos es la depuración.
Aún cuando no contenga errores es probable que haya maneras de utilizar
R de manera adecuada para que un programa se ejecute de forma más
eficiente. Para ello es de gran utilidad disponer de mecanismos para determinar los ‘cuellos de botella” en los que el código puede pasar mucho
-demasiado- tiempo ejecutándose.
El proceso de saber cuánto tiempo demora la ejecución de un programa
en cada instrucción recibe el nombre de Profiling, y aunque no dice nada
sobre cómo mejorar el código si que indica donde es conveniente invertir
mayores esfuerzos optimizandolo.
A veces una forma sencilla de optimizar es combinar el código R con codigo
compilado en otro lenguaje, por ejemplo en lenguaje C.
2.
Depuración y control de errores
Si un programa contiene errores no es que no sea eficiente, es que no funciona,
o lo que es peor, no hace lo que se espera de él.
En esta sección nos centraremos en la detección de errores que producen la
interrupción de la ejecución, aunque los métodos expuestos pueden también ser
utilizados para la revisión del código y la detección de errores lógicos.
La detección y eliminación de los errores en un programa recibe el nombre
genérico de depuración. La depuración de un programa suele realizarse en dos
pasos:
En primer lugar hay que localizar donde el sistema (R en este caso) ha
detectado el error.
Desde este punto suele ser preciso retroceder hasta encontrar lo que ha
producido el problema.
2.1.
Trazado de la ejecución. Pila de llamadas
Idealmente desearı́amos encontrar que llamada concreta produce un error,
pero esto no siempre resulta fácil o posible. A menudo el problema no lo genera
una lı́nea sino una manipulación anterior de los datos que hacen que esta linea
determinada falle en esta ejecución determinada.
Ası́ pues lo que resulta útil es saber que funciones estan activas en el momento de producirse el error, es decir que funciones estan siendo evaluadas. La
lista de las funciones evaluadas en un momento dado recibe el nombre de pila
de llamada o call stack.
Una vez se ha producido un error la función traceback() permite localizar
donde ha sucedido. Esta función muestra la pila de llamadas que estaba activa
en el momento del error con lo que la última llamada indica en que función se
encontraba esta pila cuando se producjo el error.
2
>
+
+
+
>
+
+
>
>
foo <- function(x) {
print(1)
bar(2)
}
bar <- function(x) {
x + una.variable.que.no.existe
}
foo(2)
traceback()
Ciertos errores, por ejemplo una raı́z cuadrada de un numero negativo o
una división por cero, no son necesariamente tratados como tales, y tan sólo se
produce un mensaje de advertencia (o ni eso segun el valor de options(”warn”)
>
>
+
+
>
+
+
+
+
+
>
+
+
>
+
+
>
options(warn = 1)
discri <- function(a, b, ce) {
return(b^2 - 4 * a * ce)
}
numer <- function(b, disc) {
if (disc == 0)
num <- -b
else num <- c(-b - sqrt(disc), -b + sqrt(disc))
return(num)
}
soluc <- function(a, numereq) {
return(numereq/(2 * a))
}
equac2G <- function(a, b, ce) {
soluc(a, numereq = numer(b, disc = discri(a, b, ce)))
}
equac2G(0, 1, 1)
[1] -Inf
NaN
> equac2G(1, 2, 1)
[1] -1
> equac2G(3, 2, 1)
[1] NaN NaN
Este comportamiento puede modificarse convirtiendo los warnings en errores
lo que permitirá utilizar traceback() para localizar su origen. Para ello debemos
cambiar las opciones de aviso haciendo options(’warn’=2). Si una vez hecho
se repite la llamada a equac2G (3,2,1) se producirá un error y la instrucción
traceback() permitirá localizarlo. Observese que en este caso se recupera también
la redefinición de la opción “warnings”.
3
> options("warn"=2)
> equac2G (3,2,1)
Error en sqrt(disc) : (convertido del aviso) Se han producido NaNs
> traceback()
7: doWithOneRestart(return(expr), restart)
6: withOneRestart(expr, restarts[[1]])
5: withRestarts({
.Internal(.signalCondition(simpleWarning(msg, call), msg,
call))
.Internal(.dfltWarn(msg, call))
}, muffleWarning = function() NULL)
4: .signalSimpleWarning("Se han producido NaNs", quote(sqrt(disc)))
3: numer(b, disc = discri(a, b, ce))
2: soluc(a, numereq = numer(b, disc = discri(a, b, ce)))
1: equac2G(3, 2, 1)
> options("warn"=1)
A veces el error se produce porque de una versión a otra una clase o una
función cambian su comportamiento.
Aunque traceback() permite localizar la llamada que produjo el error puede
no ser obvio a que se debe éste. En éstos casos puede interesarnos reseguir la
ejecución paso a paso, o bien retroceder hacia funciones anteriores.
2.2.
Depuración en R
La depuración en R se basa principalmente en la función browser. Ésta inicia
una ejecución paso a paso y puede invocarse desde dentro de cualquier función.
Es decir si sabemos en que punto puede producirse el error podemos insertar la
llamada antes de éste punto.
>
+
+
+
+
>
>
logit0 <- function(p) {
quoc <- p/(1 - p)
browser()
logQuoc <- log(quoc)
}
logit0(1)
logit0(-1)
La forma más habitual de utilizar la ejecución paso a paso es mediante la
instrucción debug que se aplica a cada funcion que deseamos seguir paso a paso.
> debug(logit0)
> logit0(2)
Cuando se invoque foo entraremos en una ejecución paso a paso que además
nos mostrará cada lı́nea antes de ejecutarla.
Desde dentro del depurador podemos
4
Visualizar cualquier variable global (o si está disponible) local en la función.
Ejecutar instrucciones de R
Ejecutar instrucciones especı́ficas del depurador (haga help(debug) para
más explicaciones.
Las instrucciones de depuracion que muestra la ayuda de R son:
n (or just return). Advance to the next step.
c
continue to the end of the current context: e.g. to the end of
the loop if within a loop or to the end of the function.
'cont'
synonym for 'c'.
'where' print a stack trace of all active function calls.
'Q'
exit the browser and the current evaluation and return to the
top-level prompt.
(Leading and trailing whitespace is ignored, except for return).
' '
' '
Dos puntos importantes:
Para salir del depurador debe escribirse ’Q’
Para que ya no se invoque al depurador debe invertirse la llamada con
undebug().
2.2.1.
Iniciar el navegador con el error
Si se activa la opción de error ’recover’ se entrará en el navegador (’browser’)
cuando se produzca el error sin necesidad de insertar llamadas puntuales a
browse. Observese que al entrar en el navegador se muestra la lista de llamadas.
Al seleccionar una de ellas las variables locales a esta funciçonm estan disponibles
para inspeccionarlas escribiendo su nombre.
ATENCIÓN: No debe confundirse el navegador con el depurador. El primero
permite acceder a las variables locales de cada llamada en la lista. El segundo
permite la ejecución paso a paso.
> options(error = recover, warn = 2)
> equac2G(3, 2, 1)
> options(error = NULL, warn = 1)
Si en vez de ’recover’ se utiliza la opción ’dump.frames’ los marcos de llamada quedaran almacenados en un objeto que podremos revisar con la opción
debugger().
> options(error = dump.frames, warn = 2)
> equac2G(3, 2, 1)
> options(error = NULL, warn = 1)
5
2.2.2.
La función trace
La función trace tiene una funcionalidad similar a la del navegador con la
diferencia que imprime una linea cuando el proceso entra en la función a seguir.
>
>
>
>
options(warn = 2)
trace(numer)
for (i in 1:3) equac2G(i, 2, 1)
options(warn = 1)
2.3.
Manejo de excepciones
En ciertas ocasiones deseamos que el control del error pueda realizarse desde
dentro de la función misma. Esto puede parecer un problema en tanto que si se
produce un error la ejecución se detiene.
El sistema de Control de excepciones permite capturar el error sin que se
interrumpa el programa. Para ello es preciso que el código incluya la llamada
a la función que puede producir el error dentro de la instrucción try. Ésta
retornará el valor de la función que contiene, si no se ha poducido ningún error
y un objeto de tipo ’error’ cuando suceda éste.
La ejecución no se detiene pero la detección de si se ha producido o no un
error corre a cargo del ususario.
>
>
+
+
+
+
+
+
+
+
+
+
>
options(warn = 2)
numerTry <- function(b, disc) {
if (disc == 0)
num <- -b
else {
raiz <- try(sqrt(disc), silent = TRUE)
if (class(raiz) == "try-error")
num <- "Error: Discriminante menor que cero"
else num <- c(-b - raiz, -b + raiz)
}
return(num)
}
numerTry(1, 2)
[1] -2.4142136
0.4142136
> numerTry(1, -1)
[1] "Error: Discriminante menor que cero"
> options(warn = 1)
El siguiente ejemplo nuestra como la función testBiocConnection intenta
determinar si se puede acceder al repositorio de Bioconductor en internet, y si
ello no es posible captura el error y cierra la conexión.
6
> require(Biobase)
> testBioCConnection
function ()
{
curNetOpt <- getOption("internet.info")
on.exit(options(internet.info = curNetOpt), add = TRUE)
options(internet.info = 3)
http <- as.logical(capabilities(what = "http/ftp"))
if (http == FALSE)
return(FALSE)
bioCoption <- getOption("BIOC")
if (is.null(bioCoption))
bioCoption <- "http://www.bioconductor.org"
biocURL <- url(paste(bioCoption, "/main.html", sep = ""))
options(show.error.messages = FALSE)
test <- try(readLines(biocURL)[1])
options(show.error.messages = TRUE)
if (inherits(test, "try-error"))
return(FALSE)
else close(biocURL)
return(TRUE)
}
<environment: namespace:Biobase>
En vez de try se puede utilizar tryCatch que permite gestionar por separado
las acciones a efectuar si se produce un warning o un error.
> foo <- function(x) {
+
if (x < 3)
+
list() + x
+
else {
+
if (x < 10)
+
warning("ouch")
+
else 33
+
}
+ }
> tryCatch(foo(2), error = function(e) "Un error", warning = function(e) "Un aviso",
+
finally = print("Si falla no podremos continuar"))
[1] "Si falla no podremos continuar"
[1] "Un error"
> tryCatch(foo(5), error = function(e) "Un error", warning = function(e) "Un aviso",
+
finally = print("Si falla podremos continuar"))
[1] "Si falla podremos continuar"
[1] "Un aviso"
7
tryCatch(foo(1), finally= print("Si falla nos quedamos aqui..."))
Error en list() + x : argumento no-numérico para operador binario
[1] "Si falla nos quedamos aqui..."
2.4.
Extensiones al sistema básico
Existen diversos paquetes y algunos sitios donde se amplı́a la información y
las posibilidades del sistema de depuración de R .
Aunque no entraremos en detalle se puede destacar:
Algunos documentos populares
ˆ
An Introduction to the Interactive Debugging Tools in R (Roger
Peng)
ˆ
Debugging in R (Debugging in R)
El paquete debug con extensiones basadas en Tcl/Tk para permitir una
depuración “algo más visual”. El uso del paquete se describe en el artı́culo
Debugging without (too many) tears. R News, 3(3):29-32
Este paquete contiene funciones de depuración más potentes que las del
depurador de R permitiendo colocar puntos de ruptura, saltar lineas o ir
a una posición concreta
El paquete codeTools contiene funciones que permiten analizar que otras
funciones se encuentran en uso, que variables estan asignadas o sin utilizar
y utilidades similares.
>
>
>
>
>
>
>
stopifnot(require(debug))
source(file.path("./RCode/buildGOProf.R"))
simpleLLids <- as.character(c(2189, 5575, 5569, 11))
mtrace(GOTermsList)
simpleGOlist <- GOTermsList(simpleLLids, na.rm = F)
mtrace.off()
stopifnot(require(codetools))
3.
Programación eficiente
R es un lenguaje interpretado, lo que suele ser sinónimo de ineficiencia. Esto
debe de matizarse porque aunque es cierto que el mismo código se ejecuta más
rápido compilado que interpretado, ello no sognifica que cualquier alternativa
“compilable” vaya a hacerlo mejor.
A pesar de lo anterior, R se puede considerar poco efiiente en muchos aspectos por lo que puede valer la pena intentar aumentar la eficiencia de nuestro
código.
Lieges (2007) da algunas normas generales para conseguir unos programas
más eficientes:
8
El lector deberı́a considerar la programación vectorial como la ley
El codigo existente y optimizado suele basarse en rutinas FORTRAN o C
ya implementadas. “Mejor no reinventar la rueda”.
Las partes crı́ticas del código pueden escribirse en C o FORTRAN y compilarlas lincándolas a nuestro código a través de bibliotecas.
Si se puede hay que utilizar un “haiga” (lo más potente que “haiga”): Más
memória, más velocidad, más ordenadores, más procesadores ayudaran a
realizar los cálculos más rápidamente.
3.1.
Estilos de programación
Una forma de programar adecuada es el priomer paso hacia un código eficiente. Lieges (2007) da algunos consejos de buen estilo que enumeramos a continuación:
Generalidad Una función debe ser lo más general posible.
Comprensibilidad Hay que documentar al máximo el código realizado.
Legibilidad El código demasiado compacto es difı́cil de entender. Casi tanto
como el demasiado poco compacto.
Estética El código debe ser claro de leer con indentaciones, lineas en blanco
y separaciones apropiadas.
Eficiencia Cuando la ejecución dependa de la velocidad i/o de la memoria
debe de optimizarse, siempre que sea posible, el código que se cree.
3.2.
Programación vectorial y bucles
En general la programación vectorial es mejor que los bucles. Esto no significa
que un bucle sea siempre malo, pero conviene atender algunas reglas.
3.2.1.
Uso razonable de los bucles
A veces es conveniente utilizar un bucle. Por ejemplo ciertas operaciones
vectoriales pueden saturar la memoria, bloqueando o ralentizando el sistema.
Una versión basada en bucles que descomponga el cálculo en partes y utilice
siempre la misma, y menor, fracción de memoria puede ser más adecuada.
Ası́ por ejemplo los bucles pueden resultar más adecuados para cálculos
recursivos que se programen de manera iterativa.
Si se va a utilizar un bucle puede ser conveniente tener en cuenta los siguientes puntos
Es conveniente inicializar un objeto con las dimensiones que se requeriran
para toda la ejecución Es decir es mucho más rápido crear el objeto y
llenarlo, que hacerlo crecer a medida que se crea.
9
>
>
>
+
+
+
>
foo <- function(x) x
n <- 10000
omple1 <- function(n) {
a <- NULL
for (i in 1:n) a <- c(a, foo(i))
}
system.time(omple1(10000))
user
0.21
system elapsed
0.05
0.25
> omple2 <- function(n) {
+
a <- numeric(n)
+
for (i in 1:n) a[i] <- foo(i)
+ }
> system.time(omple2(10000))
user
0.12
system elapsed
0.00
0.12
Dentro de un bucle no deben realizarse comprobaciones innecesarias
Si un cálculo puede desplazarse fuera del bucle situarl allı́
> calcul1 <- function(n) {
+
a <- numeric(n)
+
for (i in 1:n) a[i] <- 2 * n * pi * foo(i)
+ }
> system.time(calcul1(10000))
user
0.15
system elapsed
0.00
0.17
> calcul2 <- function(n) {
+
a <- numeric(n)
+
for (i in 1:n) a[i] <- foo(i)
+
a <- 2 * n * pi * a
+ }
> system.time(calcul2(10000))
user
0.11
3.2.2.
system elapsed
0.00
0.11
Programación vectorial: apply() y compañı́a
La utilización de funciones de la familia apply suele generar código eficiente,
en tanto que se trata de instrucciones vectorizadas.
Obviamente no se trata de decir “Con apply ya es eficiente” sinó de aplicarlo
correctamente en cada caso.
10
3.3.
Herramientas para el análisis de la eficiencia
3.3.1.
Medida del tiempo de ejecución
Para optimizar una función es preciso poder medir el tiempo que tarda en
ejecutarse. Esto puede hacerse con proc.time() y system.time(). La primera
decuelve la hora del sistema al invocarla por lo que se puede llamar antes y
después de la ejecución. La segunda cuenta directamente el tiempo invertido en
el proceso.
>
+
+
+
>
>
>
>
calcul1 <- function(n) {
a <- numeric(n)
for (i in 1:n) a[i] <- 2 * n * pi * foo(i)
}
p1 <- proc.time()
calcul1(10000)
p2 <- proc.time()
p2 - p1
user
0.15
system elapsed
0.00
0.15
> system.time(calcul1(10000))
user
0.17
3.3.2.
system elapsed
0.00
0.17
Uso de la memoria
En R es posible indicar al arrancar como se utilizará la memoria (?Memory
proporciona una explicación aceptable).
A medida que se trabaja suele ser aconsejable “limpiar los restos” que van
quedando en la memoria. Aunque R lo haga de manera automática parece ser
conveniente después que se ha eliminado un objeto grande de la memoria.
> memUsage <- function() {
+
print("memory.size reports the current or maximum memory allocation of the malloc func
+
print("memory.limit reports or increases the limit in force on the total allocation")
+
print(memory.size()/2^20)
+
print(memory.limit()/2^20)
+
gc()
+ }
> memUsage()
[1]
[1]
[1]
[1]
"memory.size reports the current or maximum memory allocation of the malloc function use
"memory.limit reports or increases the limit in force on the total allocation"
0.0002136681
0.003905296
11
used (Mb) gc trigger (Mb) max used (Mb)
Ncells 6140959 164.0
9040660 241.5 6910418 184.6
Vcells 6716555 51.3
19593219 149.5 35778869 273.0
3.4.
Analisis del tiempo de ejecución o Profiling
Las funciones de medida del tiempo tan sólo indican el tiempo transcurrido
del principio al final de una ejecución. Si se desea descubrir en que parte del
código se gasta más tiempo puede recurrirse al sistema de “perfilado” o profiling.
Su uso es muy sencillo.
Antes de iniciar el proceso que se quiere analizar se invoca la función
Rprof(). A intervalos regulares esta medirá el tiempo transcurrido en
cada parte del código.
El proceso finaliza con una llamada a summaryRprof() que nos muestra el
tiempo transcurrido por el programa en cada parte de éste, lo que puede
ayudarnos a descubrir eventuales cuellos de botella.
El resultado de summaryRprof() son dos matrices, la primera ordnada por
“self.time” y la segunda por el tiempo total.
> Rprof()
> mad(runif(1e+07))
[1] 0.3705096
> Rprof(NULL)
> summaryRprof()
$by.self
sort.int
is.na
runif
abs
any
<Anonymous>
doTryCatch
eval.with.vis
evalFunc
mad
processa
Sweave
try
tryCatch
tryCatchList
self.time self.pct total.time total.pct
2.34
35.8
3.06
46.8
1.96
30.0
1.96
30.0
0.92
14.1
0.92
14.1
0.66
10.1
0.66
10.1
0.62
9.5
0.62
9.5
0.04
0.6
0.04
0.6
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
12
tryCatchOne
median
median.default
mean
sort
sort.default
0.00
0.00
0.00
0.00
0.00
0.00
0.0
0.0
0.0
0.0
0.0
0.0
6.54
5.62
4.34
3.06
3.06
3.06
100.0
85.9
66.4
46.8
46.8
46.8
$by.total
<Anonymous>
doTryCatch
eval.with.vis
evalFunc
mad
processa
Sweave
try
tryCatch
tryCatchList
tryCatchOne
median
median.default
sort.int
mean
sort
sort.default
is.na
runif
abs
any
total.time total.pct self.time self.pct
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
6.54
100.0
0.00
0.0
5.62
85.9
0.00
0.0
4.34
66.4
0.00
0.0
3.06
46.8
2.34
35.8
3.06
46.8
0.00
0.0
3.06
46.8
0.00
0.0
3.06
46.8
0.00
0.0
1.96
30.0
1.96
30.0
0.92
14.1
0.92
14.1
0.66
10.1
0.66
10.1
0.62
9.5
0.62
9.5
0.04
0.6
0.04
0.6
$sampling.time
[1] 6.54
13
Descargar