]> git.llucax.com Git - z.facultad/75.00/informe.git/blob - source/solucion.rst
Eliminar capítulo de Viabilidad
[z.facultad/75.00/informe.git] / source / solucion.rst
1
2 .. Acá va lo que decidí hacer en base al análisis anterior y sus razones.
3    ESTADO: EMPEZADO
4
5
6 .. _solucion:
7
8 Solución adoptada
9 ============================================================================
10
11 Como hemos visto en :ref:`dgc_bad`, la mejora del recolector de basura puede
12 ser abordada desde múltiples flancos. Por lo tanto, para reducir la cantidad
13 de posibilidades hay que tener en cuenta uno de los principales objetivos de
14 este trabajo: encontrar una solución que tenga una buena probabilidad de ser
15 adoptada por el lenguaje, o alguno de sus compiladores al menos. Para asegurar
16 esto, la solución debe tener un alto grado de aceptación en la comunidad, lo
17 que implica algunos puntos claves:
18
19 * La eficiencia general de la solución no debe ser notablemente peor, en
20   ningún aspecto, que la implementación actual.
21 * Los cambios no deben ser drásticos.
22 * La solución debe atacar de forma efectiva al menos uno de los problemas
23   principales del recolector actual.
24
25 Bajo estos requerimientos, se concluye que probablemente el área más fértil
26 para explorar sea la falta de concurrencia por cumplir todos estos puntos:
27
28 * Si bien hay evidencia en la literatura sobre el incremento del tiempo de
29   ejecución total de ejecución de un programa al usar algoritmos concurrentes,
30   éste no es, en general, muy grande comparativamente.
31 * Existen algoritmos de recolección concurrente que no requieren ningún grado
32   de cooperación por parte del lenguaje o el compilador.
33 * La falta de concurrencia y los largos tiempos de pausa es una de las
34   críticas más frecuentes al recolector actual por parte de la comunidad.
35
36 A pesar de ser la concurrencia la veta principal a explorar en este trabajo,
37 se intenta abordar los demás problemas planteados siempre que sea posible
38 hacerlo sin alejarse demasiado del objetivo principal.
39
40
41 Banco de pruebas
42 ----------------------------------------------------------------------------
43
44 Teniendo en cuenta que uno de los objetivos principales es no empeorar la
45 eficiencia general de forma notable, la confección de un banco de pruebas es
46 un aspecto fundamental, para poder comprobar con cada cambio que la eficiencia
47 final no se vea notablemente afectada.
48
49 La confección de un banco de pruebas no es una tarea trivial, mucho menos para
50 un lenguaje con el nivel de fragmentación que tuvo D_ (que hace que a fines
51 prácticos hayan 3 versiones del lenguaje compitiendo), y cuya masa crítica de
52 usuarios es de aficionados que usualmente abandonan los proyectos, quedando
53 obsoletos rápidamente.
54
55 Con el objetivo de confeccionar este banco de pruebas, desde el comienzo del
56 trabajo se han recolectado (usando como fuente principalmente el grupo de
57 noticias de D_ [#benchmod]_) programas triviales sintetizados con el único
58 propósito de mostrar problemas con el recolector de basura. Otros programas de
59 este estilo fueron escritos explícitamente para este trabajo.
60
61 Además se han recolectado [#benchmod]_ algunos pequeños programas portados de
62 otros lenguajes de programación, que si bien son pequeños y tienen como
63 objetivo ejercitar el recolector de basura, son programas reales que resuelven
64 un problema concreto, lo que otorga un juego de pruebas un poco más amplio que
65 los programas triviales.
66
67 .. [#benchmod] Cabe destacar que en general todos los programas recolectados
68    han sido modificados levemente para ajustarlos mejor a las necesidades del
69    banco de prueba (entre las modificaciones más frecuentes se encuentran la
70    conversión de Phobos_ a Tango_ y la eliminación de mensajes por salida
71    estándar).
72
73 Pero probablemente lo más importante para confeccionar un banco de pruebas
74 verdaderamente útil es disponer de programas reales, que hayan sido diseñados
75 con el único objetivo de hacer su trabajo, sin pensar en como impacta el
76 recolector sobre ellos (ni ellos sobre el recolector). Estos programas proveen
77 las pruebas más realistas y amplias. Desgraciadamente no hay muchos programas
78 reales escritos en D_ disponibles públicamente, y no se encontró en la
79 comunidad tampoco una muestra de voluntad por compartir programas privados
80 para usar como banco de pruebas en este trabajo.
81
82 Por lo tanto el banco de pruebas que se conformó como una mezcla de estas tres
83 grandes categorías.
84
85
86 Pruebas sintetizadas
87 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
88
89 Este es el juego de programas triviales, escritos con el único objetivo de
90 ejercitar un área particular y acotada del recolector.
91
92
93 ``bigarr``
94 ^^^^^^^^^^
95 Su objetivo es ejercitar la manipulación de arreglos de tamaño considerable
96 que almacenan objetos de tamaño pequeño o mediano. Esta prueba fue hallada__
97 en el grupo de noticias de D_ y escrita por Babele Dunnit y aunque
98 originalmente fue concebido para mostrar un problema con la concatenación de
99 arreglos (como se aprecia en la sentencia ``version(loseMemory)``), ejercita
100 los aspectos más utilizados del del recolector: manipulación de arreglos
101 y petición e memoria. Es una de las pruebas que más estresa al recolector ya
102 que todo el trabajo que realiza el programa es utilizar servicios de éste.
103
104 El código fuente del programa es el siguiente::
105
106    const IT = 300;
107    const N1 = 20_000;
108    const N2 = 40_000;
109
110    class Individual
111    {
112       Individual[20] children;
113    }
114
115    class Population
116    {
117       void grow()
118       {
119          foreach (inout individual; individuals)
120             individual = new Individual;
121       }
122       Individual[N1] individuals;
123    }
124
125    version = loseMemory;
126
127    int main(char[][] args)
128    {
129
130       Population testPop1 = new Population;
131       Population testPop2 = new Population;
132       Individual[N2] indi;
133       for (int i = 0; i < IT; i++) {
134          testPop1.grow();
135          testPop2.grow();
136          version (loseMemory) {
137             indi[] = testPop1.individuals ~ testPop2.individuals;
138          }
139          version (everythingOk) {
140             indi[0..N1] = testPop1.individuals;
141             indi[N1..N2] = testPop2.individuals;
142          }
143       }
144       return 0;
145    }
146
147 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=54084
148
149
150 ``concpu`` y ``conalloc``
151 ^^^^^^^^^^^^^^^^^^^^^^^^^
152 Estos dos programas fueron escritos especialmente para este trabajo con el fin
153 de ejercitar la interacción entre el recolector y un *mutator* con varios
154 hilos. La única diferencia entre ellos es que ``concpu`` lanza hilos que hacen
155 trabajar de forma intensiva el procesador pero que no utilizan servicios del
156 recolector, salvo en el hilo principal, mientras que ``conalloc`` utiliza
157 servicios del recolector en todos los hilos lanzados.
158
159 El objetivo de estos programas es medir el impacto de las pausas del
160 recolector. Se espera medir dos tipos de pausa principales, por un lado el
161 tiempo máximo de pausa total, que puede involucrar a más de un hilo y por otro
162 el tiempo de *stop-the-world*, es decir, el tiempo en que los hilos son
163 efectivamente pausados por el recolector para tomar una *foto* de la pila
164 y registros para agregarlos al *root set*.
165
166 Se espera ``concpu`` sea capaz de explotar cualquier reducción en el tiempo de
167 *stop-the-world*, ya que los hilos solo son interrumpidos por este tipo de
168 pausa. Por otro lado, se espera que ``conalloc`` sea afectado por el tiempo
169 máximo de pausa, que podrían sufrir los hilos incluso cuando el *mundo* sigue
170 su marcha, debido al *lock* global del recolector y que los hilos usan
171 servicios de éste.
172
173 El código de ``concpu`` es el siguiente::
174
175    import tango.core.Thread: Thread;
176    import tango.core.Atomic: Atomic;
177    import tango.io.device.File: File;
178    import tango.util.digest.Sha512: Sha512;
179    import tango.util.Convert: to;
180
181    auto N = 100;
182    auto NT = 2;
183    ubyte[] BYTES;
184    Atomic!(int) running;
185
186    void main(char[][] args)
187    {
188       auto fname = args[0];
189       if (args.length > 3)
190          fname = args[3];
191       if (args.length > 2)
192          NT = to!(int)(args[2]);
193       if (args.length > 1)
194          N = to!(int)(args[1]);
195       N /= NT;
196       running.store(NT);
197       BYTES = cast(ubyte[]) File.get(fname);
198       auto threads = new Thread[NT];
199       foreach(ref thread; threads) {
200          thread = new Thread(&doSha);
201          thread.start();
202       }
203       while (running.load()) {
204          auto a = new void[](BYTES.length / 4);
205          a[] = cast(void[]) BYTES[];
206          Thread.yield();
207       }
208       foreach(thread; threads)
209          thread.join();
210    }
211
212    void doSha()
213    {
214       auto sha = new Sha512;
215       for (size_t i = 0; i < N; i++)
216          sha.update(BYTES);
217       running.decrement();
218    }
219
220 El código de ``conalloc`` es igual excepto por la función ``doSha()``, que es
221 de la siguiente manera::
222
223    void doSha()
224    {
225       for (size_t i = 0; i < N; i++) {
226          auto sha = new Sha512;
227          sha.update(BYTES);
228       }
229       running.decrement();
230    }
231
232
233 ``mcore``
234 ^^^^^^^^^
235 Escrito por David Schima y también hallado__ en el grupo de noticias de D_,
236 este programa pretende mostrar como afecta el *lock* global del recolector
237 en ambientes *multi-core*, incluso cuando a simple vista parecen no utilizarse
238 servicios del recolector::
239
240    import tango.core.Thread;
241
242    void main()
243    {
244       enum { nThreads = 4 };
245       auto threads = new Thread[nThreads];
246       foreach (ref thread; threads) {
247          thread = new Thread(&doAppending);
248          thread.start();
249       }
250       foreach (thread; threads)
251          thread.join();
252    }
253
254    void doAppending()
255    {
256       uint[] arr;
257       for (size_t i = 0; i < 1_000_000; i++)
258          arr ~= i;
259    }
260
261 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=103563
262
263 El secreto está en que la concatenación de arreglos utiliza por detrás
264 servicios del recolector, por lo tanto un programa multi-hilo en el cual los
265 hilos (aparentemente) no comparten ningún estado, se puede ver
266 considerablemente afectado por el recolector (siendo este efecto más visible
267 en ambientes *multi-core* por el nivel de sincronización extra que significa
268 a nivel de *hardware*). Cabe destacar que, sin embargo, en Linux_ no es tan
269 notorio.
270
271
272 ``split``
273 ^^^^^^^^^
274 Este programa trivial lee un archivo de texto y genera un arreglo de cadenas
275 de texto resultantes de partir el texto en palabras. Fue escrito por Leonardo
276 Maffi y también hallado__ en el grupo de noticias de D_. Su objetivo era
277 mostrar lo ineficiente que puede ser concatenar datos a un mismo arreglo
278 repetidas veces y ha desembocado en una pequeña `optimización`__ que sirvió
279 para apalear el problema de forma razonablemente efectiva.
280
281 El código es el siguiente::
282
283    import tango.io.device.File: File;
284    import tango.text.Util: delimit;
285    import tango.util.Convert: to;
286
287    int main(char[][] args) {
288       if (args.length < 2)
289          return 1;
290       auto txt = cast(byte[]) File.get(args[1]);
291       auto n = (args.length > 2) ? to!(uint)(args[2]) : 1;
292       if (n < 1)
293          n = 1;
294       while (--n)
295          txt ~= txt;
296       auto words = delimit!(byte)(txt, cast(byte[]) " \t\n\r");
297       return !words.length;
298    }
299
300 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=67673
301 __ http://d.puremagic.com/issues/show_bug.cgi?id=1923
302
303
304 ``rnddata``
305 ^^^^^^^^^^^
306 Este programa fue escrito por Oskar Linde y nuevamente hallado__ en el grupo
307 de noticias. Fue construido para mostrar como el hecho de que el recolector
308 sea conservativo puede hacer que al leer datos binarios hayan muchos *falsos
309 punteros* que mantengan vivas celdas que en realidad ya no deberían ser
310 accesibles desde el *root set* del grafo de conectividad.
311
312 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=46407
313
314 El código del programa es el siguiente::
315
316    import tango.math.random.Random;
317
318    const IT = 125; // number of iterations, each creates an object
319    const BYTES = 1_000_000; // ~1MiB per object
320    const N = 50; // ~50MiB of initial objects
321
322    class C
323    {
324       C c; // makes the compiler not set NO_SCAN
325       long[BYTES/long.sizeof] data;
326    }
327
328    void main() {
329       auto rand = new Random();
330       C[] objs;
331             objs.length = N;
332       foreach (ref o; objs) {
333          o = new C;
334          foreach (ref x; o.data)
335             rand(x);
336       }
337       for (int i = 0; i < IT; ++i) {
338          C o = new C;
339          foreach (ref x; o.data)
340             rand(x);
341          // do something with the data...
342       }
343    }
344
345
346 ``sbtree``
347 ^^^^^^^^^^
348 Este programa está basado en la prueba de nombre ``binary-trees`` de `The
349 Computer Language Benchmarks Game`__, una colección de 12 programas escritos
350 en alrededor de 30 lenguajes de programación para comparar su eficiencia
351 (medida en tiempo de ejecución, uso de memoria y cantidad de líneas de
352 código). De este juego de programas se utilizó solo ``binary-trees`` por ser
353 el único destinado a ejercitar el manejo de memoria. El programa sólo manipula
354 árboles binarios, creándolos y recorriéndolos inmediatamente (no realiza
355 ningún trabajo útil). La traducción a D_ fue realizada por Andrey Khropov
356 y fue hallada__ en el grupo de noticias.
357
358 __ http://shootout.alioth.debian.org/
359 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=43991
360
361 El código fuente es el siguiente::
362
363    import tango.util.Convert;
364    alias char[] string;
365
366    int main(string[] args)
367    {
368       int N = args.length > 1 ? to!(int)(args[1]) : 1;
369       int minDepth = 4;
370       int maxDepth = (minDepth + 2) > N ? minDepth + 2 : N;
371       int stretchDepth = maxDepth + 1;
372       int check = TreeNode.BottomUpTree(0, stretchDepth).ItemCheck;
373       TreeNode longLivedTree = TreeNode.BottomUpTree(0, maxDepth);
374       for (int depth = minDepth; depth <= maxDepth; depth += 2) {
375          int iterations = 1 << (maxDepth - depth + minDepth);
376          check = 0;
377          for (int i = 1; i <= iterations; i++) {
378             check += TreeNode.BottomUpTree(i, depth).ItemCheck;
379             check += TreeNode.BottomUpTree(-i, depth).ItemCheck;
380          }
381       }
382       return 0;
383    }
384
385    class TreeNode
386    {
387       TreeNode left, right;
388       int item;
389
390       this(int item, TreeNode left = null, TreeNode right = null)
391       {
392          this.item = item;
393          this.left = left;
394          this.right = right;
395       }
396
397       static TreeNode BottomUpTree(int item, int depth)
398       {
399          if (depth > 0)
400             return new TreeNode(item,
401                   BottomUpTree(2 * item - 1, depth - 1),
402                   BottomUpTree(2 * item, depth - 1));
403          return new TreeNode(item);
404       }
405
406       int ItemCheck()
407       {
408          if (left)
409             return item + left.ItemCheck() - right.ItemCheck();
410          return item;
411       }
412    }
413
414
415 Programas pequeños
416 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
417
418 Todos los pequeños programas utilizados como parte del banco de prueba
419 provienen del `Olden Benchmark`__ [CAR95]_. Estos programas fueron diseñados
420 para probar el lenguaje de programación Olden__; un lenguaje diseñado para
421 paralelizar programas automáticamente en arquitecturas con memoria
422 distribuida. Son programas relativamente pequeños (entre 400 y 1000 líneas de
423 código fuente cada uno) que realizan una tarea secuencial que aloca
424 estructuras de datos dinámicamente. Las estructuras están usualmente
425 organizadas como listas o árboles, y muy raramente como arreglos. Los
426 programas pasan la mayor parte del tiempo alocando datos y el resto usando los
427 datos alocados, por lo que en general están acotados en tiempo por el uso de
428 memoria (y no de procesador).
429
430 __ http://www.irisa.fr/caps/people/truong/M2COct99/Benchmarks/Olden/Welcome.html
431 __ http://www.martincarlisle.com/olden.html
432
433 La traducción a D_ fue realizada por Leonardo Maffi y están basadas a su vez
434 en la traducción de este juego de pruebas a Java_, JOlden__ [CMK01]_. En
435 general (salvo para el programa ``voronoï``) está disponible el código fuente
436 portado a D_, Java_ y Python_, e incluso varias versiones con distintas
437 optimizaciones para reducir el consumo de tiempo y memoria. Además provee
438 comparaciones de tiempo entre todas ellas. Los programas utilizados en este
439 banco de pruebas son la versión traducida más literalmente de Java_ a D_, ya
440 que hace un uso más intensivo del recolector que las otras versiones.
441
442 __ http://www-ali.cs.umass.edu/DaCapo/benchmarks.html
443
444 A continuación se da una pequeña descripción de cada uno de los 5 programas
445 traducidos y los enlaces en donde encontrar el código fuente (y las
446 comparaciones de tiempos estar disponibles).
447
448
449 ``bh``
450 ^^^^^^
451 Este programa computa las interacciones gravitatorias entre un número
452 :math:`N` de cuerpos en tiempo :math:`O(N log N)` y está basado en árboles
453 heterogéneos de 8 ramas, según el algoritmo descripto por Barnes & Hut
454 [BH86]_.
455
456 Código fuente disponible en:
457 http://www.fantascienza.net/leonardo/js/dolden_bh.zip
458
459
460 ``bisort``
461 ^^^^^^^^^^
462 Este programa ordena :math:`N` números, donde :math:`N` es una potencia de 2,
463 usando un ordenamiento *Bitonic* adaptativo, un algoritmo paralelo óptimo para
464 computadoras con memoria compartida, según describen Bilardi & Nicolau
465 [BN98]_. Utiliza árboles binarios como principal estructuras de datos.
466
467 Código fuente disponible en:
468 http://www.fantascienza.net/leonardo/js/dolden_bisort.zip
469
470
471 ``em3d``
472 ^^^^^^^^
473 Este programa modela la propagación de ondas electromagnéticas a través de
474 objetos en 3 dimensiones. Realiza un cálculo simple sobre un grafo irregular
475 bipartito (implementado utilizando listas simplemente enlazadas) cuyos nodos
476 representan valores de campo eléctrico y magnético. El algoritmo es el
477 descripto por Culler, et al. [CDG93]_.
478
479 Código fuente disponible en:
480 http://www.fantascienza.net/leonardo/js/dolden_em3d.zip
481
482
483 ``tsp``
484 ^^^^^^^
485 Este programa implementa una heurística para resolver el problema del viajante
486 (*traveling salesman problem*) utilizando árboles binarios balanceados. El
487 algoritmo utilizado es el descripto por Karp [KAR77]_.
488
489
490 Código fuente disponible en:
491 http://www.fantascienza.net/leonardo/js/dolden_tsp.zip
492
493
494 ``voronoï``
495 ^^^^^^^^^^^
496 Este programa genera un conjunto aleatorio de puntos y computa su diagrama de
497 Voronoï, una construcción geométrica que permite construir una partición del
498 plano euclídeo, utilizando el algoritmo descripto por Guibas & Stolfi [GS85]_.
499
500 Código fuente disponible en: http://codepad.org/xGDCS3KO
501
502
503 Programas *reales*
504 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
505
506 Dil_ (escrito en su mayor parte por Aziz Köksal y publicado bajo licencia
507 GPL_) es, lamentablemente, el único programa real hallado que, a pesar de
508 estar incompleto, es lo suficientemente grande, mantenido y estable como para
509 ser incluido en el banco de pruebas. Se trata de un compilador de D_ escrito
510 en D_ y está incompleto porque no puede generar código (falta implementar el
511 análisis semántico y la generación de código), por lo que es principalmente
512 utilizado para generar documentación a partir del código.
513
514 El programa está compuesto por:
515
516 * 32.000 líneas de código fuente (aproximadamente).
517 * 86 módulos (o archivos).
518 * 322 diferentes tipos de datos definidos por el usuario, de los cuales 34 son
519   tipos *livianos* (``struct``) y 288 tipos polimórficos (``class``), de los
520   que 260 son subtipos (sub-clases).
521
522 Puede observarse entonces que a pesar de ser incompleto, es una pieza de
523 software bastante compleja y de dimensión considerable.
524
525 Además, al interpretar código fuente se hace un uso intensivo de cadenas de
526 texto que en general presentan problemas muy particulares por poder ser
527 objetos extremadamente pequeños y de tamaños poco convencionales (no múltiplos
528 de palabras, por ejemplo). A su vez, el texto interpretado es convertido a una
529 representación interna en forma de árbol (o *árbol de sintaxis abstracta*)
530 modelado por tipos *livianos* y polimórficos que están organizados en arreglos
531 dinámicos contiguos y asociativos (que usan muchos servicios del recolector),
532 y que finalmente son manipulados para obtener y generar la información
533 necesaria, creando y dejando *morir* objetos constantemente (pero no como única
534 forma de procesamiento, como otras pruebas sintetizadas).
535
536 Por último, a diferencia de muchos otros programas escritos en D_, que dadas
537 algunas de las ineficiencias del recolector invierten mucho trabajo en limitar
538 su uso, este programa no está escrito pensando en dichas limitaciones, por lo
539 que muestra un funcionamiento muy poco sesgado por estas infortunadas
540 circunstancias.
541
542 Por todas estas razones, Dil_ es el ejemplar que tal vez mejor sirve a la hora
543 de medir de forma realista los resultados obtenidos o los avances realizados.
544 Si bien, como se ha dicho anteriormente, las demás pruebas del banco pueden
545 ser útiles para encontrar problemas muy particulares, está es la que da una
546 lectura más cercana a la realidad del uso de un recolector.
547
548
549
550 Modificaciones propuestas
551 ----------------------------------------------------------------------------
552
553 Se decide realizar todas las modificaciones al recolector actual de forma
554 progresiva e incremental, partiendo como base del recolector de la versión
555 0.99.9 de Tango_.  Las razones que motivan esta decisión son varias; por un
556 lado es lo más apropiado dados los requerimientos claves mencionados al
557 principio de este capítulo. Por ejemplo, al hacer cambios incrementales es más
558 fácil comprobar que la eficiencia no se aleja mucho del actual con cada
559 modificación y una modificación gradual impone menos resistencia a la
560 aceptación del nuevo recolector.
561
562 Además la construcción de un recolector de cero es una tarea difícil
563 considerando que un error en el recolector es extremadamente complejo de
564 rastrear, dado que en general el error se detecta en el *mutator* y en una
565 instancia muy posterior al origen real del error. Esto ha sido comprobado de
566 forma práctica, dado que, a modo de ejercicio para interiorizarse en el
567 funcionamiento del *runtime* de D_, primero se ha construido desde cero una
568 implementación de un recolector *naïve*, resultando muy difícil su depuración
569 por las razones mencionadas. Por el contrario, comenzar con un recolector en
570 funcionamiento como base hace más sencillo tanto probar cada pequeña
571 modificación para asegurar que no introduce fallos, como encontrar y reparar
572 los fallos cuando estos se producen, ya que el código incorrecto introducido
573 está bien aislado e identificado.
574
575 A continuación se hace un recorrido sobre cada una de las mejoras propuestas,
576 y en los casos en los que la mejora propone un cambio algorítmico, se analiza
577 la corrección del algoritmo resultante, partiendo de la base de que el
578 algoritmo tomado como punto de partida es un marcado y barrido que utiliza la
579 abstracción tricolor para hacer la fase de marcado de forma iterativa (ver
580 :ref:`gc_mark_sweep` y :ref:`gc_intro_tricolor`), cuya corrección ya está
581 probada en la literatura preexistente.
582
583
584 .. _sol_config:
585
586 Configurabilidad
587 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
588
589 Una de las primeras mejoras propuestas es la posibilidad de configurar el
590 recolector de forma más sencilla. El requerimiento mínimo es la posibilidad de
591 configurar el recolector sin necesidad de recompilarlo. La complejidad de esto
592 surge de que el recolector debe ser transparente para el programa del usuario.
593
594 Configurar el recolector en tiempo de compilación del programa del usuario
595 probablemente requeriría modificar el compilador, y además, si bien es una
596 mejora sustancial a la configuración en tiempo de compilación del recolector,
597 no termina de ser completamente conveniente para realizar pruebas reiteradas
598 con un mismo programa para encontrar los mejores valores de configuración para
599 ese programa en particular.
600
601 Por otro lado, permitir configurar el recolector en tiempo de ejecución, una
602 vez que su estructura interna ya fue definida y creada, puede ser no solo
603 tedioso y complejo, además ineficiente, por lo tanto esta opción también se
604 descarta.
605
606 Finalmente, lo que parece ser más apropiado para un recolector, es permitir la
607 configuración en tiempo de inicialización. Es decir, configurar el recolectar
608 sin necesidad de recompilar ni el programa del usuario ni el recolector, pero
609 antes de que el programa del usuario inicie, de manera que una vez iniciado el
610 recolector con ciertos parámetros, éstos no cambien nunca más en durante la
611 vida del programa.
612
613 Este esquema provee la mejor relación entre configurabilidad, conveniencia,
614 eficiencia y simplicidad. Una posibilidad para lograr esto es utilizar
615 parámetros de línea de comandos, sin embargo no parece ni sencillo (proveer
616 una forma de leer los parámetros de línea de comandos requiere cambios en el
617 *runtime*) ni apropiado (el recolector debería ser lo más transparente posible
618 para el programa del usuario).
619
620 Otra posibilidad es utilizar variables de entorno, que parece ser la opción
621 más sencilla y apropiada. Sencilla porque las variables de entorno pueden ser
622 leídas directamente al inicializar el recolector sin necesidad de cooperación
623 alguna del *runtime*, a través de :manpage:`getenv(3)`. Apropiada porque, si
624 bien el problema de invasión del programa del usuario también existe, es una
625 práctica más frecuente y aceptada la configuración de módulos internos
626 o bibliotecas compartidas a través de variables de entorno.
627
628 Por último, antes de comenzar a usar este esquema de configuración, se
629 verifica que tomar ciertas decisiones en tiempo de ejecución no impacten en la
630 eficiencia del recolector. Para esto se convierten algunas opciones que antes
631 eran solo seleccionables en tiempo de compilación del recolector para que
632 puedan ser seleccionables en tiempo de inicialización y se comprueba que no
633 hay una penalización apreciable.
634
635
636 .. _sol_config_spec:
637
638 Especificación de opciones
639 ^^^^^^^^^^^^^^^^^^^^^^^^^^
640 Para especificar opciones de configuración, hay que hacerlo a través de la
641 variable de entorno de nombre :envvar:`D_GC_OPTS`. El valor de esa variable es
642 interpretado de la siguiente manera (en formato similar a :term:`BNF`):
643
644 .. productionlist::
645    D_GC_OPTS: `option` ( ':' `option` )* <lista de opciones>
646    option: `name` [ '=' `value` ]
647    name: `namec` `namec`*                <nombre de la opción>
648    value: `valuec`*                      <valor de la opción>
649    namec: `valuec` - '='
650    valuec: [0x01-0xFF] - ':'             <cualquiera salvo '\0' y ':'>
651
652 Es decir, se compone de una lista de opciones separadas por **:**. Cada opción
653 se especifica con un nombre, opcionalmente seguido por un valor (separados por
654 **=**).
655
656 El valor de una opción puede ser un texto arbitrario (exceptuando los
657 caracteres ``'\0'`` y ``':'`` y de longitud máxima 255), pero cada opción lo
658 interpreta de forma particular. Como caso general, hay opciones booleanas, que
659 toman como valor verdadero un cualquier número distinto de 0 (o si el valor es
660 vació, es decir, solo se indica el nombre de la opción), y como valor falso
661 cualquier otro texto.
662
663 A continuación se listan las opciones reconocidas por el recolector (indicando
664 el formato del valor de la opción de tener uno especial):
665
666 ``mem_stomp``
667    Esta es una opción (booleana) disponible en el recolector original, pero
668    que se cambia para que sea configurable en tiempo de inicialización
669    (estando desactivada por omisión). Activa la opción ``MEMSTOMP`` descripta
670    en :ref:`dgc_debug`.
671
672 ``sentinel``
673    Esta opción es también booleana (desactivada por omisión), está disponible
674    en el recolector original, y se la cambia para sea configurable en tiempo
675    de inicialización. Activa la opción ``SENTINEL`` descripta en
676    :ref:`dgc_debug`.
677
678 ``pre_alloc``
679    Esta opción permite crear una cierta cantidad de *pools* de un tamaño
680    determinado previo a que inicie el programa. Si se especifica solo un
681    número, se crea un *pool* con ese tamaño en MiB.  Si, en cambio, se
682    especifica una cadena del tipo ``3x1``, el primer número indica la cantidad
683    de *pools* y el segundo el tamaño en MiB de cada uno (3 *pools* de 1MiB en
684    este caso). Ver :ref:`sol_pre_alloc` para más detalles sobre la utilidad de
685    esta opción.
686
687 ``min_free``
688    El valor de esta opción indica el porcentaje mínimo porcentaje del *heap*
689    que debe quedar libre luego de una recolección. Siendo un porcentaje, solo
690    se aceptan valores entre 0 y 100, siendo su valor por omisión 5. Ver
691    :ref:`sol_ocup` para más detalles sobre su propósito.
692
693 ``malloc_stats_file``
694    Esta opción sirve para especificar un archivo en el cual escribir un
695    reporte de todas la operaciones de pedido de memoria realizadas por el
696    programa (durante su tiempo de vida).  Ver :ref:`sol_stats` para más
697    detalles sobre la información provista y el formato del reporte.
698
699 ``collect_stats_file``
700    Esta opción sirve para especificar un archivo en el cual escribir un
701    reporte de todas las recolecciones hechas durante el tiempo de vida del
702    programa.  Ver :ref:`sol_stats` para más detalles sobre la información
703    provista y el formato del reporte.
704
705 ``conservative``
706    Esta opción booleana permite desactivar el escaneo preciso del *heap*,
707    forzando al recolector a ser completamente conservativo (excepto por los
708    bloques con el atributo ``NO_SCAN`` que siguen sin ser escaneados). Ver
709    :ref:`sol_precise` para más detalles sobre la existencia de esta opción.
710
711 ``fork``
712    Esta opción booleana (activada por omisión) permite seleccionar si el
713    recolector debe correr la fase de marcado en paralelo o no (es decir, si el
714    recolector corre de forma concurrente con el *mutator*).  Para más detalles
715    ver :ref:`sol_fork`.
716
717 ``eager_alloc``
718    Esta opción booleana (activada por omisión), sólo puede estar activa si
719    ``fork`` también está activa y sirve para indicar al recolector que reserve
720    un nuevo *pool* de memoria cuando una petición no puede ser satisfecha,
721    justo antes de lanzar la recolección concurrente. Ver
722    :ref:`sol_eager_alloc` para más detalles sobre el propósito de esta opción.
723
724 ``early_collect``
725    Esta opción booleana (desactivada por omisión), también sólo puede estar
726    activa si ``fork`` está activa y sirve para indicar al recolector que lance
727    una recolección (concurrente) antes de que la memoria libre se termine (la
728    recolección temprana será disparada cuando el porcentaje de memoria libre
729    sea menor a ``min_free``). Ver :ref:`sol_early_collect` para más detalles
730    sobre el propósito de esta opción.
731
732 Cualquier opción o valor no reconocido es ignorado por el recolector. Se
733 utilizan los valores por omisión de las opciones que no fueron especificadas,
734 o cuyos valores no pudieron ser interpretados correctamente.
735
736 Para cambiar la configuración del recolector se puede invocar el programa de
737 la siguiente manera (usando un intérprete de comandos del tipo *bourne
738 shell*):
739
740 .. code-block:: none
741
742    D_GC_OPTS=conservative:eager_alloc=0:early_collect=1:pre_alloc=2x5 ./programa
743
744 En este ejemplo, se activan las opciones ``conservative`` y ``early_collect``,
745 se desactiva ``eager_alloc`` y se crean 2 *pools* de 5MiB cada uno al
746 inicializar el recolector.
747
748
749 Reestructuración y cambios menores
750 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
751
752 Si bien se decide no comenzar una implementación desde cero, se ha mostrado
753 (ver :ref:`dgc_bad_code`) que la implementación actual es lo suficientemente
754 desprolija como para complicar su modificación. Es por esto que se hacen
755 algunas reestructuraciones básicas del código, reescribiendo o saneando de
756 forma incremental todas aquellas partes que complican su evolución.
757
758 Además de las modificaciones puramente estéticas (aunque no por eso menos
759 valuables, ya que la legibilidad y simplicidad del código son un factor
760 fundamental a la hora de ser mantenido o extendido), se hacen otras pequeñas
761 mejoras, que se detallan a continuación.
762
763 Remoción de memoria encomendada
764 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
765 Si bien a nivel de eficiencia este cambio no tuvo impacto alguno (cuando en un
766 principio se especuló con que podría dar alguna ganancia en este sentido), se
767 elimina el concepto de memoria *encomendada* para quitar complejidad al
768 código.
769
770 Esta mejora no afecta a la corrección del algoritmo, ya que a nivel lógico el
771 recolector solo ve la memoria *encomendada*.
772
773 Micro-optimizaciones
774 ^^^^^^^^^^^^^^^^^^^^
775 Si bien se realizan varias micro-optimizaciones, probablemente la más
776 relevante es la inclusión de un caché de tamaño de bloque para el método
777 ``findSize()`` de un *pool*. Esto acelera considerablemente las operaciones
778 que necesitan pedir el tamaño de un bloque reiteradamente, por ejemplo, al
779 añadir nuevos elementos a un arreglo dinámico.
780
781 Esta mejora tampoco afecta a la corrección del algoritmo, ya que nuevamente no
782 afecta su comportamiento a nivel lógico, solo cambia detalles en la
783 implementación de forma transparentes para el algoritmo de recolección.
784
785 Optimizaciones sobre ``findPool()``
786 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
787 Al analizar los principales cuellos de botella del recolector, es notoria la
788 cantidad de tiempo que pasa ejecutando la función ``findPool()``, que dado un
789 puntero devuelve el *pool* de memoria al cual pertenece. Es por esto que se
790 minimiza el uso de esta función. Además, dado que los *pools* de memoria están
791 ordenados por el puntero de comienzo del bloque de memoria manejado por el
792 *pool*, se cambia la búsqueda (originalmente lineal) por una búsqueda binaria.
793 Finalmente, dado que la lista de libre está construida almacenando el puntero
794 al siguiente en las mismas celdas que componen la lista, se almacena también
795 el puntero al *pool* al que dicha celda pertenece (dado que la celda más
796 pequeña es de 16 bytes, podemos garantizar que caben dos punteros, incluso
797 para arquitecturas de 64 bits). De esta manera no es necesario usar
798 ``findPool()`` al quitar una celda de la lista de libres.
799
800 Una vez más, la mejora no afecta la corrección del código.
801
802 .. _sol_pre_alloc:
803
804 Pre-asignación de memoria
805 ^^^^^^^^^^^^^^^^^^^^^^^^^
806 Esta opción permite crear una cierta cantidad de *pools* de un tamaño
807 determinado previo a que inicie el programa. Normalmente el recolector no
808 reserva memoria hasta que el programa lo pida. Esto puede llegar a evitar
809 que un programa haga muchas recolecciones al comenzar, hasta que haya
810 cargado su conjunto de datos de trabajo.
811
812 Se han analizado varios valores por omisión pero ninguno es consistentemente
813 mejor que comenzar sin memoria asignada, por lo tanto no se cambia el
814 comportamiento original, pero se agrega una opción (ver ``pre_alloc`` en
815 :ref:`sol_config_spec`) para que el usuario pueda experimentar con cada
816 programa en particular si esta opción es beneficiosa.
817
818 Esta opción tampoco cambia la corrección del algoritmo de recolección, solo
819 sus condiciones iniciales.
820
821 .. _sol_ocup:
822
823 Mejora del factor de ocupación del *heap*
824 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
825 El factor de ocupación del *heap* debe ser apropiado por dos razones. Por un
826 lado, si el *heap* está demasiado ocupado todo el tiempo, serán necesarias
827 muchas recolecciones que, aunque pequeñas dado que la memoria utilizada es
828 poca, puede llegar a ser extremadamente ineficiente en casos patológicos (ver
829 :ref:`dgc_bad_ocup`). Por otro lado, si el tamaño del *heap* es extremadamente
830 grande (en comparación con el tamaño real del grupo de trabajo del programa),
831 se harán pocas recolecciones pero cada una es muy costosa, porque el algoritmo
832 de marcado y barrido es :math:`O(\lvert Heap \rvert)` (ver
833 :ref:`gc_mark_sweep`). Además la afinidad del caché va a ser extremadamente
834 pobre.
835
836 Para mantener el factor de ocupación dentro de límites razonables, se agrega
837 la opción ``min_free`` (ver :ref:`sol_config_spec`). Esta opción indica el
838 recolector cual debe ser el porcentaje mínimo del *heap* que debe quedar libre
839 luego de una recolección. En caso de no cumplirse, se pide más memoria al
840 sistema operativo para cumplir este requerimiento. Además, luego de cada
841 recolección se verifica que el tamaño del *heap* no sea mayor a ``min_free``,
842 para evitar que el *heap* crezca de forma descontrolada. Si es mayor
843 a ``min_free`` se intenta minimizar el uso de memoria liberando *pools* que
844 estén completamente desocupados, mientras que el factor de ocupación siga
845 siendo mayor a ``min_free``. Si liberar un *pool* implica pasar ese límite, no
846 se libera y se pasa a analizar el siguiente y así sucesivamente.
847
848 Esta modificación no afecta a la corrección del algoritmo, ya que no lo afecta
849 directamente.
850
851 Modificaciones descartadas
852 ^^^^^^^^^^^^^^^^^^^^^^^^^^
853 Se realizan varias otras modificaciones, con la esperanza de mejorar la
854 eficiencia del recolector, pero que, al contrario de lo esperado, empeoran la
855 eficiencia o la mejoran de forma muy marginal en comparación con la
856 complejidad agregada.
857
858 Probablemente el caso más significativo, y por tanto el único que vale la pena
859 mencionar, es la conversión de marcado iterativo a marcado recursivo y luego
860 a un esquema híbrido. Como se describe en :ref:`dgc_bad`, el marcado iterativo
861 tiene sus ventajas, pero tiene desventajas también. Al convertirlo a puramente
862 recursivo, se impracticable por resultar en errores de desbordamiento de pila.
863
864 Por lo tanto se prueba con un esquema híbrido, poniendo un límite a la
865 recursividad, volviendo al algoritmo iterativo cuando se alcanza este límite.
866
867 La implementación del algoritmo híbrido consiste en los siguientes cambios
868 sobre el algoritmo original (ver :ref:`dgc_algo_mark`)::
869
870    function mark_phase() is
871       global more_to_scan = false
872       global depth = 0                                // Agregado
873       stop_the_world()
874       clear_mark_scan_bits()
875       mark_free_lists()
876       mark_static_data()
877       push_registers_into_stack()
878       thread_self.stack.end = get_stack_top()
879       mark_stacks()
880       pop_registers_from_stack()
881       mark_user_roots()
882       mark_heap()
883       start_the_world()
884
885    function mark_range(begin, end) is
886       pointer = begin
887       global depth++                                  // Agregado
888       while pointer < end
889          [pool, page, block] = find_block(pointer)
890          if block is not null and block.mark is false
891             block.mark = true
892             if block.noscan is false
893                block.scan = true
894                if (global depth > MAX_DEPTH)          //
895                   more_to_scan = true                 //
896                else                                   // Agregado
897                   foreach ptr in block.words          //
898                      mark(ptr)                        //
899       global depth--                                  //
900
901 Al analizar los resultados de de esta modificación, se observa una mejoría muy
902 level, para valores de ``MAX_DEPTH`` mayores a cero (en algunos casos bastante
903 mayores) y en general para ``MAX_DEPTH`` cero (es decir, usando el algoritmo
904 de forma completamente iterativa) los resultados son peores, dado que se paga
905 el trabajo extra sin ganancia alguna. En la figura :vref:`fig:sol-mark-rec` se
906 puede ver, por ejemplo, el tiempo total de ejecución de Dil_ al generar la
907 documentación completa del código de Tango_, según varía el valor de
908 ``MAX_DEPTH``.
909
910 .. fig:: fig:sol-mark-rec
911
912    Análisis de tiempo total de ejecución en función del valor de
913    ``MAX_DEPTH``.
914
915    Tiempo total de ejecución de Dil_ al generar la documentación completa del
916    código de Tango_ en función del valor de ``MAX_DEPTH``. El rombo no
917    pertenece a ningún nivel de recursividad, representa el tiempo de ejecución
918    del algoritmo original (puramente iterativo).
919
920    .. image:: sol-mark-rec-dil.pdf
921
922
923 Dado que aumentar el nivel máximo de recursividad significa un uso mayor del
924 *stack*, y que esto puede impactar en el usuario (si el usuario tuviera un
925 programa que esté al borde de consumir todo el *stack*, el recolector podría
926 hacer fallar al programa de una forma inesperada para el usuario, problema que
927 sería muy difícil de depurar para éste), y que los resultados obtenidos no son
928 rotundamente superiores a los resultados sin esta modificación, se opta por no
929 incluir este cambio. Tampoco vale la pena incluirlo como una opción con valor
930 por omisión 0 porque, como se ha dicho, para este caso el resultado es incluso
931 peor que sin la modificación.
932
933 Esta modificación mantiene la corrección del recolector dado que tampoco
934 modifica el algoritmo sino su implementación. Además ambos casos extremos son
935 correctos (si ``MAX_DEPTH`` es 0, el algoritmo es puramente iterativo y si
936 pudiera ser infinito resultaría en el algoritmo puramente recursivo).
937
938
939 .. _sol_stats:
940
941 Recolección de estadísticas
942 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
943
944 Un requerimiento importante, tanto para evaluar los resultados de este trabajo
945 como para analizar el comportamiento de los programas estudiados, es la
946 recolección de estadísticas. Hay muchos aspectos que pueden ser analizados
947 a la hora de evaluar un recolector, y es por esto que se busca que la
948 recolección de datos sea lo más completa posible.
949
950 Con este objetivo, se decide recolectar datos sobre lo que, probablemente,
951 sean las operaciones más importantes del recolector: asignación de memoria
952 y recolección.
953
954 Todos los datos recolectados son almacenados en archivos que se especifican
955 a través de opciones del recolector (ver :ref:`sol_config_spec`). Los archivos
956 especificados debe poder ser escritos (y creados de ser necesario) por el
957 recolector (de otra forma se ignora la opción). El conjunto de datos
958 recolectados son almacenados en formato :term:`CSV` en el archivo, comenzando
959 con una cabecera que indica el significado de cada columna.
960
961 Los datos recolectados tienen en general 4 tipos de valores diferentes:
962
963 Tiempo
964    Se guarda en segundos como número de punto flotante (por ejemplo ``0.12``).
965
966 Puntero
967    Se guarda en forma hexadecimal (por ejemplo ``0xa1b2c3d4``).
968
969 Tamaño
970    Se guarda como un número decimal, expresado en bytes (por ejemplo ``32``).
971
972 Indicador
973    Se guarda como el número ``0`` si es falso o ``1`` si es verdadero.
974
975 Esta modificación mantiene la corrección del recolector dado que no hay cambio
976 algorítmico alguno.
977
978 Asignación de memoria
979 ^^^^^^^^^^^^^^^^^^^^^
980 La recolección de datos sobre asignación de memoria se activa asignando un
981 nombre de archivo a la opción ``malloc_stats_file``. Por cada asignación de
982 memoria pedida por el programa (es decir, por cada llamada a la función
983 ``gc_malloc()``) se guarda una fila con los siguientes datos:
984
985 1. Cantidad de segundos que pasaron desde que empezó el programa (*timestamp*).
986 2. Tiempo total que tomó la asignación de memoria.
987 3. Valor del puntero devuelto por la asignación.
988 4. Tamaño de la memoria pedida por el programa.
989 5. Si esta petición de memoria disparó una recolección o no.
990 6. Si debe ejecutarse un *finalizador* sobre el objeto (almacenado en la
991    memoria pedida) cuando ésta no sea más alcanzable (cuando sea barrido).
992 7. Si objeto carece de punteros (es decir, no debe ser escaneada).
993 8. Si objeto no debe ser movido por el recolector.
994 9. Puntero a la información sobre la ubicación de los punteros del objeto.
995 10. Tamaño del tipo del objeto.
996 11. Primera palabra con los bits que indican que palabras del tipo deben ser
997     escaneados punteros y cuales no (en hexadecimal).
998 12. Primera palabra con los bits que indican que palabras del tipo son
999     punteros garantizados (en hexadecimal).
1000
1001 Como puede apreciarse, la mayor parte de esta información sirve más para
1002 analizar el programa que el recolector. Probablemente solo el punto 2 sea de
1003 interés para analizar como se comporta el recolector.
1004
1005 El punto 8 es completamente inútil, ya que el compilador nunca provee esta
1006 información, pero se la deja por si en algún momento comienza a hacerlo. Los
1007 puntos 9 a 12 provee información sobre el tipo del objeto almacenado, útil
1008 para un marcado preciso (ver :ref:`sol_precise`).
1009
1010 El punto 6 indica, indirectamente, cuales de los objetos asignados son
1011 *pesados*, ya que éstos son los únicos que pueden tener un *finalizador*.
1012 Además, a través de los puntos 4 y 10 es posible inferir si lo que va
1013 almacenarse es un objeto solo o un arreglo de objetos.
1014
1015 Recolección de basura
1016 ^^^^^^^^^^^^^^^^^^^^^
1017 Los datos sobre las recolecciones realizadas se guardan al asignar un nombre
1018 de archivo a la opción ``collect_stats_file``. Cada vez que se dispara una
1019 recolección [#solcollect]_ (es decir, cada vez que se llama a la función
1020 ``fullcollect()``) se guarda una fila con los siguientes datos:
1021
1022 1. Cantidad de segundos que pasaron desde que empezó el programa (*timestamp*).
1023 2. Tiempo total que tomó la asignación de memoria que disparó la recolección.
1024 3. Tiempo total que tomó la recolección.
1025 4. Tiempo total que deben pausarse todos los hilos (tiempo de
1026    *stop-the-world*).
1027 5. Cantidad de memoria usada antes de la recolección.
1028 6. Cantidad de memoria libre antes de la recolección.
1029 7. Cantidad de memoria desperdiciada antes de la recolección.
1030 8. Cantidad de memoria utilizada por el mismo recolector antes de la
1031    recolección (para sus estructuras internas).
1032 9. Cantidad de memoria usada después de la recolección.
1033 10. Cantidad de memoria libre después de la recolección.
1034 11. Cantidad de memoria desperdiciada [#solwaste]_ después de la recolección.
1035 12. Cantidad de memoria utilizada por el mismo recolector después de la
1036     recolección.
1037
1038 Si bien el punto 4 parece ser el más importante para un programa que necesita
1039 baja latencia, dado el *lock* global del recolector, el punto 2 es
1040 probablemente el valor más significativo en este aspecto, dado que, a menos
1041 que el programa en cuestión utilice muy poco el recolector en distintos hilos,
1042 los hilos se verán pausados de todas formas cuando necesiten utilizar el
1043 recolector.
1044
1045 .. [#solcollect] Esto es en el sentido más amplio posible. Por ejemplo, cuando
1046    se utiliza marcado concurrente (ver :ref:`sol_fork`), se guarda esta
1047    información incluso si ya hay una recolección activa, pero el tiempo de
1048    pausa de los hilos será -1 para indicar que en realidad nunca fueron
1049    pausados.
1050
1051 .. [#solwaste] Memoria *desperdiciada* se refiere a memoria que directamente
1052    no puede utilizarse debido a la fragmentación. Si por ejemplo, se piden 65
1053    bytes de memoria, dada la organización del *heap* en bloques (ver
1054    :ref:`dgc_org`), el recolector asignará un bloque de 128 bytes, por lo
1055    tanto 63 bytes quedarán desperdiciados.
1056
1057
1058 .. _sol_precise:
1059
1060 Marcado preciso
1061 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1062
1063 En paralelo con este trabajo, David Simcha comienza a explorar la posibilidad
1064 de agregar precisión parcial al recolector, generando información sobre la
1065 ubicación de los punteros para cada tipo [DBZ3463]_. Su trabajo se limita
1066 a una implementación a nivel biblioteca de usuario y sobre `D 2.0`_.
1067 Desafortunadamente su trabajo pasa desapercibido por un buen tiempo.
1068
1069 Luego Vincent Lang (mejor conocido como *wm4* en la comunidad de D_), retoma
1070 este trabajo, pero modificando el compilador DMD_ y trabajando con `D 1.0`_
1071 y Tango_, al igual que este trabajo. Dado el objetivo y entorno común, se abre
1072 la posibilidad de adaptar los cambios de Vincent Lang a este trabajo,
1073 utilizando una versión modificada de DMD_ (dado que los cambios aún no son
1074 integrados al compilador oficial).
1075
1076 Información de tipos provista por el compilador
1077 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1078 Con éstas modificaciones, el compilador en cada asignación le pasa al
1079 recolector información sobre los punteros del tipo para el cual se pide la
1080 memoria. Esta información se pasa como un puntero a un arreglo de palabras con
1081 la estructura mostrada en la figura :vref:`fig:sol-ptrmap` y que se describe
1082 a continuación.
1083
1084 .. fig:: fig:sol-ptrmap
1085
1086    Estructura de la información de tipos provista por el compilador.
1087
1088    .. aafig::
1089       :scale: 110
1090
1091       /----- ptrmap
1092       |
1093       V
1094       +-------------+----------------------------+----------------------------+
1095       | "Tamaño en" |    "Bits indicando si la"  |      "Bits indicando si"   |
1096       | "cantidad"  |  "palabra en una posición" |      "la palabra en una"   |
1097       |    "de"     |    "debe escanearse como"  |          "posición es"     |
1098       | "palabras"  |     "si fuera un puntero"  |          "un puntero"      |
1099       +-------------+----------------------------+----------------------------+
1100
1101       |             |                            |                            |
1102       +----- 1 -----+------- ceil(N/BPW) --------+------- ceil(N/BPW) --------+
1103       |             |                            |                            |
1104
1105 * La primera palabra indica el tamaño, en **cantidad de palabras**, del tipo
1106   para el cual se pide la memoria (:math:`N`).
1107 * Las siguientes :math:`ceil(\frac{N}{BPW})` palabras indican,
1108   como un conjunto de bits, qué palabras deben ser escaneadas por el
1109   recolector como si fueran punteros (donde :math:`BPW` indica la cantidad de
1110   bits por palabra, por ejemplo 32 para x86).
1111 * Las siguientes :math:`ceil(\frac{N}{BPW})` palabras son otro conjunto de
1112   bits indicando qué palabras son realmente punteros.
1113
1114 Los conjuntos de bits guardan la información sobre la primera palabra en el
1115 bit menos significativo. Dada la complejidad de la representación, se ilustra
1116 con un ejemplo. Dada la estructura::
1117
1118    union U {
1119       ubyte ub;
1120       void* ptr;
1121    }
1122
1123    struct S
1124    {
1125       void* begin1;                        // 1 word
1126       byte[size_t.sizeof * 14 + 1] bytes;  // 15 words
1127       // el compilador agrega bytes de "padding" para alinear
1128       void* middle;                        // 1 word
1129       size_t[14] ints;                     // 14 words
1130       void* end1;                          // 1 words
1131       // hasta acá se almacenan los bits en la primera palabra
1132       void* begin2;                        // 1 words
1133       int i;                               // 1 word
1134       U u;                                 // 1 word
1135       S* s;                                // 1 word
1136    }
1137
1138 El compilador genera la estructura que se muestra en la figura
1139 :vref:`fig:sol-ptrmap-example` (asumiendo una arquitectura de 32 bits). Como
1140 puede apreciarse, el miembro ``u``, al ser una unión entre un puntero y un
1141 dato común, el compilador no puede asegurar que lo que se guarda en esa
1142 palabra sea realmente un puntero, pero indica que debe ser escaneado. El
1143 recolector debe debe ser conservativo en este caso, y escanear esa palabra
1144 como si fuera un puntero.
1145
1146 .. fig:: fig:sol-ptrmap-example
1147
1148    Ejemplo de estructura de información de tipos generada para el tipo ``S``.
1149
1150    .. aafig::
1151       :textual:
1152       :aspect: 55
1153       :scale: 110
1154
1155         /---- "bit de 'end1'"
1156         |
1157         |              /---- "bit de 'middle'"
1158         |              |
1159         |    "bits de" |    "bits de"  /---- "bit de 'begin1'"
1160         |     "'ints'" |    "'bytes'"  |
1161         |/------------\|/-------------\|
1162         V|            |V|             |V
1163       +----------------------------------+
1164       | 00000000000000000000000000100100 | "Tamaño en cantidad de palabras (36)"
1165       +==================================+ --\
1166       | 10000000000000010000000000000001 |   | "Bits que indican si hay que"
1167       +----------------------------------+   | "escanear una palabra según"
1168       | 00000000000000000000000000001101 |   | "su posición"
1169       +==================================+ --+
1170       | 10000000000000010000000000000001 |   | "Bits que indican si hay un"
1171       +----------------------------------+   | "puntero en la palabra según"
1172       | 00000000000000000000000000001001 |   | "su posición"
1173       +----------------------------------+ --/
1174         |                          |AAAA
1175         \--------------------------/||||
1176               "bits de relleno"     ||||
1177                                     ||||
1178                  "bit de 's'"       ||||
1179                     |               ||||
1180                     \---------------/||\---- "bit de 'begin2'"
1181                                      ||
1182                      /---------------/\---- "bit de 'i'"
1183                      |
1184                   "bit de 'u'"
1185
1186 Si una implementación quisiera mover memoria (ver :ref:`gc_moving`), debería
1187 mantener inmóvil a cualquier objeto que sea apuntado por una palabra de estas
1188 características, ya que no es seguro actualizar la palabra con la nueva
1189 posición el objeto movido. Es por esta razón que se provee desglosada la
1190 información sobre lo que hay que escanear, y lo que es realmente un puntero
1191 (que puede ser actualizado de forma segura por el recolector de ser
1192 necesario).
1193
1194 Implementación en el recolector
1195 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1196 La implementación está basada en la idea original de David Simcha, pero
1197 partiendo de la implementación de Vincent Lang (que está basada en Tango_)
1198 y consiste en almacenar el puntero a la estructura con la descripción del tipo
1199 generada por el compilador al final del bloque de datos. Este puntero solo se
1200 almacena si el bloque solicitado no tiene el atributo ``NO_SCAN``, dado que en
1201 ese caso no hace falta directamente escanear ninguna palabra del bloque.
1202
1203 En la figura :vref:`fig:sol-ptrmap-blk` se puede ver, como continuación del
1204 ejemplo anterior, como se almacenaría en memoria un objeto del tipo ``S``.
1205
1206 .. fig:: fig:sol-ptrmap-blk
1207
1208    Ejemplo de bloque que almacena un objeto de tipo ``S`` con información de
1209    tipo.
1210
1211    .. aafig::
1212       :scale: 110
1213
1214       |                                                                |
1215       +------------------------ 256 bytes -----------------------------+
1216       |                                                                |
1217
1218       +----------------------------------+-----------------------+-----+
1219       |                                  |                       |     |
1220       | Objeto                           | Desperdicio           | Ptr |
1221       |                                  |                       |     |
1222       +----------------------------------+-----------------------+-----+
1223
1224       |                                  |                       |     |
1225       +------------ 144 bytes -----------+------ 108 bytes ------+- 4 -+
1226       |                                  |                       |     |
1227
1228 Un problema evidente de este esquema es que si el tamaño de un objeto se
1229 aproxima mucho al tamaño de bloque (difiere en menos de una palabra), el
1230 objeto ocupará el doble de memoria.
1231
1232 El algoritmo de marcado se cambia de la siguiente forma::
1233
1234    // Agregado
1235    global conservative_scan = [1, 1, 0]
1236
1237    // Agregado
1238    function must_scan_word(pos, bits) is
1239       return bits[pos / BITS_PER_WORD] & (1 << (pos % BITS_PER_WORD))
1240
1241    function mark_range(begin, end, ptrmap) is             // Modificado
1242       number_of_words_in_type = ptrmap[0]                 // Agregado
1243       size_t* scan_bits = ptrmap + 1                      // Agregado
1244       pointer = begin
1245       while pointer < end
1246          foreach word_pos in 0..number_of_words_in_type   //
1247             if not must_scan_word(n, scan_bits)           // Agregado
1248                continue                                   //
1249             [pool, page, block] = find_block(pointer)
1250             if block is not null and block.mark is false
1251                block.mark = true
1252                if block.noscan is false
1253                   block.scan = true
1254                   global more_to_scan = true
1255          pointer += number_of_words_in_type               // Modificado
1256
1257    function mark_heap() is
1258       while global more_to_scan
1259          global more_to_scan = false
1260          foreach pool in heap
1261             foreach page in pool
1262                if page.block_size <= PAGE // saltea FREE y CONTINUATION
1263                   foreach block in page
1264                      if block.scan is true
1265                         block.scan = false
1266                         if page.block_size is PAGE // obj grande //
1267                            begin = cast(byte*) page              //
1268                            end = find_big_object_end(pool, page) //
1269                         else // objeto pequeño                   //
1270                            begin = block.begin                   //
1271                            end = block.end                       // Modificado
1272                         ptrmap = global conservative_scan        //
1273                         if NO_SCAN not in block.attrs            //
1274                            end -= size_t.sizeof                  //
1275                            ptrmap = cast(size_t*) *end           //
1276                         mark_range(begin, end, ptrmap)           //
1277
1278    function mark_static_data() is
1279       mark_range(static_data.begin, static_data.end,
1280             global conservative_scan)                // Agregado
1281
1282    function mark_stacks() is
1283       foreach thread in threads
1284          mark_range(thread.stack.begin, thread.stack.end,
1285                global conservative_scan)                  // Agregado
1286
1287    function mark_user_roots() is
1288       foreach root_range in user_roots
1289          mark_range(root_range.begin, root_range.end,
1290                global conservative_scan)              // Agregado
1291
1292 Las funciones de asignación de memoria se modifican de forma similar, para
1293 guardar el puntero a la información de tipos. Esta implementación utiliza solo
1294 la información sobre que palabras hay que tratar como punteros (deben ser
1295 escaneadas); la información sobre qué palabras son efectivamente punteros no
1296 se utiliza ya que no se mueven celdas.
1297
1298 El algoritmo sigue siendo correcto, puesto que solamente se dejan de escanear
1299 palabras que el compilador sabe que no pueden ser punteros. Si bien el
1300 lenguaje permite almacenar punteros en una variable que no lo sea, esto es
1301 comportamiento indefinido por lo tanto un programa que lo hace no es
1302 considerado correcto, por lo cual el recolector tampoco debe ser correcto en
1303 esas circunstancias.
1304
1305 Cabe destacar que la información de tipos solo se provee para objetos
1306 almacenados en el *heap*, el área de memoria estática, registros del
1307 procesador y la pila de todos los hilos siguen siendo escaneados de forma
1308 completamente conservativa. Se puede forzar el escaneo puramente conservativo
1309 utilizando la opción ``conservative`` (ver :ref:`sol_config_spec`).
1310
1311
1312 .. _sol_fork:
1313
1314 Marcado concurrente
1315 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1316
1317 Finalmente se procede al objetivo primario de este trabajo, hacer que la fase
1318 más costosa del recolector (el marcado) pueda correr de manera concurrente con
1319 el *mutator*, con el objeto principal de disminuir el tiempo de pausa.
1320
1321 Cabe aclarar, una vez más, que si bien los recolectores concurrentes buscan
1322 disminuir solo el tiempo de *stop-the-world*, en este caso es también
1323 fundamental disminuir el tiempo máximo que está tomado el *lock* global, dado
1324 que ese tiempo puede convertirse en una pausa para todos los threads que
1325 requieran servicios del recolector.
1326
1327 Se decide basar la implementación en el *paper* "Non-intrusive Cloning Garbage
1328 Collector with Stock Operating System Support" [RODR97]_ por las siguientes
1329 razones principales:
1330
1331 * Su implementación encaja de forma bastante natural con el diseño del
1332   recolector actual, por lo que requiere pocos cambios, lo que hace más
1333   factible su aceptación.
1334 * Está basado en la llamada al sistema :manpage:`fork(2)`, que no solo está
1335   muy bien soportada (y de manera muy eficiente) en Linux_, debe estar
1336   soportada en cualquier sistema operativo :term:`POSIX`.
1337 * No necesita instrumentar el código incluyendo barreras de memoria para
1338   informar al recolector cuando cambia el grafo de conectividad. Este es un
1339   aspecto fundamental, dada la filosofía de D_ de no pagar el precio de cosas
1340   que no se usan. La penalización en la eficiencia solo se paga cuando corre
1341   el recolector. Este aspecto también es crítico a la hora de evaluar la
1342   aceptación de la solución por parte de la comunidad.
1343 * Dada su sencillez general, no es difícil ofrecer el algoritmo concurrente
1344   como una opción, de manera que el usuario pueda optar por usarlo o no.
1345
1346 Llamada al sistema *fork*
1347 ^^^^^^^^^^^^^^^^^^^^^^^^^
1348 El término *fork* proviene del inglés y significa *tenedor* de manera textual,
1349 pero se lo utiliza como analogía de una bifurcación. La operación crea una
1350 copia (llamada *hijo*) del proceso que la ejecuta (llamado *padre*).
1351
1352 El punto más importante es que se crea un espacio de direcciones de memoria
1353 separado para el proceso hijo y una copia exacta de todos los segmentos de
1354 memoria del proceso padre. Es por esto que cualquier modificación que se haga
1355 en el proceso padre, no se refleja en el proceso hijo (y viceversa), a menos
1356 que la memoria sea compartida entre los procesos de forma explícita.
1357
1358 Esto, sin embargo, no significa que la memoria física sea realmente duplicada;
1359 en general todos los sistemas operativos modernos (como Linux_) utilizan una
1360 técnica llamada *copy-on-write* (*copiar-al-escribir* en castellano) que
1361 retrasa la copia de memoria hasta que alguno de los dos procesos escribe en un
1362 segmento. Recién en ese momento el sistema operativo realiza la copia de **ese
1363 segmento solamente**. Es por esto que la operación puede ser muy eficiente,
1364 y la copia de memoria es proporcional a la cantidad de cambios que hayan.
1365
1366 :manpage:`fork(2)` tiene otra propiedad importante de mencionar: detiene todos
1367 los hilos de ejecución en el proceso hijo. Es decir, el proceso hijo se crear
1368 con un solo hilo (el hilo que ejecutó la operación de :manpage:`fork(2)`).
1369
1370 Algoritmo
1371 ^^^^^^^^^
1372 Lo que propone el algoritmo es muy sencillo, utilizar la llamada al sistema
1373 :manpage:`fork(2)` para crear una *fotografía* de la memoria del proceso en un
1374 nuevo proceso. En el proceso padre sigue corriendo el *mutator* y en el
1375 proceso hijo se corre la fase de marcado. El *mutator* puede modificar el
1376 grafo de conectividad pero los cambios quedan aislados el hijo (el marcado),
1377 que tiene una visión consistente e inmutable de la memoria. El sistema
1378 operativo duplica las páginas que modifica el padre bajo demanda, por lo tanto
1379 la cantidad de memoria física realmente copiada es proporcional a la cantidad
1380 y dispersión de los cambios que haga el *mutator*.
1381
1382 La corrección del algoritmo se mantiene gracias a que la siguiente invariante
1383 se preserva:
1384
1385    Cuando una celda se convierte en basura, permanece como basura hasta ser
1386    reciclada por el recolector.
1387
1388 Es decir, el *mutator* no puede *resucitar* una celda *muerta* y esta
1389 invariante se mantiene al correr la fase de marcado sobre una vista inmutable
1390 de la memoria. El único efecto introducido es que el algoritmo toma una
1391 aproximación más conservativa. Es decir, lo que sí puede pasar es que una
1392 celda que pasó a estar *muerta* una vez que la fase de marcado se inició, pero
1393 antes de que ésta termine, la celda no se reciclará hasta la próxima
1394 recolección, dado que este algoritmo no incluye una comunicación entre
1395 *mutator* y recolector para notificar cambios en el grafo de conectividad.
1396 Pero esto no afecta la corrección del algoritmo, ya que un recolector es
1397 correcto cuando nunca recicla celdas *vivas*.
1398
1399 La única comunicación necesaria entre el *mutator* y el recolector son los
1400 bits de marcado (ver :ref:`dgc_impl`), dado que la fase de barrido debe correr
1401 en el proceso padre. No es necesaria ningún tipo de sincronización entre
1402 *mutator* y recolector más allá de que uno espera a que el otro finalice.
1403
1404 Además de almacenar el conjunto de bits ``mark`` en memoria compartida entre
1405 el proceso padre e hijo (necesario para la fase de barrido), las
1406 modificaciones necesarias para hacer la fase de marcado concurrente son las
1407 siguientes [#solforkerr]_::
1408
1409    function collect() is
1410       stop_the_world()
1411       child_pid = fork()
1412       fflush(null) // evita que se duplique la salida de los FILE* abiertos
1413       if child_pid is 0 // proceso hijo
1414          mark_phase()
1415          exit(0) // termina el proceso hijo
1416       // proceso padre
1417       start_the_world()
1418       wait(child_pid)
1419       sweep()
1420
1421    function mark_phase() is
1422       global more_to_scan = false
1423       // Borrado: stop_the_world()
1424       clear_mark_scan_bits()
1425       mark_free_lists()
1426       mark_static_data()
1427       push_registers_into_stack()
1428       thread_self.stack.end = get_stack_top()
1429       mark_stacks()
1430       pop_registers_from_stack()
1431       mark_user_roots()
1432       mark_heap()
1433       // Borrado: start_the_world()
1434
1435 Como se puede observar, el cambio es extremadamente sencillo. Sigue siendo
1436 necesario un tiempo mínimo de pausa (básicamente el tiempo que tarda la
1437 llamada al sistema operativo :manpage:`fork(2)`) para guardar una vista
1438 consistente de los registros del CPU y *stacks* de los hilos. Si bien el
1439 conjunto de bits ``mark`` es compartido por el proceso padre e hijo dado que
1440 es necesario para *comunicar* las fases de marcado y barrido, cabe notar que
1441 nunca son utilizados de forma concurrente (la fase de barrido espera que la
1442 fase de marcado termine antes de usar dichos bits), por lo tanto no necesitan
1443 ningún tipo de sincronización y nunca habrá más de una recolección en proceso
1444 debido al *lock* global del recolector.
1445
1446 A pesar de que con estos cambios el recolector técnicamente corre de forma
1447 concurrente, se puede apreciar que para un programa con un solo hilo el
1448 tiempo máximo de pausa seguirá siendo muy grande, incluso más grande que antes
1449 dado el trabajo extra que impone crear un nuevo proceso y duplicar las páginas
1450 de memoria modificadas. Lo mismo le pasará a cualquier hilo que necesite hacer
1451 uso del recolector mientras hay una recolección en proceso, debido al *lock*
1452 global.
1453
1454 Para bajar este tiempo de pausa se experimenta con dos nuevas mejoras, que se
1455 describen a continuación, cuyo objetivo es correr la fase de marcado de forma
1456 concurrente a **todos** los hilos, incluyendo el hilo que la disparó.
1457
1458 .. [#solforkerr] Se omite el manejo de errores y la activación/desactivación
1459    del marcado concurrente a través de opciones del recolector para facilitar
1460    la comprensión del algoritmo y los cambios realizados. Si devuelve con
1461    error la llamada a ``fork()`` o ``waitpid()``, se vuelve al esquema
1462    *stop-the-world* como si se hubiera desactivado el marcado concurrente
1463    utilizando la opción del recolector ``fork=0``.
1464
1465
1466 .. _sol_eager_alloc:
1467
1468 Creación ansiosa de *pools*
1469 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
1470 Esta mejora, que puede ser controlada a través de la opción ``eager_alloc``
1471 (ver :ref:`sol_config_spec`), consiste en crear un nuevo *pool* cuando un
1472 pedido de memoria no puede ser satisfecho, justo después de lanzar la
1473 recolección. Esto permite al recolector satisfacer la petición de memoria
1474 inmediatamente, corriendo la fase de marcado de forma realmente concurrente,
1475 incluso para programas con un solo hilo o programas cuyos hilos usan
1476 frecuentemente servicios del recolector. El precio a pagar es un mayor uso de
1477 memoria de forma temporal (y el trabajo extra de crear y eliminar *pools* más
1478 frecuentemente), pero es esperable que el tiempo máximo de pausa **real** se
1479 vea drásticamente disminuido.
1480
1481 A simple vista las modificaciones necesarias para su implementación parecieran
1482 ser las siguientes::
1483
1484    // Agregado
1485    global mark_pid = 0
1486
1487    // Agregado
1488    function mark_is_running() is
1489       return global mark_pid != 0
1490
1491    function collect() is
1492       if mark_is_running()                      //
1493          finished = try_wait(global mark_pid)   //
1494          if finished                            // Agregado
1495             mark_pid = 0                        //
1496             sweep()                             //
1497          return                                 //
1498       stop_the_world()
1499       child_pid = fork()
1500       fflush(null)
1501       if child_pid is 0 // proceso hijo
1502          mark_phase()
1503          exit(0)
1504       // proceso padre
1505       start_the_world()
1506       // Borrado: wait(child_pid)
1507       global mark_pid = child_pid
1508
1509 Sin embargo con sólo estas modificaciones el algoritmo deja de ser correcto,
1510 ya que tres cosas problemáticas pueden suceder:
1511
1512 1. Puede llamarse a la función ``minimize()`` mientras hay una fase de marcado
1513    corriendo en paralelo. Esto puede provocar que se libere un *pool* mientras
1514    se lo está usando en la fase de marcado, lo que no sería un problema
1515    (porque el proceso de marcado tiene una copia) si no fuera porque los bits
1516    de marcado, que son compartidos por los procesos, se liberan con el *pool*.
1517 2. Si un bloque libre es asignado después de que la fase de marcado comienza,
1518    pero antes de que termine, ese bloque será barrido dado la función
1519    ``rebuild_free_lists()`` puede reciclar páginas si todos sus bloques tienen
1520    el bit ``freebits`` activo (ver :ref:`dgc_algo_sweep`).
1521 3. El *pool* creado ansiosamente, tendrá sus bits de marcado sin activar, por
1522    lo que en la fase de barrido será interpretado como memoria libre, incluso
1523    cuando puedan estar siendo utilizados por el *mutator*.
1524
1525 El punto 1 sencillamente hace que el programa finalice con una violación de
1526 segmento (en el mejor caso) y 2 y 3 pueden desembocar en la liberación de una
1527 celda alcanzable por el *mutator*.
1528
1529 El punto 1 se resuelve a través de la siguiente modificación::
1530
1531    function minimize() is
1532       if mark_is_running()                            // Agregado
1533          return                                       //
1534       for pool in heap
1535          all_free = true
1536          for page in pool
1537             if page.block_size is not FREE
1538                all_free = false
1539                break
1540          if all_free is true
1541             free(pool.pages)
1542             free(pool)
1543             heap.remove(pool)
1544
1545 La resolución del punto 2 es un poco más laboriosa, ya que hay que mantener
1546 actualizado los ``freebits``, de forma que las celdas asignadas después de
1547 empezar la fase de marcado no sean barridas por tener ese bit activo::
1548
1549    function new_big(size) is
1550       number_of_pages = ceil(size / PAGE_SIZE)
1551       pages = find_pages(number_of_pages)
1552       if pages is null
1553          collect()
1554          pages = find_pages(number_of_pages)
1555          if pages is null
1556             minimize()
1557             pool = new_pool(number_of_pages)
1558             if pool is null
1559                return null
1560             pages = assign_pages(pool, number_of_pages)
1561       pages[0].block.free = true                         // Agregado
1562       pages[0].block_size = PAGE
1563       foreach page in pages[1..end]
1564          page.block_size = CONTINUATION
1565       return pages[0]
1566
1567    function assign_page(block_size) is
1568       foreach pool in heap
1569          foreach page in pool
1570             if page.block_size is FREE
1571                page.block_size = block_size
1572                foreach block in page
1573                   block.free = true                         // Agregado
1574                   free_lists[page.block_size].link(block)
1575
1576    function mark_phase() is
1577       global more_to_scan = false
1578       // Borrado: clear_mark_scan_bits()
1579       // Borrado: mark_free_lists()
1580       clear_scan_bits()                         // Agregado
1581       mark_free()                               //
1582       mark_static_data()
1583       push_registers_into_stack()
1584       thread_self.stack.end = get_stack_top()
1585       mark_stacks()
1586       pop_registers_from_stack()
1587       mark_user_roots()
1588       mark_heap()
1589
1590    // Agregado
1591    function clear_scan_bits() is
1592       // La implementación real limpia los bits en bloques de forma eficiente
1593       foreach pool in heap
1594          foreach page in pool
1595             foreach block in page
1596                block.scan = false
1597
1598    // Agregado
1599    function mark_free() is
1600       // La implementación real copia los bits en bloques de forma eficiente
1601       foreach pool in heap
1602          foreach page in pool
1603             foreach block in page
1604                block.mark = block.free
1605
1606    function free_big_object(pool, page) is
1607       pool_end = cast(byte*) pool.pages + (PAGE_SIZE * pool.number_of_pages)
1608       do
1609          page.block_size = FREE
1610          page.block.free = true                 // Agregado
1611          page = cast(byte*) page + PAGE_SIZE
1612       while page < pool_end and page.block_size is CONTINUATION
1613
1614    function new(size, attrs) is
1615       block_size = find_block_size(size)
1616       if block_size < PAGE
1617          block = new_small(block_size)
1618       else
1619          block = new_big(size)
1620       if block is null
1621          throw out_of_memory
1622       if final in attrs
1623          block.final = true
1624       if noscan in attrs
1625          block.noscan = true
1626       block.free = false         // Agregado
1627       return cast(void*) block
1628
1629    funciones new_pool(number_of_pages = 1) is
1630       pool = alloc(pool.sizeof)
1631       if pool is null
1632          return null
1633       pool.number_of_pages = number_of_pages
1634       pool.pages = alloc(number_of_pages * PAGE_SIZE)
1635       if pool.pages is null
1636          free(pool)
1637          return null
1638       heap.add(pool)
1639       foreach page in pool
1640          page.block_size = FREE
1641          foreach block in page      //
1642             block.free = true       // Agregado
1643             block.mark = true       //
1644       return pool
1645
1646 Finalmente, el punto número tres puede ser solucionado con el siguiente
1647 pequeño cambio::
1648
1649    funciones new_pool(number_of_pages = 1) is
1650       pool = alloc(pool.sizeof)
1651       if pool is null
1652          return null
1653       pool.number_of_pages = number_of_pages
1654       pool.pages = alloc(number_of_pages * PAGE_SIZE)
1655       if pool.pages is null
1656          free(pool)
1657          return null
1658       heap.add(pool)
1659       foreach page in pool
1660          page.block_size = FREE
1661          foreach block in page      // Agregado
1662             block.mark = true       //
1663       return pool
1664
1665 La solución es conservativa porque, por un lado evita la liberación de *pools*
1666 mientras haya una recolección en curso (lo que puede hacer que el consumo de
1667 memoria sea un poco mayor al requerido) y por otro asegura que, como se
1668 mencionó anteriormente, los cambios hechos al grafo de conectividad luego de
1669 iniciar la fase de marcado y antes de que ésta termine, no serán detectados
1670 por el recolector hasta la próxima recolección (marcar todos los bloques de
1671 un nuevo *pool* como el bit ``mark`` asegura que que la memoria no sea
1672 recolectada por la fase de barrido cuando termine el marcado).
1673
1674 Estas modificaciones son las que hacen que el algoritmo siga siendo correcto,
1675 asegurando que no se van a liberar celdas *vivas* (a expensas de diferir la
1676 liberación de algunas celdas *muertas* por algún tiempo).
1677
1678
1679 .. _sol_early_collect:
1680
1681 Recolección temprana
1682 ^^^^^^^^^^^^^^^^^^^^
1683 Esta mejora, que puede ser controlada a través de la opción ``early_collect``
1684 (ver :ref:`sol_config_spec`), consiste en lanzar una recolección preventiva,
1685 antes de que una petición de memoria falle. El momento en que se lanza la
1686 recolección es controlado por la opción ``min_free`` (ver :ref:`sol_ocup`).
1687
1688 De esta forma también puede correr de forma realmente concurrente el *mutator*
1689 y el recolector, al menos hasta que se acabe la memoria, en cuyo caso, a menos
1690 que la opción ``eager_alloc`` (ver :ref:`sol_eager_alloc`) también esté
1691 activada, se deberá esperar a que la fase de marcado termine para recuperar
1692 memoria en la fase de barrido.
1693
1694 Para facilitar la comprensión de esta mejora se muestran sólo los cambios
1695 necesarios si no se utiliza la opción ``eager_alloc``::
1696
1697    function collect(early = false) is  // Modificado
1698       if mark_is_running()
1699          finished = try_wait(global mark_pid)
1700          if finished
1701             mark_pid = 0
1702             sweep()
1703             return                     //
1704          else if early                 // Agregado
1705             return                     //
1706       stop_the_world()
1707       child_pid = fork()
1708       fflush(null)
1709       if child_pid is 0 // proceso hijo
1710          mark_phase()
1711          exit(0)
1712       // proceso padre
1713       start_the_world()
1714       if early                         //
1715          global mark_pid = child_pid   //
1716       else                             // Agregado
1717          wait(child_pid)               //
1718          sweep()                       //
1719
1720    // Agregado
1721    function early_collect() is
1722       if not collect_in_progress() and (percent_free < min_free)
1723          collect(true)
1724
1725    function new(size, attrs) is
1726       block_size = find_block_size(size)
1727       if block_size < PAGE
1728          block = new_small(block_size)
1729       else
1730          block = new_big(size)
1731       if block is null
1732          throw out_of_memory
1733       if final in attrs
1734          block.final = true
1735       if noscan in attrs
1736          block.noscan = true
1737       early_collect()               // Agregado
1738       return cast(void*) block
1739
1740 Es de esperarse que cuando no está activa la opción ``eager_alloc`` por un
1741 lado el tiempo de pausa máximo no sea tan chico como cuando sí lo está (dado
1742 que si la recolección no se lanza de forma suficientemente temprana se va
1743 a tener que esperar que la fase de marcado termine), y por otro que se hagan
1744 más recolecciones de lo necesario (cuando pasa lo contrario, se recolecta más
1745 temprano de lo que se debería). Sin embargo, también es de esperarse que el
1746 consumo de memoria sea un poco menor que al usar la opción ``eager_alloc``.
1747
1748 En cuanto a la corrección del algoritmo, éste solamente presenta los problemas
1749 número 1 y 2 mencionados en :ref:`sol_eager_alloc`, dado que jamás se crean
1750 nuevos *pools* y la solución es la ya presentada, por lo tanto el algoritmo
1751 sigue siendo correcto con los cuidados pertinentes.
1752
1753
1754
1755 Resultados
1756 ----------------------------------------------------------------------------
1757
1758 TODO
1759
1760
1761
1762 .. include:: links.rst
1763
1764 .. vim: set ts=3 sts=3 sw=3 et tw=78 spelllang=es :