Echando código: Usando ‘referencias débiles’ en Java



Java tiene desde hace tiempo (Java 2 para seer exacto) el concepto de ‘referencias débiles’. Su uso nos puede permitir mejorar el uso de la memoria en nuestra aplicación y en ciertos casos mejorar su desepeño.

Suponga que tenemos un programa que lee desde un archivo de texto una serie de líneas, cada una de ellas con un flujo de caja y una tasa de interés justo al final. El programa toma toda esta información y procede a imprimir el valor neto presente (NPV) por pantalla. Usando la clase ‘BigDecimal’ nos aseguramos de que el cálculo es exacto hasta el último decimal, pero también sabemos esta clase es más lenta que usar un dato de tipo double directamente.

El archivo de ejemplo luce como esto:

   1:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5

2:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
3:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
4:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
5:10000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
6:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
7:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
8:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
9:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
10:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
11:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
12:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
13:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
14:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
15:<....omitiendo lineas repetídas....>
98:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.7
99:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5
100:-1000.0, 500.0, 400.0, 300.0, 200.0, 100.0,0.5

El código de el programa que hace el cálculo:

Y por supuesto en mi estilo perezoso, un pequeño script para llamar a mi programa de Java:

   1:#!/bin/bash

2:declare -r FLAGS=""
3:CLASSPATH="$CLASSPATH:/home/josevnz/java/effective:."
4:exec java -classpath $CLASSPATH $FLAGS com.blogspot.elangelnegro.math.NPV $1 $2

La salida del programa en su primera version. Note los valores de consumo de memoria y tiempo de ejecución:

[josevnz@localhost effective]$ ./NPV.sh NPV.data nc

Used memory: 153928

NPV: -347.3

NPV: -347.3

NPV: -347.3

NPV: -347.3

NPV: 10652.7

….

NPV: -347.3

NPV: -347.3

NPV: -475.5

NPV: -347.3

NPV: -347.3

Used memory: 155688

Execution time: 1030 miliseconds

[josevnz@localhost effective]$

¿Qué se puede hacer para mejorar el desempeño?. Bueno, una teoría dice que si las entradas de una rútina son las mismas y el resultado que se produce es el mismo, entonces pudieramos colocarlo en un caché; El problema aquí es como decidir que tamaño es el adecuado para el caché. Java cuenta con un tipo de referencias ‘debiles’ las cuales son reclamadas por el recolector de basura de la máquina virtual si no hay nadie usandola. En este caso podemos usar un ‘WeakHashMap‘ y le dejamos a Java la tarea de decidir que referencias eliminar (en teoría la eliminación de las referencias ocurre cuando llamamos cualquier método en esta clase).

Lo que hacemos ahora es proporcionarle una referencia a un java.lang.ref.WeakHashMap a un método ‘wrapper’ el cual verificará si el resultado ya fué calculado. De ser así, retornamos el valor inmediatamente, de lo contrario, lo calculamos, lo guardamos en el caché y entonces lo retornamos a quien nos llama:

   1:package com.blogspot.elangelnegro.math;

