2 .. Acá va lo que decidí hacer en base al análisis anterior y sus razones.
9 ============================================================================
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:
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.
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:
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.
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.
42 ----------------------------------------------------------------------------
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.
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.
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.
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.
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
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.
82 Por lo tanto el banco de pruebas que se conformó como una mezcla de estas tres
87 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
89 Este es el juego de programas triviales, escritos con el único objetivo de
90 ejercitar un área particular y acotada del recolector.
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.
104 El código fuente del programa es el siguiente::
112 Individual[20] children;
119 foreach (inout individual; individuals)
120 individual = new Individual;
122 Individual[N1] individuals;
125 version = loseMemory;
127 int main(char[][] args)
130 Population testPop1 = new Population;
131 Population testPop2 = new Population;
133 for (int i = 0; i < IT; i++) {
136 version (loseMemory) {
137 indi[] = testPop1.individuals ~ testPop2.individuals;
139 version (everythingOk) {
140 indi[0..N1] = testPop1.individuals;
141 indi[N1..N2] = testPop2.individuals;
147 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=54084
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.
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*.
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
173 El código de ``concpu`` es el siguiente::
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;
184 Atomic!(int) running;
186 void main(char[][] args)
188 auto fname = args[0];
192 NT = to!(int)(args[2]);
194 N = to!(int)(args[1]);
197 BYTES = cast(ubyte[]) File.get(fname);
198 auto threads = new Thread[NT];
199 foreach(ref thread; threads) {
200 thread = new Thread(&doSha);
203 while (running.load()) {
204 auto a = new void[](BYTES.length / 4);
205 a[] = cast(void[]) BYTES[];
208 foreach(thread; threads)
214 auto sha = new Sha512;
215 for (size_t i = 0; i < N; i++)
220 El código de ``conalloc`` es igual excepto por la función ``doSha()``, que es
221 de la siguiente manera::
225 for (size_t i = 0; i < N; i++) {
226 auto sha = new Sha512;
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::
240 import tango.core.Thread;
244 enum { nThreads = 4 };
245 auto threads = new Thread[nThreads];
246 foreach (ref thread; threads) {
247 thread = new Thread(&doAppending);
250 foreach (thread; threads)
257 for (size_t i = 0; i < 1_000_000; i++)
261 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=103563
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
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.
281 El código es el siguiente::
283 import tango.io.device.File: File;
284 import tango.text.Util: delimit;
285 import tango.util.Convert: to;
287 int main(char[][] args) {
290 auto txt = cast(byte[]) File.get(args[1]);
291 auto n = (args.length > 2) ? to!(uint)(args[2]) : 1;
296 auto words = delimit!(byte)(txt, cast(byte[]) " \t\n\r");
297 return !words.length;
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
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.
312 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=46407
314 El código del programa es el siguiente::
316 import tango.math.random.Random;
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
324 C c; // makes the compiler not set NO_SCAN
325 long[BYTES/long.sizeof] data;
329 auto rand = new Random();
332 foreach (ref o; objs) {
334 foreach (ref x; o.data)
337 for (int i = 0; i < IT; ++i) {
339 foreach (ref x; o.data)
341 // do something with the data...
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.
358 __ http://shootout.alioth.debian.org/
359 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=43991
361 El código fuente es el siguiente::
363 import tango.util.Convert;
366 int main(string[] args)
368 int N = args.length > 1 ? to!(int)(args[1]) : 1;
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);
377 for (int i = 1; i <= iterations; i++) {
378 check += TreeNode.BottomUpTree(i, depth).ItemCheck;
379 check += TreeNode.BottomUpTree(-i, depth).ItemCheck;
387 TreeNode left, right;
390 this(int item, TreeNode left = null, TreeNode right = null)
397 static TreeNode BottomUpTree(int item, int depth)
400 return new TreeNode(item,
401 BottomUpTree(2 * item - 1, depth - 1),
402 BottomUpTree(2 * item, depth - 1));
403 return new TreeNode(item);
409 return item + left.ItemCheck() - right.ItemCheck();
416 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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).
430 __ http://www.irisa.fr/caps/people/truong/M2COct99/Benchmarks/Olden/Welcome.html
431 __ http://www.martincarlisle.com/olden.html
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.
442 __ http://www-ali.cs.umass.edu/DaCapo/benchmarks.html
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).
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
456 Código fuente disponible en:
457 http://www.fantascienza.net/leonardo/js/dolden_bh.zip
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.
467 Código fuente disponible en:
468 http://www.fantascienza.net/leonardo/js/dolden_bisort.zip
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]_.
479 Código fuente disponible en:
480 http://www.fantascienza.net/leonardo/js/dolden_em3d.zip
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]_.
490 Código fuente disponible en:
491 http://www.fantascienza.net/leonardo/js/dolden_tsp.zip
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]_.
500 Código fuente disponible en: http://codepad.org/xGDCS3KO
504 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
514 El programa está compuesto por:
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).
522 Puede observarse entonces que a pesar de ser incompleto, es una pieza de
523 software bastante compleja y de dimensión considerable.
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).
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
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.
550 Modificaciones propuestas
551 ----------------------------------------------------------------------------
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.
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.
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.
587 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
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.
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
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
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).
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.
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.
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`):
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 ':'>
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
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.
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):
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
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
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
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.
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.
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.
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.
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
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.
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.
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.
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
742 D_GC_OPTS=conservative:eager_alloc=0:early_collect=1:pre_alloc=2x5 ./programa
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.
749 Reestructuración y cambios menores
750 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
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.
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
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*.
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.
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.
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.
800 Una vez más, la mejora no afecta la corrección del código.
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.
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.
818 Esta opción tampoco cambia la corrección del algoritmo de recolección, solo
819 sus condiciones iniciales.
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
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.
848 Esta modificación no afecta a la corrección del algoritmo, ya que no lo afecta
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.
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.
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.
867 La implementación del algoritmo híbrido consiste en los siguientes cambios
868 sobre el algoritmo original (ver :ref:`dgc_algo_mark`)::
870 function mark_phase() is
871 global more_to_scan = false
872 global depth = 0 // Agregado
874 clear_mark_scan_bits()
877 push_registers_into_stack()
878 thread_self.stack.end = get_stack_top()
880 pop_registers_from_stack()
885 function mark_range(begin, end) is
887 global depth++ // Agregado
889 [pool, page, block] = find_block(pointer)
890 if block is not null and block.mark is false
892 if block.noscan is false
894 if (global depth > MAX_DEPTH) //
895 more_to_scan = true //
897 foreach ptr in block.words //
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
910 .. fig:: fig:sol-mark-rec
912 Análisis de tiempo total de ejecución en función del valor de
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).
920 .. image:: sol-mark-rec-dil.pdf
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.
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).
941 Recolección de estadísticas
942 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
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
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.
961 Los datos recolectados tienen en general 4 tipos de valores diferentes:
964 Se guarda en segundos como número de punto flotante (por ejemplo ``0.12``).
967 Se guarda en forma hexadecimal (por ejemplo ``0xa1b2c3d4``).
970 Se guarda como un número decimal, expresado en bytes (por ejemplo ``32``).
973 Se guarda como el número ``0`` si es falso o ``1`` si es verdadero.
975 Esta modificación mantiene la corrección del recolector dado que no hay cambio
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:
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).
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.
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`).
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.
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:
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
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
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
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
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.
1061 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
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).
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
1084 .. fig:: fig:sol-ptrmap
1086 Estructura de la información de tipos provista por el compilador.
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 +-------------+----------------------------+----------------------------+
1102 +----- 1 -----+------- ceil(N/BPW) --------+------- ceil(N/BPW) --------+
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.
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::
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
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.
1146 .. fig:: fig:sol-ptrmap-example
1148 Ejemplo de estructura de información de tipos generada para el tipo ``S``.
1155 /---- "bit de 'end1'"
1157 | /---- "bit de 'middle'"
1159 | "bits de" | "bits de" /---- "bit de 'begin1'"
1160 | "'ints'" | "'bytes'" |
1161 |/------------\|/-------------\|
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 +----------------------------------+ --/
1175 \--------------------------/||||
1176 "bits de relleno" ||||
1180 \---------------/||\---- "bit de 'begin2'"
1182 /---------------/\---- "bit de 'i'"
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
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.
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``.
1206 .. fig:: fig:sol-ptrmap-blk
1208 Ejemplo de bloque que almacena un objeto de tipo ``S`` con información de
1215 +------------------------ 256 bytes -----------------------------+
1218 +----------------------------------+-----------------------+-----+
1220 | Objeto | Desperdicio | Ptr |
1222 +----------------------------------+-----------------------+-----+
1225 +------------ 144 bytes -----------+------ 108 bytes ------+- 4 -+
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.
1232 El algoritmo de marcado se cambia de la siguiente forma::
1235 global conservative_scan = [1, 1, 0]
1238 function must_scan_word(pos, bits) is
1239 return bits[pos / BITS_PER_WORD] & (1 << (pos % BITS_PER_WORD))
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
1246 foreach word_pos in 0..number_of_words_in_type //
1247 if not must_scan_word(n, scan_bits) // Agregado
1249 [pool, page, block] = find_block(pointer)
1250 if block is not null and block.mark is false
1252 if block.noscan is false
1254 global more_to_scan = true
1255 pointer += number_of_words_in_type // Modificado
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
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) //
1278 function mark_static_data() is
1279 mark_range(static_data.begin, static_data.end,
1280 global conservative_scan) // Agregado
1282 function mark_stacks() is
1283 foreach thread in threads
1284 mark_range(thread.stack.begin, thread.stack.end,
1285 global conservative_scan) // Agregado
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
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.
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.
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`).
1315 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
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.
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:
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.
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*).
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.
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.
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)`).
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*.
1382 La corrección del algoritmo se mantiene gracias a que la siguiente invariante
1385 Cuando una celda se convierte en basura, permanece como basura hasta ser
1386 reciclada por el recolector.
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*.
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.
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]_::
1409 function collect() is
1412 fflush(null) // evita que se duplique la salida de los FILE* abiertos
1413 if child_pid is 0 // proceso hijo
1415 exit(0) // termina el proceso hijo
1421 function mark_phase() is
1422 global more_to_scan = false
1423 // Borrado: stop_the_world()
1424 clear_mark_scan_bits()
1427 push_registers_into_stack()
1428 thread_self.stack.end = get_stack_top()
1430 pop_registers_from_stack()
1433 // Borrado: start_the_world()
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.
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*
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ó.
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``.
1466 .. _sol_eager_alloc:
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.
1481 A simple vista las modificaciones necesarias para su implementación parecieran
1482 ser las siguientes::
1488 function mark_is_running() is
1489 return global mark_pid != 0
1491 function collect() is
1492 if mark_is_running() //
1493 finished = try_wait(global mark_pid) //
1494 if finished // Agregado
1501 if child_pid is 0 // proceso hijo
1506 // Borrado: wait(child_pid)
1507 global mark_pid = child_pid
1509 Sin embargo con sólo estas modificaciones el algoritmo deja de ser correcto,
1510 ya que tres cosas problemáticas pueden suceder:
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*.
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*.
1529 El punto 1 se resuelve a través de la siguiente modificación::
1531 function minimize() is
1532 if mark_is_running() // Agregado
1537 if page.block_size is not FREE
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::
1549 function new_big(size) is
1550 number_of_pages = ceil(size / PAGE_SIZE)
1551 pages = find_pages(number_of_pages)
1554 pages = find_pages(number_of_pages)
1557 pool = new_pool(number_of_pages)
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
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)
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
1583 push_registers_into_stack()
1584 thread_self.stack.end = get_stack_top()
1586 pop_registers_from_stack()
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
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
1606 function free_big_object(pool, page) is
1607 pool_end = cast(byte*) pool.pages + (PAGE_SIZE * pool.number_of_pages)
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
1614 function new(size, attrs) is
1615 block_size = find_block_size(size)
1616 if block_size < PAGE
1617 block = new_small(block_size)
1619 block = new_big(size)
1626 block.free = false // Agregado
1627 return cast(void*) block
1629 funciones new_pool(number_of_pages = 1) is
1630 pool = alloc(pool.sizeof)
1633 pool.number_of_pages = number_of_pages
1634 pool.pages = alloc(number_of_pages * PAGE_SIZE)
1635 if pool.pages is null
1639 foreach page in pool
1640 page.block_size = FREE
1641 foreach block in page //
1642 block.free = true // Agregado
1643 block.mark = true //
1646 Finalmente, el punto número tres puede ser solucionado con el siguiente
1649 funciones new_pool(number_of_pages = 1) is
1650 pool = alloc(pool.sizeof)
1653 pool.number_of_pages = number_of_pages
1654 pool.pages = alloc(number_of_pages * PAGE_SIZE)
1655 if pool.pages is null
1659 foreach page in pool
1660 page.block_size = FREE
1661 foreach block in page // Agregado
1662 block.mark = true //
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).
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).
1679 .. _sol_early_collect:
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`).
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.
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``::
1697 function collect(early = false) is // Modificado
1698 if mark_is_running()
1699 finished = try_wait(global mark_pid)
1704 else if early // Agregado
1709 if child_pid is 0 // proceso hijo
1715 global mark_pid = child_pid //
1721 function early_collect() is
1722 if not collect_in_progress() and (percent_free < min_free)
1725 function new(size, attrs) is
1726 block_size = find_block_size(size)
1727 if block_size < PAGE
1728 block = new_small(block_size)
1730 block = new_big(size)
1737 early_collect() // Agregado
1738 return cast(void*) block
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``.
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.
1756 ----------------------------------------------------------------------------
1762 .. include:: links.rst
1764 .. vim: set ts=3 sts=3 sw=3 et tw=78 spelllang=es :