2:
3:import java.math.BigDecimal;
4:
5:import java.io.IOException;
6:import java.io.FileReader;
7:import java.io.BufferedReader;
8:
9:import java.lang.ref.WeakReference;
10:import java.util.Map;
11:import java.util.HashMap;
12:
13:/**
14: * This class shows how to use the BigDecimal class for numeric calculation that require to be exact.
15: * This particular class implements the formula of present value
16: * <ul>
17: * <li> http://www.investopedia.com/articles/03/101503.asp
18: * <li> http://www.developer.com/java/other/article.php/631281
19: * <li> http://www.developer.com/java/article.php/788311
20: * </ul>
21: * @author Jose Vicente Nunez Zuleta (josevnz@yahoo.com)
22: * @version 0.1
23: */
24:public final class NPV {
25:
26: /**
27: * Constant used on the calculation of the NPV
28: */
29: private static final BigDecimal ONE = new BigDecimal("1.0");
30:
31: /**
32: * A primitive way to calculate the power of a number.
33: * @param number the number to multiply
34: * @param power How many times
35: * @return The number
36: */
37: private static BigDecimal power(BigDecimal number, int power) {
38: BigDecimal powerc = number;
39: for (int j=1; j < power; j++) {
40: powerc = powerc.multiply(number);
41: }
42: return powerc;
43: }
44:
45: private static final IllegalArgumentException ILLEGAL_ARGUMENT_EXCEPTION =
new
IllegalArgumentException();
46:
47: /**
48: * Calculate the Net Present Value for a given set of CashFlows and
a
discount rate
49: * @param cashflows The Cashflows as BigDecimal objects.
cashflow[cashflows.length - 1] is the discount rate
50: * @return The discount rate as a BigDecimal
51: */
52: public static BigDecimal getNPV(String [] cashflows) {
53: BigDecimal pv = new BigDecimal(cashflows[0]);
54: int length = cashflows.length - 1;
55: BigDecimal discountRate = new BigDecimal(cashflows[length]);
56: for (int i=1; i < length; i++) {
57: pv = pv.add(new BigDecimal(cashflows[i]).divide(
58: power(discountRate.add(ONE), i), BigDecimal.ROUND_HALF_EVEN)
59: );
60: }
61: return pv;
62: }
63:
64: /**
65: * Calculate the Net Present Value for a given set of CashFlows and a discount rate.
66: * http://www-106.ibm.com/developerworks/java/library/j-arrays/
67: * @param cashflows The Cashflows as BigDecimal objects. cashflow[cashflows.length
- 1] is the discount rate
68: * @param cache External cache provided to store the results from previous
calculations

69: * @param key An artifically created key to the NPV value. Is faster to
compare
a String than a whole array, so the more help the better...
70: * @return The discount rate as a BigDecimal
71: */
72: public static BigDecimal getNPV(String [] cashflows, Map cache, String key) {
73: WeakReference ref = (WeakReference) cache.get(key);
74: BigDecimal pv = null;
75: if ( ref == null) {
76: pv = getNPV(cashflows);
77: cache.put(key, new WeakReference(pv));
78: } else {
79: pv = (BigDecimal) ref.get();
80: }
81: return pv;
82: }
83:
84: /**
85: * Command line entry point
86: * @param args The location of the data file. Each cashflow is separated
by
a ',' and the last number is the interest rate.
87: * @throws IOException
88: */
89: public static void main(String [] args) throws IOException {
90: BufferedReader reader = null;
91: boolean useCache = false;
//
We use a boolean because we will do repeated comparisons here
92: Runtime runtime = Runtime.getRuntime();
93: long start = System.currentTimeMillis();
94: HashMap cache = null;
95: long end = 0;
96: long linecount = 1;
97: if (! ((args != null) && (args.length == 2))) {
98: throw ILLEGAL_ARGUMENT_EXCEPTION;
99: } else {
100: if (args[1].equals("c")) {
101: useCache = true;
102: cache = new HashMap();
103: } else if (args[1].equals("nc")) {
104: useCache = false;
105: } else {
106: throw ILLEGAL_ARGUMENT_EXCEPTION;
107: }
108: // Show the memory usage before reading the whole file
109: System.out.println("Used memory: " + (runtime.totalMemory() -
runtime.freeMemory()));
110: }
111: try {
112: reader = new BufferedReader(new FileReader(args[0]));
113: for (String line = reader.readLine(); line != null; line =
reader.readLine(), linecount++) {
114: if (! useCache) {
115: System.out.println("NPV: " + getNPV(line.split(",")));
116: } else {
117: System.out.println("NPV: " +
getNPV
(line.split(","), cache, line));
118: }
119: }
120: // Show the memory usage after calculating the memory usage from the file
121: if (cache != null) {
122: cache.clear();
123: }
124: System.gc();
125: System.out.println("Used memory: " + (runtime.totalMemory() -
runtime.freeMemory()));
126: } catch (IOException ioexp) {
127: throw ioexp;
128: } catch (NumberFormatException nfe) {
129: System.err.println("Error at line: " + linecount);
130: throw nfe;
131: } finally {
132: if (reader != null) {
133: try {
134: reader.close();
135: } catch (IOException ignore) {
136: // Empty on purpose
137: }
138: }
139: end = System.currentTimeMillis();
140: System.out.println("Execution time: " + (end-start) + " miliseconds");
141: }
142: }
143:}



La magia ocurre entre las lineas 64-82. Ahora veamos el uso de memoria y los tiempos de ejecución:

[josevnz@localhost effective]$ ./NPV.sh NPV.data c

Used memory: 153944

NPV: -347.3

NPV: -347.3

NPV: -347.3

NPV: -347.3

NPV: 10652.7

NPV: -347.3

NPV: -347.3

NPV: -347.3

NPV: -347.3

NPV: -347.3

NPV: -347.3

….

NPV: -347.3

NPV: -475.5

NPV: -347.3

NPV: -347.3

Used memory: 430184

Execution time: 500 miliseconds

Y note como varia el uso de la memoria se incrementa mucho más y el tiempo de ejecución disminuye (la mitad). Si bien la técnica es útil, note como nunca hay un almuerzo grátis (free lunch).

Espero que la técnica le resulte útil, aqui les dejo un pequeño tutorial y espero sus comentarios por acá 🙂