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.
44 ----------------------------------------------------------------------------
46 Teniendo en cuenta que uno de los objetivos principales es no empeorar la
47 eficiencia general de forma notable, la confección de un banco de pruebas es
48 un aspecto fundamental, para poder comprobar con cada cambio que la eficiencia
49 final no se vea notablemente afectada.
51 La confección de un banco de pruebas no es una tarea trivial, mucho menos para
52 un lenguaje con el nivel de fragmentación que tuvo D_ (que hace que a fines
53 prácticos hayan 3 versiones del lenguaje compitiendo), y cuya masa crítica de
54 usuarios es de aficionados que usualmente abandonan los proyectos, quedando
55 obsoletos rápidamente.
57 Con el objetivo de confeccionar este banco de pruebas, desde el comienzo del
58 trabajo se han recolectado (usando como fuente principalmente el grupo de
59 noticias de D_ [#benchmod]_) programas triviales sintetizados con el único
60 propósito de mostrar problemas con el recolector de basura. Otros programas de
61 este estilo fueron escritos explícitamente para este trabajo.
63 Además se han recolectado [#benchmod]_ algunos pequeños programas portados de
64 otros lenguajes de programación, que si bien son pequeños y tienen como
65 objetivo ejercitar el recolector de basura, son programas reales que resuelven
66 un problema concreto, lo que otorga un juego de pruebas un poco más amplio que
67 los programas triviales.
69 .. [#benchmod] Cabe destacar que en general todos los programas recolectados
70 han sido modificados levemente para ajustarlos mejor a las necesidades del
71 banco de prueba (entre las modificaciones más frecuentes se encuentran la
72 conversión de Phobos_ a Tango_ y la eliminación de mensajes por salida
75 Pero probablemente lo más importante para confeccionar un banco de pruebas
76 verdaderamente útil es disponer de programas reales, que hayan sido diseñados
77 con el único objetivo de hacer su trabajo, sin pensar en como impacta el
78 recolector sobre ellos (ni ellos sobre el recolector). Estos programas proveen
79 las pruebas más realistas y amplias. Desgraciadamente no hay muchos programas
80 reales escritos en D_ disponibles públicamente, y no se encontró en la
81 comunidad tampoco una muestra de voluntad por compartir programas privados
82 para usar como banco de pruebas en este trabajo.
84 Por lo tanto el banco de pruebas que se conformó como una mezcla de estas tres
91 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
93 Este es el juego de programas triviales, escritos con el único objetivo de
94 ejercitar un área particular y acotada del recolector.
99 Su objetivo es ejercitar la manipulación de arreglos de tamaño considerable
100 que almacenan objetos de tamaño pequeño o mediano. Esta prueba fue hallada__
101 en el grupo de noticias de D_ y escrita por Babele Dunnit y aunque
102 originalmente fue concebido para mostrar un problema con la concatenación de
103 arreglos (como se aprecia en la sentencia ``version(loseMemory)``), ejercita
104 los aspectos más utilizados del del recolector: manipulación de arreglos
105 y petición e memoria. Es una de las pruebas que más estresa al recolector ya
106 que todo el trabajo que realiza el programa es utilizar servicios de éste.
108 El código fuente del programa es el siguiente::
116 Individual[20] children;
123 foreach (inout individual; individuals)
124 individual = new Individual;
126 Individual[N1] individuals;
129 version = loseMemory;
131 int main(char[][] args)
134 Population testPop1 = new Population;
135 Population testPop2 = new Population;
137 for (int i = 0; i < IT; i++) {
140 version (loseMemory) {
141 indi[] = testPop1.individuals ~ testPop2.individuals;
143 version (everythingOk) {
144 indi[0..N1] = testPop1.individuals;
145 indi[N1..N2] = testPop2.individuals;
151 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=54084
154 ``concpu`` y ``conalloc``
155 ^^^^^^^^^^^^^^^^^^^^^^^^^
156 Estos dos programas fueron escritos especialmente para este trabajo con el fin
157 de ejercitar la interacción entre el recolector y un *mutator* con varios
158 hilos. La única diferencia entre ellos es que ``concpu`` lanza hilos que hacen
159 trabajar de forma intensiva el procesador pero que no utilizan servicios del
160 recolector, salvo en el hilo principal, mientras que ``conalloc`` utiliza
161 servicios del recolector en todos los hilos lanzados.
163 El objetivo de estos programas es medir el impacto de las pausas del
164 recolector. Se espera medir dos tipos de pausa principales, por un lado el
165 tiempo máximo de pausa real, que puede involucrar a más de un hilo y por otro
166 el tiempo de *stop-the-world*, es decir, el tiempo en que los hilos son
167 efectivamente pausados por el recolector para tomar una *foto* de la pila
168 y registros para agregarlos al *root set*.
170 Se espera ``concpu`` sea capaz de explotar cualquier reducción en el tiempo de
171 *stop-the-world*, ya que los hilos solo son interrumpidos por este tipo de
172 pausa. Por otro lado, se espera que ``conalloc`` sea afectado por el tiempo
173 máximo de pausa, que podrían sufrir los hilos incluso cuando el *mundo* sigue
174 su marcha, debido al *lock* global del recolector y que los hilos usan
177 El código de ``concpu`` es el siguiente::
179 import tango.core.Thread: Thread;
180 import tango.core.Atomic: Atomic;
181 import tango.io.device.File: File;
182 import tango.util.digest.Sha512: Sha512;
183 import tango.util.Convert: to;
188 Atomic!(int) running;
190 void main(char[][] args)
192 auto fname = args[0];
196 NT = to!(int)(args[2]);
198 N = to!(int)(args[1]);
201 BYTES = cast(ubyte[]) File.get(fname);
202 auto threads = new Thread[NT];
203 foreach(ref thread; threads) {
204 thread = new Thread(&doSha);
207 while (running.load()) {
208 auto a = new void[](BYTES.length / 4);
209 a[] = cast(void[]) BYTES[];
212 foreach(thread; threads)
218 auto sha = new Sha512;
219 for (size_t i = 0; i < N; i++)
224 El código de ``conalloc`` es igual excepto por la función ``doSha()``, que es
225 de la siguiente manera::
229 for (size_t i = 0; i < N; i++) {
230 auto sha = new Sha512;
239 Escrito por David Schima y también hallado__ en el grupo de noticias de D_,
240 este programa pretende mostrar como afecta el *lock* global del recolector
241 en ambientes *multi-core*, incluso cuando a simple vista parecen no utilizarse
242 servicios del recolector::
244 import tango.core.Thread;
248 enum { nThreads = 4 };
249 auto threads = new Thread[nThreads];
250 foreach (ref thread; threads) {
251 thread = new Thread(&doAppending);
254 foreach (thread; threads)
261 for (size_t i = 0; i < 1_000_000; i++)
265 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=103563
267 El secreto está en que la concatenación de arreglos utiliza por detrás
268 servicios del recolector, por lo tanto un programa multi-hilo en el cual los
269 hilos (aparentemente) no comparten ningún estado, se puede ver
270 considerablemente afectado por el recolector (siendo este efecto más visible
271 en ambientes *multi-core* por el nivel de sincronización extra que significa
272 a nivel de *hardware*). Cabe destacar que, sin embargo, en Linux_ no es tan
278 Este programa trivial lee un archivo de texto y genera un arreglo de cadenas
279 de texto resultantes de partir el texto en palabras. Fue escrito por Leonardo
280 Maffi y también hallado__ en el grupo de noticias de D_. Su objetivo era
281 mostrar lo ineficiente que puede ser concatenar datos a un mismo arreglo
282 repetidas veces y ha desembocado en una pequeña `optimización`__ que sirvió
283 para apalear el problema de forma razonablemente efectiva.
285 El código es el siguiente::
287 import tango.io.device.File: File;
288 import tango.text.Util: delimit;
289 import tango.util.Convert: to;
291 int main(char[][] args) {
294 auto txt = cast(byte[]) File.get(args[1]);
295 auto n = (args.length > 2) ? to!(uint)(args[2]) : 1;
300 auto words = delimit!(byte)(txt, cast(byte[]) " \t\n\r");
301 return !words.length;
304 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=67673
305 __ http://d.puremagic.com/issues/show_bug.cgi?id=1923
310 Este programa fue escrito por Oskar Linde y nuevamente hallado__ en el grupo
311 de noticias. Fue construido para mostrar como el hecho de que el recolector
312 sea conservativo puede hacer que al leer datos binarios hayan muchos *falsos
313 punteros* que mantengan vivas celdas que en realidad ya no deberían ser
314 accesibles desde el *root set* del grafo de conectividad.
316 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=46407
318 El código del programa es el siguiente::
320 import tango.math.random.Random;
322 const IT = 125; // number of iterations, each creates an object
323 const BYTES = 1_000_000; // ~1MiB per object
324 const N = 50; // ~50MiB of initial objects
328 C c; // makes the compiler not set NO_SCAN
329 long[BYTES/long.sizeof] data;
333 auto rand = new Random();
336 foreach (ref o; objs) {
338 foreach (ref x; o.data)
341 for (int i = 0; i < IT; ++i) {
343 foreach (ref x; o.data)
345 // do something with the data...
352 Este programa está basado en la prueba de nombre ``binary-trees`` de `The
353 Computer Language Benchmarks Game`__, una colección de 12 programas escritos
354 en alrededor de 30 lenguajes de programación para comparar su eficiencia
355 (medida en tiempo de ejecución, uso de memoria y cantidad de líneas de
356 código). De este juego de programas se utilizó solo ``binary-trees`` por ser
357 el único destinado a ejercitar el manejo de memoria. El programa sólo manipula
358 árboles binarios, creándolos y recorriéndolos inmediatamente (no realiza
359 ningún trabajo útil). La traducción a D_ fue realizada por Andrey Khropov
360 y fue hallada__ en el grupo de noticias.
362 __ http://shootout.alioth.debian.org/
363 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=43991
365 El código fuente es el siguiente::
367 import tango.util.Convert;
370 int main(string[] args)
372 int N = args.length > 1 ? to!(int)(args[1]) : 1;
374 int maxDepth = (minDepth + 2) > N ? minDepth + 2 : N;
375 int stretchDepth = maxDepth + 1;
376 int check = TreeNode.BottomUpTree(0, stretchDepth).ItemCheck;
377 TreeNode longLivedTree = TreeNode.BottomUpTree(0, maxDepth);
378 for (int depth = minDepth; depth <= maxDepth; depth += 2) {
379 int iterations = 1 << (maxDepth - depth + minDepth);
381 for (int i = 1; i <= iterations; i++) {
382 check += TreeNode.BottomUpTree(i, depth).ItemCheck;
383 check += TreeNode.BottomUpTree(-i, depth).ItemCheck;
391 TreeNode left, right;
394 this(int item, TreeNode left = null, TreeNode right = null)
401 static TreeNode BottomUpTree(int item, int depth)
404 return new TreeNode(item,
405 BottomUpTree(2 * item - 1, depth - 1),
406 BottomUpTree(2 * item, depth - 1));
407 return new TreeNode(item);
413 return item + left.ItemCheck() - right.ItemCheck();
422 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
424 Todos los pequeños programas utilizados como parte del banco de prueba
425 provienen del `Olden Benchmark`__ [CAR95]_. Estos programas fueron diseñados
426 para probar el lenguaje de programación Olden__; un lenguaje diseñado para
427 paralelizar programas automáticamente en arquitecturas con memoria
428 distribuida. Son programas relativamente pequeños (entre 400 y 1000 líneas de
429 código fuente cada uno) que realizan una tarea secuencial que asigna
430 estructuras de datos dinámicamente. Las estructuras están usualmente
431 organizadas como listas o árboles, y muy raramente como arreglos. Los
432 programas pasan la mayor parte del tiempo alocando datos y el resto usando los
433 datos alocados, por lo que en general están acotados en tiempo por el uso de
434 memoria (y no de procesador).
436 __ http://www.irisa.fr/caps/people/truong/M2COct99/Benchmarks/Olden/Welcome.html
437 __ http://www.martincarlisle.com/olden.html
439 La traducción a D_ fue realizada por Leonardo Maffi y están basadas a su vez
440 en la traducción de este juego de pruebas a Java_, JOlden__ [CMK01]_. En Java_
441 no se recomienda utilizar este conjunto de pruebas para medir la eficiencia
442 del recolector de basura, dado que se han creado mejores pruebas para este
443 propósito, como DaCapo__ [BLA06]_, sin embargo, dada la falta de programas
444 disponibles en general, y de un conjunto de pruebas especialmente diseñado
445 para evaluar el recolector de basura en D_, se decide utilizarlas en este
446 trabajo de todos modos. Sin embargo sus resultados deben ser interpretados con
447 una pizca de sal por lo mencionado anteriormente.
449 __ http://www-ali.cs.umass.edu/DaCapo/benchmarks.html
450 __ http://www.dacapobench.org/
452 En general (salvo para el programa ``voronoï``) está disponible el código
453 fuente portado a D_, Java_ y Python_, e incluso varias versiones con distintas
454 optimizaciones para reducir el consumo de tiempo y memoria. Además provee
455 comparaciones de tiempo entre todas ellas. Los programas utilizados en este
456 banco de pruebas son la versión traducida más literalmente de Java_ a D_, ya
457 que hace un uso más intensivo del recolector que las otras versiones.
459 A continuación se da una pequeña descripción de cada uno de los 5 programas
460 traducidos y los enlaces en donde encontrar el código fuente (y las
461 comparaciones de tiempos estar disponibles).
466 Este programa computa las interacciones gravitatorias entre un número
467 :math:`N` de cuerpos en tiempo :math:`O(N log N)` y está basado en árboles
468 heterogéneos de 8 ramas, según el algoritmo descripto por Barnes & Hut
471 Código fuente disponible en:
472 http://www.fantascienza.net/leonardo/js/dolden_bh.zip
477 Este programa ordena :math:`N` números, donde :math:`N` es una potencia de 2,
478 usando un ordenamiento *Bitonic* adaptativo, un algoritmo paralelo óptimo para
479 computadoras con memoria compartida, según describen Bilardi & Nicolau
480 [BN98]_. Utiliza árboles binarios como principal estructuras de datos.
482 Código fuente disponible en:
483 http://www.fantascienza.net/leonardo/js/dolden_bisort.zip
488 Este programa modela la propagación de ondas electromagnéticas a través de
489 objetos en 3 dimensiones. Realiza un cálculo simple sobre un grafo irregular
490 bipartito (implementado utilizando listas simplemente enlazadas) cuyos nodos
491 representan valores de campo eléctrico y magnético. El algoritmo es el
492 descripto por Culler, et al. [CDG93]_.
494 Código fuente disponible en:
495 http://www.fantascienza.net/leonardo/js/dolden_em3d.zip
500 Este programa implementa una heurística para resolver el problema del viajante
501 (*traveling salesman problem*) utilizando árboles binarios balanceados. El
502 algoritmo utilizado es el descripto por Karp [KAR77]_.
505 Código fuente disponible en:
506 http://www.fantascienza.net/leonardo/js/dolden_tsp.zip
511 Este programa genera un conjunto aleatorio de puntos y computa su diagrama de
512 Voronoï, una construcción geométrica que permite construir una partición del
513 plano euclídeo, utilizando el algoritmo descripto por Guibas & Stolfi [GS85]_.
515 Código fuente disponible en: http://codepad.org/xGDCS3KO
521 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
523 Dil_ (escrito en su mayor parte por Aziz Köksal y publicado bajo licencia
524 GPL_) es, lamentablemente, el único programa real hallado que, a pesar de
525 estar incompleto, es lo suficientemente grande, mantenido y estable como para
526 ser incluido en el banco de pruebas. Se trata de un compilador de D_ escrito
527 en D_ y está incompleto porque no puede generar código (falta implementar el
528 análisis semántico y la generación de código), por lo que es principalmente
529 utilizado para generar documentación a partir del código.
531 El programa está compuesto por:
533 * 32.000 líneas de código fuente (aproximadamente).
534 * 86 módulos (o archivos).
535 * 322 diferentes tipos de datos definidos por el usuario, de los cuales 34 son
536 tipos *livianos* (``struct``) y 288 tipos polimórficos (``class``), de los
537 que 260 son subtipos (sub-clases).
539 Puede observarse entonces que a pesar de ser incompleto, es una pieza de
540 software bastante compleja y de dimensión considerable.
542 Además, al interpretar código fuente se hace un uso intensivo de cadenas de
543 texto que en general presentan problemas muy particulares por poder ser
544 objetos extremadamente pequeños y de tamaños poco convencionales (no múltiplos
545 de palabras, por ejemplo). A su vez, el texto interpretado es convertido a una
546 representación interna en forma de árbol (o *árbol de sintaxis abstracta*)
547 modelado por tipos *livianos* y polimórficos que están organizados en arreglos
548 dinámicos contiguos y asociativos (que usan muchos servicios del recolector),
549 y que finalmente son manipulados para obtener y generar la información
550 necesaria, creando y dejando *morir* objetos constantemente (pero no como única
551 forma de procesamiento, como otras pruebas sintetizadas).
553 Por último, a diferencia de muchos otros programas escritos en D_, que dadas
554 algunas de las ineficiencias del recolector invierten mucho trabajo en limitar
555 su uso, este programa no está escrito pensando en dichas limitaciones, por lo
556 que muestra un funcionamiento muy poco sesgado por estas infortunadas
559 Por todas estas razones, Dil_ es el ejemplar que tal vez mejor sirve a la hora
560 de medir de forma realista los resultados obtenidos o los avances realizados.
561 Si bien, como se ha dicho anteriormente, las demás pruebas del banco pueden
562 ser útiles para encontrar problemas muy particulares, está es la que da una
563 lectura más cercana a la realidad del uso de un recolector.
568 Modificaciones propuestas
569 ----------------------------------------------------------------------------
571 Se decide realizar todas las modificaciones al recolector actual de forma
572 progresiva e incremental, partiendo como base del recolector de la versión
573 0.99.9 de Tango_. Las razones que motivan esta decisión son varias; por un
574 lado es lo más apropiado dados los requerimientos claves mencionados al
575 principio de este capítulo. Por ejemplo, al hacer cambios incrementales es más
576 fácil comprobar que la eficiencia no se aleja mucho del actual con cada
577 modificación y una modificación gradual impone menos resistencia a la
578 aceptación del nuevo recolector.
580 Además la construcción de un recolector de cero es una tarea difícil
581 considerando que un error en el recolector es extremadamente complejo de
582 rastrear, dado que en general el error se detecta en el *mutator* y en una
583 instancia muy posterior al origen real del error. Esto ha sido comprobado de
584 forma práctica, dado que, a modo de ejercicio para interiorizarse en el
585 funcionamiento del *runtime* de D_, primero se ha construido desde cero una
586 implementación de un recolector *naïve*, resultando muy difícil su depuración
587 por las razones mencionadas. Por el contrario, comenzar con un recolector en
588 funcionamiento como base hace más sencillo tanto probar cada pequeña
589 modificación para asegurar que no introduce fallos, como encontrar y reparar
590 los fallos cuando estos se producen, ya que el código incorrecto introducido
591 está bien aislado e identificado.
593 A continuación se hace un recorrido sobre cada una de las mejoras propuestas,
594 y en los casos en los que la mejora propone un cambio algorítmico, se analiza
595 la corrección del algoritmo resultante, partiendo de la base de que el
596 algoritmo tomado como punto de partida es un marcado y barrido que utiliza la
597 abstracción tricolor para hacer la fase de marcado de forma iterativa (ver
598 :ref:`gc_mark_sweep` y :ref:`gc_intro_tricolor`), cuya corrección ya está
599 probada en la literatura preexistente.
605 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
607 Una de las primeras mejoras propuestas es la posibilidad de configurar el
608 recolector de forma más sencilla. El requerimiento mínimo es la posibilidad de
609 configurar el recolector sin necesidad de recompilarlo. La complejidad de esto
610 surge de que el recolector debe ser transparente para el programa del usuario.
612 Configurar el recolector en tiempo de compilación del programa del usuario
613 probablemente requeriría modificar el compilador, y además, si bien es una
614 mejora sustancial a la configuración en tiempo de compilación del recolector,
615 no termina de ser completamente conveniente para realizar pruebas reiteradas
616 con un mismo programa para encontrar los mejores valores de configuración para
617 ese programa en particular.
619 Por otro lado, permitir configurar el recolector en tiempo de ejecución, una
620 vez que su estructura interna ya fue definida y creada, puede ser no solo
621 tedioso y complejo, además ineficiente, por lo tanto esta opción también se
624 Finalmente, lo que parece ser más apropiado para un recolector, es permitir la
625 configuración en tiempo de inicialización. Es decir, configurar el recolectar
626 sin necesidad de recompilar ni el programa del usuario ni el recolector, pero
627 antes de que el programa del usuario inicie, de manera que una vez iniciado el
628 recolector con ciertos parámetros, éstos no cambien nunca más en durante la
631 Este esquema provee la mejor relación entre configurabilidad, conveniencia,
632 eficiencia y simplicidad. Una posibilidad para lograr esto es utilizar
633 parámetros de línea de comandos, sin embargo no parece ni sencillo (proveer
634 una forma de leer los parámetros de línea de comandos requiere cambios en el
635 *runtime*) ni apropiado (el recolector debería ser lo más transparente posible
636 para el programa del usuario).
638 Otra posibilidad es utilizar variables de entorno, que parece ser la opción
639 más sencilla y apropiada. Sencilla porque las variables de entorno pueden ser
640 leídas directamente al inicializar el recolector sin necesidad de cooperación
641 alguna del *runtime*, a través de :manpage:`getenv(3)`. Apropiada porque, si
642 bien el problema de invasión del programa del usuario también existe, es una
643 práctica más frecuente y aceptada la configuración de módulos internos
644 o bibliotecas compartidas a través de variables de entorno.
646 Por último, antes de comenzar a usar este esquema de configuración, se
647 verifica que tomar ciertas decisiones en tiempo de ejecución no impacten en la
648 eficiencia del recolector. Para esto se convierten algunas opciones que antes
649 eran solo seleccionables en tiempo de compilación del recolector para que
650 puedan ser seleccionables en tiempo de inicialización y se comprueba que no
651 hay una penalización apreciable.
656 Especificación de opciones
657 ^^^^^^^^^^^^^^^^^^^^^^^^^^
658 Para especificar opciones de configuración, hay que hacerlo a través de la
659 variable de entorno de nombre :envvar:`D_GC_OPTS`. El valor de esa variable es
660 interpretado de la siguiente manera (en formato similar a :term:`BNF`):
663 D_GC_OPTS: `option` ( ':' `option` )* <lista de opciones>
664 option: `name` [ '=' `value` ]
665 name: `namec` `namec`* <nombre de la opción>
666 value: `valuec`* <valor de la opción>
667 namec: `valuec` - '='
668 valuec: [0x01-0xFF] - ':' <cualquier char salvo '\0' y ':'>
670 Es decir, se compone de una lista de opciones separadas por **:**. Cada opción
671 se especifica con un nombre, opcionalmente seguido por un valor (separados por
674 El valor de una opción puede ser un texto arbitrario (exceptuando los
675 caracteres ``'\0'`` y ``':'`` y de longitud máxima 255), pero cada opción lo
676 interpreta de forma particular. Como caso general, hay opciones booleanas, que
677 toman como valor verdadero un cualquier número distinto de 0 (o si el valor es
678 vació, es decir, solo se indica el nombre de la opción), y como valor falso
679 cualquier otro texto.
681 A continuación se listan las opciones reconocidas por el recolector (indicando
682 el formato del valor de la opción de tener uno especial):
685 Esta es una opción (booleana) disponible en el recolector original, pero
686 que se cambia para que sea configurable en tiempo de inicialización
687 (estando desactivada por omisión). Activa la opción ``MEMSTOMP`` descripta
691 Esta opción es también booleana (desactivada por omisión), está disponible
692 en el recolector original, y se la cambia para sea configurable en tiempo
693 de inicialización. Activa la opción ``SENTINEL`` descripta en
697 Esta opción permite crear una cierta cantidad de *pools* de un tamaño
698 determinado previo a que inicie el programa. Si se especifica solo un
699 número, se crea un *pool* con ese tamaño en MiB. Si, en cambio, se
700 especifica una cadena del tipo ``3x1``, el primer número indica la cantidad
701 de *pools* y el segundo el tamaño en MiB de cada uno (3 *pools* de 1MiB en
702 este caso). Ver :ref:`sol_pre_alloc` para más detalles sobre la utilidad de
706 El valor de esta opción indica el porcentaje mínimo porcentaje del *heap*
707 que debe quedar libre luego de una recolección. Siendo un porcentaje, solo
708 se aceptan valores entre 0 y 100, siendo su valor por omisión 5. Ver
709 :ref:`sol_ocup` para más detalles sobre su propósito.
711 ``malloc_stats_file``
712 Esta opción sirve para especificar un archivo en el cual escribir un
713 reporte de todas la operaciones de pedido de memoria realizadas por el
714 programa (durante su tiempo de vida). Ver :ref:`sol_stats` para más
715 detalles sobre la información provista y el formato del reporte.
717 ``collect_stats_file``
718 Esta opción sirve para especificar un archivo en el cual escribir un
719 reporte de todas las recolecciones hechas durante el tiempo de vida del
720 programa. Ver :ref:`sol_stats` para más detalles sobre la información
721 provista y el formato del reporte.
724 Esta opción booleana permite desactivar el escaneo preciso del *heap*,
725 forzando al recolector a ser completamente conservativo (excepto por los
726 bloques con el atributo ``NO_SCAN`` que siguen sin ser escaneados). Ver
727 :ref:`sol_precise` para más detalles sobre la existencia de esta opción.
730 Esta opción booleana (activada por omisión) permite seleccionar si el
731 recolector debe correr la fase de marcado en paralelo o no (es decir, si el
732 recolector corre de forma concurrente con el *mutator*). Para más detalles
736 Esta opción booleana (activada por omisión), sólo puede estar activa si
737 ``fork`` también está activa y sirve para indicar al recolector que reserve
738 un nuevo *pool* de memoria cuando una petición no puede ser satisfecha,
739 justo antes de lanzar la recolección concurrente. Ver
740 :ref:`sol_eager_alloc` para más detalles sobre el propósito de esta opción.
743 Esta opción booleana (desactivada por omisión), también sólo puede estar
744 activa si ``fork`` está activa y sirve para indicar al recolector que lance
745 una recolección (concurrente) antes de que la memoria libre se termine (la
746 recolección temprana será disparada cuando el porcentaje de memoria libre
747 sea menor a ``min_free``). Ver :ref:`sol_early_collect` para más detalles
748 sobre el propósito de esta opción.
750 Cualquier opción o valor no reconocido es ignorado por el recolector. Se
751 utilizan los valores por omisión de las opciones que no fueron especificadas,
752 o cuyos valores no pudieron ser interpretados correctamente.
754 Para cambiar la configuración del recolector se puede invocar el programa de
755 la siguiente manera (usando un intérprete de comandos del tipo *bourne
760 D_GC_OPTS=conservative:eager_alloc=0:early_collect=1:pre_alloc=2x5 ./programa
762 En este ejemplo, se activan las opciones ``conservative`` y ``early_collect``,
763 se desactiva ``eager_alloc`` y se crean 2 *pools* de 5MiB cada uno al
764 inicializar el recolector.
767 Reestructuración y cambios menores
768 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
770 Si bien se decide no comenzar una implementación desde cero, se ha mostrado
771 (ver :ref:`dgc_bad_code`) que la implementación actual es lo suficientemente
772 desprolija como para complicar su modificación. Es por esto que se hacen
773 algunas reestructuraciones básicas del código, reescribiendo o saneando de
774 forma incremental todas aquellas partes que complican su evolución.
776 Además de las modificaciones puramente estéticas (aunque no por eso menos
777 valuables, ya que la legibilidad y simplicidad del código son un factor
778 fundamental a la hora de ser mantenido o extendido), se hacen otras pequeñas
779 mejoras, que se detallan a continuación.
781 Remoción de memoria *no-encomendada*
782 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
783 Se elimina la distinción entre memoria *encomendada* y *no-encomendada* (ver
784 :ref:`dgc_committed`), pasando a estar *encomendada* toda la memoria
785 administrada por el recolector.
787 Si bien a nivel de eficiencia este cambio no tuvo impacto alguno (cuando en un
788 principio se especuló con que podría dar alguna ganancia en este sentido), se
789 elimina el concepto de memoria *encomendada* para quitar complejidad al
792 Esta mejora no afecta a la corrección del algoritmo, ya que a nivel lógico el
793 recolector solo ve la memoria *encomendada*.
795 .. _sol_minor_findsize:
797 Caché de ``Pool.findSize()``
798 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
799 Se crea un caché de tamaño de bloque para el método ``findSize()`` de un
800 *pool*. Esto acelera considerablemente las operaciones que necesitan pedir el
801 tamaño de un bloque reiteradamente, por ejemplo, al añadir nuevos elementos
802 a un arreglo dinámico.
804 Esta mejora tampoco afecta a la corrección del algoritmo, ya que nuevamente no
805 afecta su comportamiento a nivel lógico, solo cambia detalles en la
806 implementación de forma transparentes para el algoritmo de recolección.
808 Optimizaciones sobre ``findPool()``
809 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
810 Al analizar los principales cuellos de botella del recolector, es notoria la
811 cantidad de tiempo que pasa ejecutando la función ``findPool()``, que dado un
812 puntero devuelve el *pool* de memoria al cual pertenece. Es por esto que se
813 minimiza el uso de esta función. Además, dado que los *pools* de memoria están
814 ordenados por el puntero de comienzo del bloque de memoria manejado por el
815 *pool*, se cambia la búsqueda (originalmente lineal) por una búsqueda binaria.
816 Finalmente, dado que la lista de libre está construida almacenando el puntero
817 al siguiente en las mismas celdas que componen la lista, se almacena también
818 el puntero al *pool* al que dicha celda pertenece (dado que la celda más
819 pequeña es de 16 bytes, podemos garantizar que caben dos punteros, incluso
820 para arquitecturas de 64 bits). De esta manera no es necesario usar
821 ``findPool()`` al quitar una celda de la lista de libres.
823 Una vez más, la mejora no afecta la corrección del código.
827 Pre-asignación de memoria
828 ^^^^^^^^^^^^^^^^^^^^^^^^^
829 Esta opción permite crear una cierta cantidad de *pools* de un tamaño
830 determinado previo a que inicie el programa. Normalmente el recolector no
831 reserva memoria hasta que el programa lo pida. Esto puede llegar a evitar
832 que un programa haga muchas recolecciones al comenzar, hasta que haya
833 cargado su conjunto de datos de trabajo.
835 Se han analizado varios valores por omisión pero ninguno es consistentemente
836 mejor que comenzar sin memoria asignada, por lo tanto no se cambia el
837 comportamiento original, pero se agrega una opción (ver ``pre_alloc`` en
838 :ref:`sol_config_spec`) para que el usuario pueda experimentar con cada
839 programa en particular si esta opción es beneficiosa.
841 Esta opción tampoco cambia la corrección del algoritmo de recolección, solo
842 sus condiciones iniciales.
846 Mejora del factor de ocupación del *heap*
847 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
848 El factor de ocupación del *heap* debe ser apropiado por dos razones. Por un
849 lado, si el *heap* está demasiado ocupado todo el tiempo, serán necesarias
850 muchas recolecciones que, aunque pequeñas dado que la memoria utilizada es
851 poca, puede llegar a ser extremadamente ineficiente en casos patológicos (ver
852 :ref:`dgc_bad_ocup`). Por otro lado, si el tamaño del *heap* es extremadamente
853 grande (en comparación con el tamaño real del grupo de trabajo del programa),
854 se harán pocas recolecciones pero cada una es muy costosa, porque el algoritmo
855 de marcado y barrido es :math:`O(\lvert Heap \rvert)` (ver
856 :ref:`gc_mark_sweep`). Además la afinidad del caché va a ser extremadamente
859 Para mantener el factor de ocupación dentro de límites razonables, se agrega
860 la opción ``min_free`` (ver :ref:`sol_config_spec`). Esta opción indica el
861 recolector cual debe ser el porcentaje mínimo del *heap* que debe quedar libre
862 luego de una recolección. En caso de no cumplirse, se pide más memoria al
863 sistema operativo para cumplir este requerimiento. Además, luego de cada
864 recolección se verifica que el tamaño del *heap* no sea mayor a ``min_free``,
865 para evitar que el *heap* crezca de forma descontrolada. Si es mayor
866 a ``min_free`` se intenta minimizar el uso de memoria liberando *pools* que
867 estén completamente desocupados, mientras que el factor de ocupación siga
868 siendo mayor a ``min_free``. Si liberar un *pool* implica pasar ese límite, no
869 se libera y se pasa a analizar el siguiente y así sucesivamente.
871 Esta modificación no afecta a la corrección del algoritmo, ya que no lo afecta
874 Modificaciones descartadas
875 ^^^^^^^^^^^^^^^^^^^^^^^^^^
876 Se realizan varias otras modificaciones, con la esperanza de mejorar la
877 eficiencia del recolector, pero que, al contrario de lo esperado, empeoran la
878 eficiencia o la mejoran de forma muy marginal en comparación con la
879 complejidad agregada.
881 Probablemente el caso más significativo, y por tanto el único que vale la pena
882 mencionar, es la conversión de marcado iterativo a marcado recursivo y luego
883 a un esquema híbrido. Como se describe en :ref:`dgc_bad`, el marcado iterativo
884 tiene sus ventajas, pero tiene desventajas también. Al convertirlo a puramente
885 recursivo, se impracticable por resultar en errores de desbordamiento de pila.
887 Por lo tanto se prueba con un esquema híbrido, poniendo un límite a la
888 recursividad, volviendo al algoritmo iterativo cuando se alcanza este límite.
890 La implementación del algoritmo híbrido consiste en los siguientes cambios
891 sobre el algoritmo original (ver :ref:`dgc_algo_mark`)::
893 function mark_phase() is
894 global more_to_scan = false
895 global depth = 0 // Agregado
897 clear_mark_scan_bits()
900 push_registers_into_stack()
901 thread_self.stack.end = get_stack_top()
903 pop_registers_from_stack()
908 function mark_range(begin, end) is
910 global depth++ // Agregado
912 [pool, page, block] = find_block(pointer)
913 if block is not null and block.mark is false
915 if block.noscan is false
917 if (global depth > MAX_DEPTH) //
918 more_to_scan = true //
920 foreach ptr in block.words //
924 Al analizar los resultados de de esta modificación, se observa una mejoría muy
925 level, para valores de ``MAX_DEPTH`` mayores a cero (en algunos casos bastante
926 mayores) y en general para ``MAX_DEPTH`` cero (es decir, usando el algoritmo
927 de forma completamente iterativa) los resultados son peores, dado que se paga
928 el trabajo extra sin ganancia alguna. En la figura :vref:`fig:sol-mark-rec` se
929 puede ver, por ejemplo, el tiempo total de ejecución de Dil_ al generar la
930 documentación completa del código de Tango_, según varía el valor de
933 .. fig:: fig:sol-mark-rec
935 Análisis de tiempo total de ejecución en función del valor de
938 Tiempo total de ejecución de Dil_ al generar la documentación completa del
939 código de Tango_ en función del valor de ``MAX_DEPTH``. El rombo no
940 pertenece a ningún nivel de recursividad, representa el tiempo de ejecución
941 del algoritmo original (puramente iterativo).
943 .. image:: sol-mark-rec-dil.pdf
946 Dado que aumentar el nivel máximo de recursividad significa un uso mayor del
947 *stack*, y que esto puede impactar en el usuario (si el usuario tuviera un
948 programa que esté al borde de consumir todo el *stack*, el recolector podría
949 hacer fallar al programa de una forma inesperada para el usuario, problema que
950 sería muy difícil de depurar para éste), y que los resultados obtenidos no son
951 rotundamente superiores a los resultados sin esta modificación, se opta por no
952 incluir este cambio. Tampoco vale la pena incluirlo como una opción con valor
953 por omisión 0 porque, como se ha dicho, para este caso el resultado es incluso
954 peor que sin la modificación.
956 Esta modificación mantiene la corrección del recolector dado que tampoco
957 modifica el algoritmo sino su implementación. Además ambos casos extremos son
958 correctos (si ``MAX_DEPTH`` es 0, el algoritmo es puramente iterativo y si
959 pudiera ser infinito resultaría en el algoritmo puramente recursivo).
964 Recolección de estadísticas
965 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
967 Un requerimiento importante, tanto para evaluar los resultados de este trabajo
968 como para analizar el comportamiento de los programas estudiados, es la
969 recolección de estadísticas. Hay muchos aspectos que pueden ser analizados
970 a la hora de evaluar un recolector, y es por esto que se busca que la
971 recolección de datos sea lo más completa posible.
973 Con este objetivo, se decide recolectar datos sobre lo que, probablemente,
974 sean las operaciones más importantes del recolector: asignación de memoria
977 Todos los datos recolectados son almacenados en archivos que se especifican
978 a través de opciones del recolector (ver :ref:`sol_config_spec`). Los archivos
979 especificados debe poder ser escritos (y creados de ser necesario) por el
980 recolector (de otra forma se ignora la opción). El conjunto de datos
981 recolectados son almacenados en formato :term:`CSV` en el archivo, comenzando
982 con una cabecera que indica el significado de cada columna.
984 Los datos recolectados tienen en general 4 tipos de valores diferentes:
987 Se guarda en segundos como número de punto flotante (por ejemplo ``0.12``).
990 Se guarda en forma hexadecimal (por ejemplo ``0xa1b2c3d4``).
993 Se guarda como un número decimal, expresado en bytes (por ejemplo ``32``).
996 Se guarda como el número ``0`` si es falso o ``1`` si es verdadero.
998 Esta modificación mantiene la corrección del recolector dado que no hay cambio
1001 Asignación de memoria
1002 ^^^^^^^^^^^^^^^^^^^^^
1003 La recolección de datos sobre asignación de memoria se activa asignando un
1004 nombre de archivo a la opción ``malloc_stats_file``. Por cada asignación de
1005 memoria pedida por el programa (es decir, por cada llamada a la función
1006 ``gc_malloc()``) se guarda una fila con los siguientes datos:
1008 1. Cantidad de segundos que pasaron desde que empezó el programa (*timestamp*).
1009 2. Tiempo total que tomó la asignación de memoria.
1010 3. Valor del puntero devuelto por la asignación.
1011 4. Tamaño de la memoria pedida por el programa.
1012 5. Si esta petición de memoria disparó una recolección o no.
1013 6. Si debe ejecutarse un *finalizador* sobre el objeto (almacenado en la
1014 memoria pedida) cuando ésta no sea más alcanzable (cuando sea barrido).
1015 7. Si objeto carece de punteros (es decir, no debe ser escaneada).
1016 8. Si objeto no debe ser movido por el recolector.
1017 9. Puntero a la información sobre la ubicación de los punteros del objeto.
1018 10. Tamaño del tipo del objeto.
1019 11. Primera palabra con los bits que indican que palabras del tipo deben ser
1020 escaneados punteros y cuales no (en hexadecimal).
1021 12. Primera palabra con los bits que indican que palabras del tipo son
1022 punteros garantizados (en hexadecimal).
1024 Como puede apreciarse, la mayor parte de esta información sirve más para
1025 analizar el programa que el recolector. Probablemente solo el punto 2 sea de
1026 interés para analizar como se comporta el recolector.
1028 El punto 8 es completamente inútil, ya que el compilador nunca provee esta
1029 información, pero se la deja por si en algún momento comienza a hacerlo. Los
1030 puntos 9 a 12 provee información sobre el tipo del objeto almacenado, útil
1031 para un marcado preciso (ver :ref:`sol_precise`).
1033 El punto 6 indica, indirectamente, cuales de los objetos asignados son
1034 *pesados*, ya que éstos son los únicos que pueden tener un *finalizador*.
1035 Además, a través de los puntos 4 y 10 es posible inferir si lo que va
1036 almacenarse es un objeto solo o un arreglo de objetos.
1038 Recolección de basura
1039 ^^^^^^^^^^^^^^^^^^^^^
1040 Los datos sobre las recolecciones realizadas se guardan al asignar un nombre
1041 de archivo a la opción ``collect_stats_file``. Cada vez que se dispara una
1042 recolección [#solcollect]_ (es decir, cada vez que se llama a la función
1043 ``fullcollect()``) se guarda una fila con los siguientes datos:
1045 1. Cantidad de segundos que pasaron desde que empezó el programa (*timestamp*).
1046 2. Tiempo total que tomó la asignación de memoria que disparó la recolección.
1047 3. Tiempo total que tomó la recolección.
1048 4. Tiempo total que deben pausarse todos los hilos (tiempo de
1050 5. Cantidad de memoria usada antes de la recolección.
1051 6. Cantidad de memoria libre antes de la recolección.
1052 7. Cantidad de memoria desperdiciada antes de la recolección.
1053 8. Cantidad de memoria utilizada por el mismo recolector antes de la
1054 recolección (para sus estructuras internas).
1055 9. Cantidad de memoria usada después de la recolección.
1056 10. Cantidad de memoria libre después de la recolección.
1057 11. Cantidad de memoria desperdiciada [#solwaste]_ después de la recolección.
1058 12. Cantidad de memoria utilizada por el mismo recolector después de la
1061 Si bien el punto 4 parece ser el más importante para un programa que necesita
1062 baja latencia, dado el *lock* global del recolector, el punto 2 es
1063 probablemente el valor más significativo en este aspecto, dado que, a menos
1064 que el programa en cuestión utilice muy poco el recolector en distintos hilos,
1065 los hilos se verán pausados de todas formas cuando necesiten utilizar el
1068 .. [#solcollect] Esto es en el sentido más amplio posible. Por ejemplo, cuando
1069 se utiliza marcado concurrente (ver :ref:`sol_fork`), se guarda esta
1070 información incluso si ya hay una recolección activa, pero el tiempo de
1071 pausa de los hilos será -1 para indicar que en realidad nunca fueron
1074 .. [#solwaste] Memoria *desperdiciada* se refiere a memoria que directamente
1075 no puede utilizarse debido a la fragmentación. Si por ejemplo, se piden 65
1076 bytes de memoria, dada la organización del *heap* en bloques (ver
1077 :ref:`dgc_org`), el recolector asignará un bloque de 128 bytes, por lo
1078 tanto 63 bytes quedarán desperdiciados.
1084 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1086 En paralelo con este trabajo, David Simcha comienza a explorar la posibilidad
1087 de agregar precisión parcial al recolector, generando información sobre la
1088 ubicación de los punteros para cada tipo [DBZ3463]_. Su trabajo se limita
1089 a una implementación a nivel biblioteca de usuario y sobre `D 2.0`_.
1090 Desafortunadamente su trabajo pasa desapercibido por un buen tiempo.
1092 Luego Vincent Lang (mejor conocido como *wm4* en la comunidad de D_), retoma
1093 este trabajo, pero modificando el compilador DMD_ y trabajando con `D 1.0`_
1094 y Tango_, al igual que este trabajo. Dado el objetivo y entorno común, se abre
1095 la posibilidad de adaptar los cambios de Vincent Lang a este trabajo,
1096 utilizando una versión modificada de DMD_ (dado que los cambios aún no son
1097 integrados al compilador oficial).
1099 Información de tipos provista por el compilador
1100 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1101 Con éstas modificaciones, el compilador en cada asignación le pasa al
1102 recolector información sobre los punteros del tipo para el cual se pide la
1103 memoria. Esta información se pasa como un puntero a un arreglo de palabras con
1104 la estructura mostrada en la figura :vref:`fig:sol-ptrmap` y que se describe
1107 .. fig:: fig:sol-ptrmap
1109 Estructura de la información de tipos provista por el compilador.
1117 +-------------+----------------------------+----------------------------+
1118 | "Tamaño en" | "Bits indicando si la" | "Bits indicando si" |
1119 | "cantidad" | "palabra en una posición" | "la palabra en una" |
1120 | "de" | "debe escanearse como" | "posición es" |
1121 | "palabras" | "si fuera un puntero" | "un puntero" |
1122 +-------------+----------------------------+----------------------------+
1125 +----- 1 -----+------- ceil(N/BPW) --------+------- ceil(N/BPW) --------+
1128 * La primera palabra indica el tamaño, en **cantidad de palabras**, del tipo
1129 para el cual se pide la memoria (:math:`N`).
1130 * Las siguientes :math:`ceil(\frac{N}{BPW})` palabras indican,
1131 como un conjunto de bits, qué palabras deben ser escaneadas por el
1132 recolector como si fueran punteros (donde :math:`BPW` indica la cantidad de
1133 bits por palabra, por ejemplo 32 para x86).
1134 * Las siguientes :math:`ceil(\frac{N}{BPW})` palabras son otro conjunto de
1135 bits indicando qué palabras son realmente punteros.
1137 Los conjuntos de bits guardan la información sobre la primera palabra en el
1138 bit menos significativo. Dada la complejidad de la representación, se ilustra
1139 con un ejemplo. Dada la estructura::
1148 void* begin1; // 1 word
1149 byte[size_t.sizeof * 14 + 1] bytes; // 15 words
1150 // el compilador agrega bytes de "padding" para alinear
1151 void* middle; // 1 word
1152 size_t[14] ints; // 14 words
1153 void* end1; // 1 words
1154 // hasta acá se almacenan los bits en la primera palabra
1155 void* begin2; // 1 words
1161 El compilador genera la estructura que se muestra en la figura
1162 :vref:`fig:sol-ptrmap-example` (asumiendo una arquitectura de 32 bits). Como
1163 puede apreciarse, el miembro ``u``, al ser una unión entre un puntero y un
1164 dato común, el compilador no puede asegurar que lo que se guarda en esa
1165 palabra sea realmente un puntero, pero indica que debe ser escaneado. El
1166 recolector debe debe ser conservativo en este caso, y escanear esa palabra
1167 como si fuera un puntero.
1169 .. fig:: fig:sol-ptrmap-example
1171 Ejemplo de estructura de información de tipos generada para el tipo ``S``.
1178 /---- "bit de 'end1'" -\
1180 | /---- "bit de 'middle'" | "de bits"
1182 | "bits de" | "bits de" /---- "bit de 'begin1'" | "primera"
1183 | "'ints'" | "'bytes'" | | "palabra"
1184 |/------------\|/-------------\| -/
1186 +----------------------------------+
1187 | 00000000000000000000000000100100 | "Tamaño en cantidad de palabras (36)"
1188 +==================================+ --\
1189 | 10000000000000010000000000000001 | | "Bits que indican si hay que"
1190 +----------------------------------+ | "escanear una palabra según"
1191 | 00000000000000000000000000001101 | | "su posición"
1192 +==================================+ --+
1193 | 10000000000000010000000000000001 | | "Bits que indican si hay un"
1194 +----------------------------------+ | "puntero en la palabra según"
1195 | 00000000000000000000000000001001 | | "su posición"
1196 +----------------------------------+ --/
1198 \--------------------------/|||| -\
1199 "bits de relleno" |||| |
1200 |||| | "Significado"
1201 "bit de 's'" |||| | "de bits"
1203 \---------------/||\---- "bit de 'begin2'" | "segunda"
1205 /---------------/\---- "bit de 'i'" |
1209 Si una implementación quisiera mover memoria (ver :ref:`gc_moving`), debería
1210 mantener inmóvil a cualquier objeto que sea apuntado por una palabra de estas
1211 características, ya que no es seguro actualizar la palabra con la nueva
1212 posición el objeto movido. Es por esta razón que se provee desglosada la
1213 información sobre lo que hay que escanear, y lo que es realmente un puntero
1214 (que puede ser actualizado de forma segura por el recolector de ser
1217 Implementación en el recolector
1218 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1219 La implementación está basada en la idea original de David Simcha, pero
1220 partiendo de la implementación de Vincent Lang (que está basada en Tango_)
1221 y consiste en almacenar el puntero a la estructura con la descripción del tipo
1222 generada por el compilador al final del bloque de datos. Este puntero solo se
1223 almacena si el bloque solicitado no tiene el atributo ``NO_SCAN``, dado que en
1224 ese caso no hace falta directamente escanear ninguna palabra del bloque.
1226 En la figura :vref:`fig:sol-ptrmap-blk` se puede ver, como continuación del
1227 ejemplo anterior, como se almacenaría en memoria un objeto del tipo ``S``.
1229 .. fig:: fig:sol-ptrmap-blk
1231 Ejemplo de bloque que almacena un objeto de tipo ``S`` con información de
1238 +------------------------ 256 bytes -----------------------------+
1241 +----------------------------------+-----------------------+-----+
1243 | Objeto | Desperdicio | Ptr |
1245 +----------------------------------+-----------------------+-----+
1248 +------------ 144 bytes -----------+------ 108 bytes ------+- 4 -+
1251 Un problema evidente de este esquema es que si el tamaño de un objeto se
1252 aproxima mucho al tamaño de bloque (difiere en menos de una palabra), el
1253 objeto ocupará el doble de memoria.
1255 El algoritmo de marcado se cambia de la siguiente forma::
1258 global conservative_scan = [1, 1, 0]
1261 function must_scan_word(pos, bits) is
1262 return bits[pos / BITS_PER_WORD] & (1 << (pos % BITS_PER_WORD))
1264 function mark_range(begin, end, ptrmap) is // Modificado
1265 number_of_words_in_type = ptrmap[0] // Agregado
1266 size_t* scan_bits = ptrmap + 1 // Agregado
1269 foreach word_pos in 0..number_of_words_in_type //
1270 if not must_scan_word(n, scan_bits) // Agregado
1272 [pool, page, block] = find_block(pointer)
1273 if block is not null and block.mark is false
1275 if block.noscan is false
1277 global more_to_scan = true
1278 pointer += number_of_words_in_type // Modificado
1280 function mark_heap() is
1281 while global more_to_scan
1282 global more_to_scan = false
1283 foreach pool in heap
1284 foreach page in pool
1285 if page.block_size <= PAGE // saltea FREE y CONTINUATION
1286 foreach block in page
1287 if block.scan is true
1289 if page.block_size is PAGE // obj grande //
1290 begin = cast(byte*) page //
1291 end = find_big_object_end(pool, page) //
1292 else // objeto pequeño //
1293 begin = block.begin //
1294 end = block.end // Modificado
1295 ptrmap = global conservative_scan //
1296 if NO_SCAN not in block.attrs //
1297 end -= size_t.sizeof //
1298 ptrmap = cast(size_t*) *end //
1299 mark_range(begin, end, ptrmap) //
1301 function mark_static_data() is
1302 mark_range(static_data.begin, static_data.end,
1303 global conservative_scan) // Agregado
1305 function mark_stacks() is
1306 foreach thread in threads
1307 mark_range(thread.stack.begin, thread.stack.end,
1308 global conservative_scan) // Agregado
1310 function mark_user_roots() is
1311 foreach root_range in user_roots
1312 mark_range(root_range.begin, root_range.end,
1313 global conservative_scan) // Agregado
1315 Las funciones de asignación de memoria se modifican de forma similar, para
1316 guardar el puntero a la información de tipos. Esta implementación utiliza solo
1317 la información sobre que palabras hay que tratar como punteros (deben ser
1318 escaneadas); la información sobre qué palabras son efectivamente punteros no
1319 se utiliza ya que no se mueven celdas.
1321 El algoritmo sigue siendo correcto, puesto que solamente se dejan de escanear
1322 palabras que el compilador sabe que no pueden ser punteros. Si bien el
1323 lenguaje permite almacenar punteros en una variable que no lo sea, esto es
1324 comportamiento indefinido por lo tanto un programa que lo hace no es
1325 considerado correcto, por lo cual el recolector tampoco debe ser correcto en
1326 esas circunstancias.
1328 Cabe destacar que la información de tipos solo se provee para objetos
1329 almacenados en el *heap*, el área de memoria estática, registros del
1330 procesador y la pila de todos los hilos siguen siendo escaneados de forma
1331 completamente conservativa. Se puede forzar el escaneo puramente conservativo
1332 utilizando la opción ``conservative`` (ver :ref:`sol_config_spec`).
1338 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1340 Finalmente se procede al objetivo primario de este trabajo, hacer que la fase
1341 más costosa del recolector (el marcado) pueda correr de manera concurrente con
1342 el *mutator*, con el objeto principal de disminuir el tiempo de pausa.
1344 Cabe aclarar, una vez más, que si bien los recolectores concurrentes buscan
1345 disminuir solo el tiempo de *stop-the-world*, en este caso es también
1346 fundamental disminuir el tiempo máximo que está tomado el *lock* global, dado
1347 que ese tiempo puede convertirse en una pausa para todos los threads que
1348 requieran servicios del recolector.
1350 Se decide basar la implementación en el *paper* "Non-intrusive Cloning Garbage
1351 Collector with Stock Operating System Support" [RODR97]_ por las siguientes
1352 razones principales:
1354 * Su implementación encaja de forma bastante natural con el diseño del
1355 recolector actual, por lo que requiere pocos cambios, lo que hace más
1356 factible su aceptación.
1357 * Está basado en la llamada al sistema :manpage:`fork(2)`, que no solo está
1358 muy bien soportada (y de manera muy eficiente) en Linux_, debe estar
1359 soportada en cualquier sistema operativo :term:`POSIX`.
1360 * No necesita instrumentar el código incluyendo barreras de memoria para
1361 informar al recolector cuando cambia el grafo de conectividad. Este es un
1362 aspecto fundamental, dada la filosofía de D_ de no pagar el precio de cosas
1363 que no se usan. La penalización en la eficiencia solo se paga cuando corre
1364 el recolector. Este aspecto también es crítico a la hora de evaluar la
1365 aceptación de la solución por parte de la comunidad.
1366 * Dada su sencillez general, no es difícil ofrecer el algoritmo concurrente
1367 como una opción, de manera que el usuario pueda optar por usarlo o no.
1369 Llamada al sistema *fork*
1370 ^^^^^^^^^^^^^^^^^^^^^^^^^
1371 El término *fork* proviene del inglés y significa *tenedor* de manera textual,
1372 pero se lo utiliza como analogía de una bifurcación. La operación crea una
1373 copia (llamada *hijo*) del proceso que la ejecuta (llamado *padre*).
1375 El punto más importante es que se crea un espacio de direcciones de memoria
1376 separado para el proceso hijo y una copia exacta de todos los segmentos de
1377 memoria del proceso padre. Es por esto que cualquier modificación que se haga
1378 en el proceso padre, no se refleja en el proceso hijo (y viceversa), a menos
1379 que la memoria sea compartida entre los procesos de forma explícita.
1381 Esto, sin embargo, no significa que la memoria física sea realmente duplicada;
1382 en general todos los sistemas operativos modernos (como Linux_) utilizan una
1383 técnica llamada *copy-on-write* (*copiar-al-escribir* en castellano) que
1384 retrasa la copia de memoria hasta que alguno de los dos procesos escribe en un
1385 segmento. Recién en ese momento el sistema operativo realiza la copia de **ese
1386 segmento solamente**. Es por esto que la operación puede ser muy eficiente,
1387 y la copia de memoria es proporcional a la cantidad de cambios que hayan.
1389 :manpage:`fork(2)` tiene otra propiedad importante de mencionar: detiene todos
1390 los hilos de ejecución en el proceso hijo. Es decir, el proceso hijo se crear
1391 con un solo hilo (el hilo que ejecutó la operación de :manpage:`fork(2)`).
1395 Lo que propone el algoritmo es muy sencillo, utilizar la llamada al sistema
1396 :manpage:`fork(2)` para crear una *fotografía* de la memoria del proceso en un
1397 nuevo proceso. En el proceso padre sigue corriendo el *mutator* y en el
1398 proceso hijo se corre la fase de marcado. El *mutator* puede modificar el
1399 grafo de conectividad pero los cambios quedan aislados el hijo (el marcado),
1400 que tiene una visión consistente e inmutable de la memoria. El sistema
1401 operativo duplica las páginas que modifica el padre bajo demanda, por lo tanto
1402 la cantidad de memoria física realmente copiada es proporcional a la cantidad
1403 y dispersión de los cambios que haga el *mutator*.
1405 La corrección del algoritmo se mantiene gracias a que la siguiente invariante
1408 Cuando una celda se convierte en basura, permanece como basura hasta ser
1409 reciclada por el recolector.
1411 Es decir, el *mutator* no puede *resucitar* una celda *muerta* y esta
1412 invariante se mantiene al correr la fase de marcado sobre una vista inmutable
1413 de la memoria. El único efecto introducido es que el algoritmo toma una
1414 aproximación más conservativa. Es decir, lo que sí puede pasar es que una
1415 celda que pasó a estar *muerta* una vez que la fase de marcado se inició, pero
1416 antes de que ésta termine, la celda no se reciclará hasta la próxima
1417 recolección, dado que este algoritmo no incluye una comunicación entre
1418 *mutator* y recolector para notificar cambios en el grafo de conectividad.
1419 Pero esto no afecta la corrección del algoritmo, ya que un recolector es
1420 correcto cuando nunca recicla celdas *vivas*.
1422 La única comunicación necesaria entre el *mutator* y el recolector son los
1423 bits de marcado (ver :ref:`dgc_impl`), dado que la fase de barrido debe correr
1424 en el proceso padre. No es necesaria ningún tipo de sincronización entre
1425 *mutator* y recolector más allá de que uno espera a que el otro finalice.
1427 Además de almacenar el conjunto de bits ``mark`` en memoria compartida entre
1428 el proceso padre e hijo (necesario para la fase de barrido), las
1429 modificaciones necesarias para hacer la fase de marcado concurrente son las
1430 siguientes [#solforkerr]_::
1432 function collect() is
1435 fflush(null) // evita que se duplique la salida de los FILE* abiertos
1436 if child_pid is 0 // proceso hijo
1438 exit(0) // termina el proceso hijo
1444 function mark_phase() is
1445 global more_to_scan = false
1446 // Borrado: stop_the_world()
1447 clear_mark_scan_bits()
1450 push_registers_into_stack()
1451 thread_self.stack.end = get_stack_top()
1453 pop_registers_from_stack()
1456 // Borrado: start_the_world()
1458 Como se puede observar, el cambio es extremadamente sencillo. Sigue siendo
1459 necesario un tiempo mínimo de pausa (básicamente el tiempo que tarda la
1460 llamada al sistema operativo :manpage:`fork(2)`) para guardar una vista
1461 consistente de los registros del CPU y *stacks* de los hilos. Si bien el
1462 conjunto de bits ``mark`` es compartido por el proceso padre e hijo dado que
1463 es necesario para *comunicar* las fases de marcado y barrido, cabe notar que
1464 nunca son utilizados de forma concurrente (la fase de barrido espera que la
1465 fase de marcado termine antes de usar dichos bits), por lo tanto no necesitan
1466 ningún tipo de sincronización y nunca habrá más de una recolección en proceso
1467 debido al *lock* global del recolector.
1469 A pesar de que con estos cambios el recolector técnicamente corre de forma
1470 concurrente, se puede apreciar que para un programa con un solo hilo el
1471 tiempo máximo de pausa seguirá siendo muy grande, incluso más grande que antes
1472 dado el trabajo extra que impone crear un nuevo proceso y duplicar las páginas
1473 de memoria modificadas. Lo mismo le pasará a cualquier hilo que necesite hacer
1474 uso del recolector mientras hay una recolección en proceso, debido al *lock*
1477 Para bajar este tiempo de pausa se experimenta con dos nuevas mejoras, que se
1478 describen a continuación, cuyo objetivo es correr la fase de marcado de forma
1479 concurrente a **todos** los hilos, incluyendo el hilo que la disparó.
1481 .. [#solforkerr] Se omite el manejo de errores y la activación/desactivación
1482 del marcado concurrente a través de opciones del recolector para facilitar
1483 la comprensión del algoritmo y los cambios realizados. Si devuelve con
1484 error la llamada a ``fork()`` o ``waitpid()``, se vuelve al esquema
1485 *stop-the-world* como si se hubiera desactivado el marcado concurrente
1486 utilizando la opción del recolector ``fork=0``.
1489 .. _sol_eager_alloc:
1491 Creación ansiosa de *pools* (*eager allocation*)
1492 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1493 Esta mejora, que puede ser controlada a través de la opción ``eager_alloc``
1494 (ver :ref:`sol_config_spec`), consiste en crear un nuevo *pool* cuando un
1495 pedido de memoria no puede ser satisfecho, justo después de lanzar la
1496 recolección. Esto permite al recolector satisfacer la petición de memoria
1497 inmediatamente, corriendo la fase de marcado de forma realmente concurrente,
1498 incluso para programas con un solo hilo o programas cuyos hilos usan
1499 frecuentemente servicios del recolector. El precio a pagar es un mayor uso de
1500 memoria de forma temporal (y el trabajo extra de crear y eliminar *pools* más
1501 frecuentemente), pero es esperable que el tiempo máximo de pausa **real** se
1502 vea drásticamente disminuido.
1504 A simple vista las modificaciones necesarias para su implementación parecieran
1505 ser las siguientes::
1511 function mark_is_running() is
1512 return global mark_pid != 0
1514 function collect() is
1515 if mark_is_running() //
1516 finished = try_wait(global mark_pid) //
1517 if finished // Agregado
1524 if child_pid is 0 // proceso hijo
1529 // Borrado: wait(child_pid)
1530 global mark_pid = child_pid
1532 Sin embargo con sólo estas modificaciones el algoritmo deja de ser correcto,
1533 ya que tres cosas problemáticas pueden suceder:
1535 1. Puede llamarse a la función ``minimize()`` mientras hay una fase de marcado
1536 corriendo en paralelo. Esto puede provocar que se libere un *pool* mientras
1537 se lo está usando en la fase de marcado, lo que no sería un problema
1538 (porque el proceso de marcado tiene una copia) si no fuera porque los bits
1539 de marcado, que son compartidos por los procesos, se liberan con el *pool*.
1540 2. Si un bloque libre es asignado después de que la fase de marcado comienza,
1541 pero antes de que termine, ese bloque será barrido dado la función
1542 ``rebuild_free_lists()`` puede reciclar páginas si todos sus bloques tienen
1543 el bit ``freebits`` activo (ver :ref:`dgc_algo_sweep`).
1544 3. El *pool* creado ansiosamente, tendrá sus bits de marcado sin activar, por
1545 lo que en la fase de barrido será interpretado como memoria libre, incluso
1546 cuando puedan estar siendo utilizados por el *mutator*.
1548 El punto 1 sencillamente hace que el programa finalice con una violación de
1549 segmento (en el mejor caso) y 2 y 3 pueden desembocar en la liberación de una
1550 celda alcanzable por el *mutator*.
1552 El punto 1 se resuelve a través de la siguiente modificación::
1554 function minimize() is
1555 if mark_is_running() // Agregado
1560 if page.block_size is not FREE
1568 La resolución del punto 2 es un poco más laboriosa, ya que hay que mantener
1569 actualizado los ``freebits``, de forma que las celdas asignadas después de
1570 empezar la fase de marcado no sean barridas por tener ese bit activo::
1572 function new_big(size) is
1573 number_of_pages = ceil(size / PAGE_SIZE)
1574 pages = find_pages(number_of_pages)
1577 pages = find_pages(number_of_pages)
1580 pool = new_pool(number_of_pages)
1583 pages = assign_pages(pool, number_of_pages)
1584 pages[0].block.free = true // Agregado
1585 pages[0].block_size = PAGE
1586 foreach page in pages[1..end]
1587 page.block_size = CONTINUATION
1590 function assign_page(block_size) is
1591 foreach pool in heap
1592 foreach page in pool
1593 if page.block_size is FREE
1594 page.block_size = block_size
1595 foreach block in page
1596 block.free = true // Agregado
1597 free_lists[page.block_size].link(block)
1599 function mark_phase() is
1600 global more_to_scan = false
1601 // Borrado: clear_mark_scan_bits()
1602 // Borrado: mark_free_lists()
1603 clear_scan_bits() // Agregado
1606 push_registers_into_stack()
1607 thread_self.stack.end = get_stack_top()
1609 pop_registers_from_stack()
1614 function clear_scan_bits() is
1615 // La implementación real limpia los bits en bloques de forma eficiente
1616 foreach pool in heap
1617 foreach page in pool
1618 foreach block in page
1622 function mark_free() is
1623 // La implementación real copia los bits en bloques de forma eficiente
1624 foreach pool in heap
1625 foreach page in pool
1626 foreach block in page
1627 block.mark = block.free
1629 function free_big_object(pool, page) is
1630 pool_end = cast(byte*) pool.pages + (PAGE_SIZE * pool.number_of_pages)
1632 page.block_size = FREE
1633 page.block.free = true // Agregado
1634 page = cast(byte*) page + PAGE_SIZE
1635 while page < pool_end and page.block_size is CONTINUATION
1637 function new(size, attrs) is
1638 block_size = find_block_size(size)
1639 if block_size < PAGE
1640 block = new_small(block_size)
1642 block = new_big(size)
1649 block.free = false // Agregado
1650 return cast(void*) block
1652 funciones new_pool(number_of_pages = 1) is
1653 pool = alloc(pool.sizeof)
1656 pool.number_of_pages = number_of_pages
1657 pool.pages = alloc(number_of_pages * PAGE_SIZE)
1658 if pool.pages is null
1662 foreach page in pool
1663 page.block_size = FREE
1664 foreach block in page //
1665 block.free = true // Agregado
1666 block.mark = true //
1669 Finalmente, el punto número tres puede ser solucionado con el siguiente
1672 funciones new_pool(number_of_pages = 1) is
1673 pool = alloc(pool.sizeof)
1676 pool.number_of_pages = number_of_pages
1677 pool.pages = alloc(number_of_pages * PAGE_SIZE)
1678 if pool.pages is null
1682 foreach page in pool
1683 page.block_size = FREE
1684 foreach block in page // Agregado
1685 block.mark = true //
1688 La solución es conservativa porque, por un lado evita la liberación de *pools*
1689 mientras haya una recolección en curso (lo que puede hacer que el consumo de
1690 memoria sea un poco mayor al requerido) y por otro asegura que, como se
1691 mencionó anteriormente, los cambios hechos al grafo de conectividad luego de
1692 iniciar la fase de marcado y antes de que ésta termine, no serán detectados
1693 por el recolector hasta la próxima recolección (marcar todos los bloques de
1694 un nuevo *pool* como el bit ``mark`` asegura que que la memoria no sea
1695 recolectada por la fase de barrido cuando termine el marcado).
1697 Estas modificaciones son las que hacen que el algoritmo siga siendo correcto,
1698 asegurando que no se van a liberar celdas *vivas* (a expensas de diferir la
1699 liberación de algunas celdas *muertas* por algún tiempo).
1702 .. _sol_early_collect:
1704 Recolección temprana (*early collection*)
1705 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1706 Esta mejora, que puede ser controlada a través de la opción ``early_collect``
1707 (ver :ref:`sol_config_spec`), consiste en lanzar una recolección preventiva,
1708 antes de que una petición de memoria falle. El momento en que se lanza la
1709 recolección es controlado por la opción ``min_free`` (ver :ref:`sol_ocup`).
1711 De esta forma también puede correr de forma realmente concurrente el *mutator*
1712 y el recolector, al menos hasta que se acabe la memoria, en cuyo caso, a menos
1713 que la opción ``eager_alloc`` (ver :ref:`sol_eager_alloc`) también esté
1714 activada, se deberá esperar a que la fase de marcado termine para recuperar
1715 memoria en la fase de barrido.
1717 Para facilitar la comprensión de esta mejora se muestran sólo los cambios
1718 necesarios si no se utiliza la opción ``eager_alloc``::
1720 function collect(early = false) is // Modificado
1721 if mark_is_running()
1722 finished = try_wait(global mark_pid)
1727 else if early // Agregado
1732 if child_pid is 0 // proceso hijo
1738 global mark_pid = child_pid //
1744 function early_collect() is
1745 if not collect_in_progress() and (percent_free < min_free)
1748 function new(size, attrs) is
1749 block_size = find_block_size(size)
1750 if block_size < PAGE
1751 block = new_small(block_size)
1753 block = new_big(size)
1760 early_collect() // Agregado
1761 return cast(void*) block
1763 Es de esperarse que cuando no está activa la opción ``eager_alloc`` por un
1764 lado el tiempo de pausa máximo no sea tan chico como cuando sí lo está (dado
1765 que si la recolección no se lanza de forma suficientemente temprana se va
1766 a tener que esperar que la fase de marcado termine), y por otro que se hagan
1767 más recolecciones de lo necesario (cuando pasa lo contrario, se recolecta más
1768 temprano de lo que se debería). Sin embargo, también es de esperarse que el
1769 consumo de memoria sea un poco menor que al usar la opción ``eager_alloc``.
1771 En cuanto a la corrección del algoritmo, éste solamente presenta los problemas
1772 número 1 y 2 mencionados en :ref:`sol_eager_alloc`, dado que jamás se crean
1773 nuevos *pools* y la solución es la ya presentada, por lo tanto el algoritmo
1774 sigue siendo correcto con los cuidados pertinentes.
1779 ----------------------------------------------------------------------------
1781 Los resultados de las modificación propuestas en la sección anterior (ver
1782 :ref:`sol_mod`) se evalúan utilizando el conjunto de pruebas mencionado en la
1783 sección :ref:`sol_bench`).
1785 En esta sección se describe la forma en la que el conjunto de pruebas es
1786 utilizado, la forma en la que se ejecutan los programas para recolectar dichos
1787 resultados y las métricas principales utilizadas para analizarlos.
1789 A fines prácticos, y haciendo alusión al nombre utilizado por Tango_, en esta
1790 sección se utiliza el nombre **TBGC** (acrónimo para el nombre en inglés
1791 *Tango Basic Garbage Collector*) para hacer referencia al recolector original
1792 provisto por Tango_ 0.99.9 (que, recordamos, es el punto de partida de este
1793 trabajo). Por otro lado, y destacando la principal modificación propuesta por
1794 este trabajo, haremos referencia al recolector resultante de éste utilizando
1795 el nombre **CDGC** (acrónimo para el nombre en inglés *Concurrent D Garbage
1799 Ejecución del conjunto de pruebas
1800 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1802 Dado el indeterminismo inherente a los sistemas operativos de tiempo
1803 compartido modernos, se hace un particular esfuerzo por obtener resultados lo
1804 más estable posible.
1806 Hardware y software utilizado
1807 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1808 Para realizar las pruebas se utiliza el siguiente hardware:
1810 * Procesador Intel(R) Core(TM)2 Quad CPU Q8400 @ 2.66GHz.
1811 * 2GiB de memoria RAM.
1813 El entorno de software es el siguiente:
1815 * Sistema operativo Debian_ Sid (para arquitectura *amd64*).
1817 * DMD_ 1.063 modificado para proveer información de tipos al recolector (ver
1818 :ref:`sol_precise`).
1819 * *Runtime* Tango_ 0.99.9 modificado para utilizar la información de tipos
1820 provista por el compilador modificado.
1822 * Embedded GNU_ C Library 2.11.2.
1824 Si bien el sistema operativo utiliza arquitectura *amd64*, dado que DMD_
1825 todavía no soporta 64 bits, se compila y corren los programas de D_ en 32
1828 Opciones del compilador
1829 ^^^^^^^^^^^^^^^^^^^^^^^
1830 Los programas del conjunto de pruebas se compilan utilizando las siguientes
1831 opciones del compilador DMD_:
1834 Aplica optimizaciones generales.
1837 Aplica la optimización de expansión de funciones. Consiste en sustituir la
1838 llamada a función por el cuerpo de la función (en general solo para
1839 funciones pequeñas).
1842 No genera el código para verificar pre y post-condiciones, invariantes de
1843 representación, operaciones fuera de los límites de un arreglo y
1844 *assert*\ 's en general (ver :ref:`d_dbc`).
1846 Parámetros de los programas
1847 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
1848 Los programas de prueba se ejecutan siempre con los mismos parámetros (a menos
1849 que se especifique lo contrario), que se detallan a continuación.
1856 Procesa 40 veces un archivo de texto plano (de 4MiB de tamaño) [#solbible]_
1857 utilizando 4 hilos (más el principal).
1862 Procesa 40 veces un archivo de texto plano (de 4MiB de tamaño) [#solbible]_
1863 utilizando 4 hilos (más el principal).
1868 Procesa dos veces un archivo de texto plano (de 4MiB de tamaño)
1874 Construyen árboles con profundidad máxima 16.
1879 Computa las interacciones gravitatorias entre 4.000 cuerpos.
1884 Ordena alrededor de 2 millones de números (exactamente :math:`2^21
1888 ``-n 4000 -d 300 -i 74``
1890 Realiza 74 iteraciones para modelar 4.000 nodos con grado 300.
1895 Resuelve el problema del viajante a través de una heurística para un
1901 Se construye un diagrama con 30.000 nodos.
1904 ``ddoc $dst_dir -hl --kandil -version=Tango -version=TangoDoc
1905 -version=Posix -version=linux $tango_files``
1907 Genera la documentación de todo el código fuente de Tango_ 0.99.9, donde
1908 ``$dst_dir`` es el directorio donde almacenar los archivos generados
1909 y ``$tango_files`` es la lista de archivos fuente de Tango_.
1913 El resto de los programas se ejecutan sin parámetros (ver :ref:`sol_bench`
1914 para una descripción detallada sobre cada uno).
1916 .. [#solbible] El archivo contiene la Biblia completa, la versión traducida al
1917 inglés autorizada por el Rey Jaime o Jacobo (*Authorized King James
1918 Version* en inglés). Obtenida de: http://download.o-bible.com:8080/kjv.gz
1920 Recolectores y configuraciones utilizadas
1921 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1922 En general se presentan resultados para TBGC y varias configuraciones de CDGC,
1923 de manera de poder tener una mejor noción de que mejoras y problemas puede
1924 introducir cada una de las modificaciones más importantes.
1926 CDGC se utiliza con siguientes configuraciones:
1931 En modo conservativo. Específicamente, utilizando el juego de opciones::
1933 conservative=1:fork=0:early_collect=0:eager_alloc=0
1936 En modo preciso (ver :ref:`sol_precise`). Específicamente, utilizando el
1939 conservative=0:fork=0:early_collect=0:eager_alloc=0
1942 En modo preciso activando el marcado concurrente (ver :ref:`sol_fork`).
1943 Específicamente, utilizando el juego de opciones::
1945 conservative=0:fork=1:early_collect=0:eager_alloc=0
1948 En modo preciso activando el marcado concurrente con recolección temprana
1949 (ver :ref:`sol_early_collect`). Específicamente, utilizando el juego de
1952 conservative=0:fork=1:early_collect=1:eager_alloc=0
1955 En modo preciso activando el marcado concurrente con creación ansiosa de
1956 *pools* (ver :ref:`sol_eager_alloc`). Específicamente, utilizando el juego
1959 conservative=0:fork=1:early_collect=0:eager_alloc=1
1962 En modo preciso activando el marcado concurrente con recolección temprana
1963 y creación ansiosa de *pools*. Específicamente, utilizando el juego de
1966 conservative=0:fork=1:early_collect=1:eager_alloc=1
1972 Para analizar los resultados se utilizan varias métricas. Las más importantes
1975 * Tiempo total de ejecución.
1976 * Tiempo máximo de *stop-the-world*.
1977 * Tiempo máximo de pausa real.
1978 * Cantidad máxima de memoria utilizada.
1979 * Cantidad total de recolecciones realizadas.
1981 El tiempo total de ejecución es una buena medida del **rendimiento** general
1982 del recolector, mientras que la cantidad total de recolecciones realizadas
1983 suele ser una buena medida de su **eficacia** [#soleficacia]_.
1985 Los tiempos máximos de pausa, *stop-the-world* y real, son una buena medida de
1986 la **latencia** del recolector; el segundo siendo una medida más realista dado
1987 que es raro que los demás hilos no utilicen servicios del recolector mientras
1988 hay una recolección en curso. Esta medida es particularmente importante para
1989 programas que necesiten algún nivel de ejecución en *tiempo-real*.
1991 En general el consumo de tiempo y espacio es un compromiso, cuando se consume
1992 menos tiempo se necesita más espacio y viceversa. La cantidad máxima de
1993 memoria utilizada nos da un parámetro de esta relación.
1995 .. [#soleficacia] Esto no es necesariamente cierto para recolectores con
1996 particiones (ver :ref:`gc_part`) o incrementales (ver :ref:`gc_inc`), dado
1997 que en ese caso podría realizar muchas recolecciones pero cada una muy
2000 Métodología de medición
2001 ^^^^^^^^^^^^^^^^^^^^^^^
2002 Para medir el tiempo total de ejecución se utiliza el comando
2003 :manpage:`time(1)` con la especificación de formato ``%e``, siendo la medición
2004 más realista porque incluye el tiempo de carga del ejecutable, inicialización
2005 del *runtime* de D_ y del recolector.
2007 Todas las demás métricas se obtienen utilizando la salida generada por la
2008 opción ``collect_stats_file`` (ver :ref:`sol_stats`), por lo que no pueden ser
2009 medidos para TBGC. Sin embargo se espera que para esos casos los resultados no
2010 sean muy distintos a CDGC utilizando la configuración **cons** (ver sección
2013 Cabe destacar que las corridas para medir el tiempo total de ejecución no son
2014 las mismas que al utilizar la opción ``collect_stats_file``; cuando se mide el
2015 tiempo de ejecución no se utiliza esa opción porque impone un trabajo extra
2016 importante y perturbaría demasiado la medición del tiempo. Sin embargo, los
2017 tiempos medidos internamente al utilizar la opción ``collect_stats_file`` son
2018 muy precisos, dado que se hace un particular esfuerzo para que no se haga un
2019 trabajo extra mientras se está midiendo el tiempo.
2021 Al obtener el tiempo de *stop-the-world* se ignoran los apariciones del valor
2022 ``-1``, que indica que se solicitó una recolección pero que ya había otra en
2023 curso, por lo que no se pausan los hilos realmente. Como tiempo de pausa real
2024 (ver :ref:`sol_fork` para más detalles sobre la diferencia con el tiempo de
2025 *stop-the-world*) se toma el valor del tiempo que llevó la asignación de
2026 memoria que disparó la recolección.
2028 Para medir la cantidad de memoria máxima se calcula el valor máximo de la
2029 sumatoria de: memoria usada, memoria libre, memoria desperdiciada y memoria
2030 usada por el mismo recolector (es decir, el total de memoria pedida por el
2031 programa al sistema operativo, aunque no toda este siendo utilizada por el
2032 *mutator* realmente).
2034 Por último, la cantidad total de recolecciones realizadas se calcula contando
2035 la cantidad de entradas del archivo generado por ``collect_stats_file``,
2036 ignorando la cabecera y las filas cuyo valor de tiempo de *stop-the-world* es
2037 ``-1``, debido a que en ese caso no se disparó realmente una recolección dado
2038 que ya había una en curso.
2040 Además, ciertas pruebas se corren variando la cantidad de procesadores
2041 utilizados, para medir el impacto de la concurrencia en ambientes con un
2042 procesador solo y con múltiples procesadores. Para esto se utiliza el comando
2043 :manpage:`taskset`, que establece la *afinidad* de un proceso, *atándolo*
2044 a correr en un cierto conjunto de procesadores. Si bien las pruebas se
2045 realizan utilizando 1, 2, 3 y 4 procesadores, los resultados presentados en
2046 general se limitan a 1 y 4 procesadores, ya que no se observan diferencias
2047 sustanciales al utilizar 2 o 3 procesadores con respecto a usar 4 (solamente
2048 se ven de forma más atenuadas las diferencias entre la utilización de
2049 1 o 4 procesadores). Dado que de por sí ya son muchos los datos a procesar
2050 y analizar, agregar más resultados que no aportan información valiosa termina
2051 resultando contraproducente.
2053 En los casos donde se utilizan otro tipo de métricas para evaluar aspectos
2054 particulares sobre alguna modificación se describe como se realiza la medición
2055 donde se utiliza la métrica especial.
2057 Variabilidad de los resultados entre ejecuciones
2058 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2059 Es de esperarse que haya una cierta variación en los resultados entre
2060 corridas, dada la indeterminación inherente a los sistemas operativos de
2061 tiempo compartido, que compiten por los recursos de la computadora.
2063 Para minimizar esta variación se utilizan varias herramientas. En primer
2064 lugar, se corren las pruebas estableciendo máxima prioridad (-19 en Linux_) al
2065 proceso utilizando el comando :manpage:`nice(1)`. La variación en la
2066 frecuencia del reloj los procesadores (para ahorrar energía) puede ser otra
2067 fuente de variación, por lo que se usa el comando :manpage:`cpufreq-set(1)`
2068 para establecer la máxima frecuencia disponible de manera fija.
2070 Sin embargo, a pesar de tomar estas precauciones, se sigue observando una
2071 amplia variabilidad entre corridas. Además se observa una variación más
2072 importante de la esperada no solo en el tiempo, también en el consumo de
2073 memoria, lo que es más extraño. Esta variación se debe principalmente a que
2074 Linux_ asigna el espacio de direcciones a los procesos con una componente
2075 azarosa (por razones de seguridad). Además, por omisión, la llamada al sistema
2076 :manpage:`mmap(2)` asigna direcciones de memoria altas primero, entregando
2077 direcciones más bajas en llamadas subsiguientes [LWN90311]_.
2079 El comando :manpage:`setarch(8)` sirve para controlar éste y otros aspectos de
2080 Linux_. La opción ``-L`` hace que se utilice un esquema de asignación de
2081 direcciones antiguo, que no tiene una componente aleatoria y asigna primero
2082 direcciones bajas. La opción ``-R`` solamente desactiva la componente azarosa
2083 al momento de asignar direcciones.
2085 .. ftable:: t:sol-setarch
2087 Variación entre corridas para TBGC.
2089 Variación entre corridas para TBGC. La medición está efectuada utilizando
2090 los valores máximo, mínimo y media estadística de 20 corridas, utilizando
2091 la siguiente métrica: :math:`\frac{max - min}{\mu}`. La medida podría
2092 realizarse utilizando el desvío estándar en vez de la amplitud máxima, pero
2093 en este cuadro se quiere ilustrar la variación máxima, no la típica.
2097 Del tiempo total de ejecución.
2099 ======== ======== ======== ========
2100 Programa Normal ``-R`` ``-L``
2101 ======== ======== ======== ========
2102 bh 0.185 0.004 0.020
2103 bigarr 0.012 0.002 0.016
2104 bisort 0.006 0.003 0.006
2105 conalloc 0.004 0.004 0.004
2106 concpu 0.272 0.291 0.256
2107 dil 0.198 0.128 0.199
2108 em3d 0.006 0.033 0.029
2109 mcore 0.009 0.009 0.014
2110 rnddata 0.015 0.002 0.011
2111 sbtree 0.012 0.002 0.012
2112 split 0.025 0.000 0.004
2113 tsp 0.071 0.068 0.703
2114 voronoi 0.886 0.003 0.006
2115 ======== ======== ======== ========
2119 Del consumo máximo de memoria.
2121 ======== ======== ======== ========
2122 Programa Normal ``-R`` ``-L``
2123 ======== ======== ======== ========
2124 bh 0.001 0.000 0.001
2125 bigarr 0.001 0.000 0.001
2126 bisort 0.000 0.000 0.000
2127 conalloc 0.753 0.000 0.001
2128 concpu 0.002 0.000 0.001
2129 dil 0.055 0.028 0.013
2130 em3d 0.000 0.001 0.001
2131 mcore 0.447 0.482 0.460
2132 rnddata 0.000 0.000 0.000
2133 sbtree 0.000 0.000 0.000
2134 split 0.000 0.000 0.000
2135 tsp 0.000 0.001 0.000
2136 voronoi 0.001 0.000 0.000
2137 ======== ======== ======== ========
2139 Ambas opciones, reducen notablemente la variación en los resultados (ver
2140 cuadro :vref:`t:sol-setarch`). Esto probablemente se debe a la naturaleza
2141 conservativa del recolector, dado que la probabilidad de tener *falsos
2142 punteros* depende directamente de los valores de las direcciones de memoria,
2143 aunque las pruebas en la que hay concurrencia involucrada, se siguen viendo
2144 grandes variaciones, que probablemente estén vinculadas a problemas de
2145 sincronización que se ven expuestos gracias al indeterminismo inherente a los
2146 programas multi-hilo.
2148 Si bien se obtienen resultados más estables utilizando un esquema diferente al
2149 utilizado por omisión, se decide no hacerlo dado que las mediciones serían
2150 menos realistas. Los usuarios en general no usan esta opción y se presentaría
2151 una visión más acotada sobre el comportamiento de los programas. Sin embargo,
2152 para evaluar el este efecto en los resultados, siempre que sea posible se
2153 analizan los resultados de un gran número de corridas observando
2154 principalmente su mínima, media, máxima y desvío estándar.
2156 .. Tamaño del ejecutable (XXX: SEGUN LAS PRUEBAS NO FUCKING CAMBIA!!!)
2157 El tamaño del ejecutable es un factor importante. Cuanto más grande es el
2158 ejecutable, más parecieran variar los resultados. Por ejemplo se observa un
2159 incremento de la estabilidad de los resultados al eliminar toda información
2160 de depuración (*debug*) del ejecutable, utilizando el comando
2161 :manpage:`strip(1)` (*stripped*). En el cuadro :vref:`t:sol-exesize-tbgc`
2162 se puede ver la reducción del tamaño del ejecutable para TBGC cuando se
2163 elimina la información de depuración (4.25 veces más chico en promedio),
2164 mientas que en el cuadro :vref:`t:sol-exesize-cdgc` se puede ver CDGC (4.6
2165 veces más chico en promedio).
2166 .. ftable:: t:sol-exesize-tbgc
2167 Reducción del tamaño del ejecutable para TBGC.
2168 ======== ======== ======== ==============
2169 Nombre Debug Stripped Debug/Stripped
2170 ======== ======== ======== ==============
2171 bh 586517 138060 4.248
2172 bigarr 547687 192004 2.852
2173 bisort 485857 115164 4.219
2174 conalloc 616613 149848 4.115
2175 concpu 616575 149848 4.115
2176 dil 7293277 1859208 3.923
2177 em3d 505019 116324 4.341
2178 mcore 461767 105748 4.367
2179 rnddata 2832935 1492588 1.898
2180 sbtree 526402 129860 4.054
2181 split 589353 144308 4.084
2182 tree 462009 105844 4.365
2183 tsp 544901 128412 4.243
2184 voronoi 601259 141112 4.261
2185 ======== ======== ======== ==============
2186 .. ftable:: t:sol-exesize-cdgc
2187 Reducción del tamaño del ejecutable para CDGC.
2188 ======== ======== ======== ===============
2189 Nombre Debug Stripped Debug/Stripped
2190 ======== ======== ======== ===============
2191 bh 736115 159884 4.604
2192 bigarr 697406 213832 3.261
2193 bisort 635537 136988 4.639
2194 conalloc 766328 171676 4.464
2195 concpu 766294 171676 4.464
2196 dil 7442657 1881028 3.957
2197 em3d 658827 142248 4.632
2198 mcore 611486 127576 4.793
2199 rnddata 2986736 1518512 1.967
2200 sbtree 680217 155784 4.366
2201 split 739072 166136 4.449
2202 tree 611728 127672 4.791
2203 tsp 694581 150236 4.623
2204 voronoi 750847 162936 4.608
2205 ======== ======== ======== ===============
2206 TODO: Mostrar tiempos de corridas.
2209 .. Resultados generales
2210 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2212 .. Primero se presenta una visión global de los resultados, utilizando las
2213 métricas más importantes. Para generar los gráficos se utilizan los valores
2214 máximos (en blanco), mínimos (en negro), media y desvío estándar (en gris)
2215 calculados en base a, como mínimo, 20 corridas (para algunos casos se hacen
2219 Resultados para pruebas sintizadas
2220 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2222 A continuación se presentan los resultados obtenidos para las pruebas
2223 sintetizadas (ver :ref:`sol_bench_synth`). Se recuerda que este conjunto de
2224 resultados es útil para analizar ciertos aspectos puntuales de las
2225 modificaciones propuestas, pero en general distan mucho de como se comporta un
2226 programa real, por lo que los resultados deben ser analizados teniendo esto
2231 .. fig:: fig:sol-bigarr-1cpu
2233 Resultados para ``bigarr`` (utilizando 1 procesador).
2235 Resultados para ``bigarr`` (utilizando 1 procesador). Se presenta el
2236 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2237 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2238 ejecución) o 20 corridas (para el resto).
2242 Tiempo de ejecución (seg)
2244 .. image:: plots/time-bigarr-1cpu.pdf
2248 Cantidad de recolecciones
2250 .. image:: plots/ncol-bigarr-1cpu.pdf
2254 Uso máximo de memoria (MiB)
2256 .. image:: plots/mem-bigarr-1cpu.pdf
2260 *Stop-the-world* máximo (seg)
2262 .. image:: plots/stw-bigarr-1cpu.pdf
2266 Pausa real máxima (seg)
2268 .. image:: plots/pause-bigarr-1cpu.pdf
2270 .. fig:: fig:sol-bigarr-4cpu
2272 Resultados para ``bigarr`` (utilizando 4 procesadores).
2274 Resultados para ``bigarr`` (utilizando 4 procesadores). Se presenta el
2275 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2276 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2277 ejecución) o 20 corridas (para el resto).
2281 Tiempo de ejecución (seg)
2283 .. image:: plots/time-bigarr-4cpu.pdf
2287 Cantidad de recolecciones
2289 .. image:: plots/ncol-bigarr-4cpu.pdf
2293 Uso máximo de memoria (MiB)
2295 .. image:: plots/mem-bigarr-4cpu.pdf
2299 *Stop-the-world* máximo (seg)
2301 .. image:: plots/stw-bigarr-4cpu.pdf
2305 Pausa real máxima (seg)
2307 .. image:: plots/pause-bigarr-4cpu.pdf
2309 En la figura :vref:`fig:sol-bigarr-1cpu` se pueden observar los resultados
2310 para ``bigarr`` al utilizar un solo procesador. En ella se puede notar que el
2311 tiempo total de ejecución en general aumenta al utilizar CDGC, esto es
2312 esperable, dado esta prueba se limitan a usar servicios del recolector. Dado
2313 que esta ejecución utiliza solo un procesador y por lo tanto no se puede sacar
2314 provecho a la concurrencia, es de esperarse que el trabajo extra realizado por
2315 las modificaciones se vea reflejado en los resultados. En la
2316 :vref:`fig:sol-bigarr-4cpu` (resultados al utilizar 4 procesadores) se puede
2317 observar como al usar solamente *eager allocation* se recupera un poco el
2318 tiempo de ejecución, probablemente debido al incremento en la concurrencia
2319 (aunque no se observa el mismo efecto al usar *early collection*).
2321 Observando el tiempo total de ejecución, no se esperaba un incremento tan
2322 notorio al pasar de TBGC a una configuración equivalente de CDGC **cons**,
2323 haciendo un breve análisis de las posibles causas, lo más probable parece ser
2324 el incremento en la complejidad de la fase de marcado dada capacidad para
2325 marcar de forma precisa (aunque no se use la opción, se paga el precio de la
2326 complejidad extra y sin obtener los beneficios). Además se puede observar
2327 como el agregado de precisión al marcado mejora un poco las cosas (donde sí se
2328 obtiene rédito de la complejidad extra en el marcado).
2330 En general se observa que al usar *eager allocation* el consumo de memoria
2331 y los tiempos de pausa se disparan mientras que la cantidad de recolecciones
2332 disminuye drásticamente. Lo que se observa es que el programa es
2333 más veloz pidiendo memoria que recolectándola, por lo que crece mucho el
2334 consumo de memoria. Como consecuencia la fase de barrido (que no corre en
2335 paralelo al *mutator* como la fase de marcado) empieza a ser predominante en
2336 el tiempo de pausa por ser tan grande la cantidad de memoria a barrer. Este
2337 efecto se ve tanto al usar 1 como 4 procesadores, aunque el efecto es mucho
2338 más nocivo al usar 1 debido a la alta variabilidad que impone la competencia
2339 entre el *mutator* y recolector al correr de forma concurrente.
2341 Sin embargo, el tiempo de *stop-the-world* es siempre considerablemente más
2342 pequeño al utilizar marcado concurrente en CDGC, incluso cuando se utiliza
2343 *eager allocation*, aunque en este caso aumenta un poco, también debido al
2344 incremento en el consumo de memoria, ya que el sistema operativo tiene que
2345 copiar tablas de memoria más grandes al efectuar el *fork* (ver
2350 .. fig:: fig:sol-concpu-1cpu
2352 Resultados para ``concpu`` (utilizando 1 procesador).
2354 Resultados para ``concpu`` (utilizando 1 procesador). Se presenta el
2355 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2356 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2357 ejecución) o 20 corridas (para el resto).
2361 Tiempo de ejecución (seg)
2363 .. image:: plots/time-concpu-1cpu.pdf
2367 Cantidad de recolecciones
2369 .. image:: plots/ncol-concpu-1cpu.pdf
2373 Uso máximo de memoria (MiB)
2375 .. image:: plots/mem-concpu-1cpu.pdf
2379 *Stop-the-world* máximo (seg)
2381 .. image:: plots/stw-concpu-1cpu.pdf
2385 Pausa real máxima (seg)
2387 .. image:: plots/pause-concpu-1cpu.pdf
2389 .. fig:: fig:sol-concpu-4cpu
2391 Resultados para ``concpu`` (utilizando 4 procesadores).
2393 Resultados para ``concpu`` (utilizando 4 procesadores). Se presenta el
2394 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2395 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2396 ejecución) o 20 corridas (para el resto).
2400 Tiempo de ejecución (seg)
2402 .. image:: plots/time-concpu-4cpu.pdf
2406 Cantidad de recolecciones
2408 .. image:: plots/ncol-concpu-4cpu.pdf
2412 Uso máximo de memoria (MiB)
2414 .. image:: plots/mem-concpu-4cpu.pdf
2418 *Stop-the-world* máximo (seg)
2420 .. image:: plots/stw-concpu-4cpu.pdf
2424 Pausa real máxima (seg)
2426 .. image:: plots/pause-concpu-4cpu.pdf
2428 En la figura :vref:`fig:sol-concpu-1cpu` se pueden observar los resultados
2429 para ``concpu`` al utilizar un solo procesador. En ella se aprecia que el
2430 tiempo total de ejecución disminuye levemente al usar marcado concurrente
2431 mientras no se utilice *eager allocation* pero aumenta al utilizarlo.
2433 Con respecto a la cantidad de recolecciones, uso máximo de memoria y tiempo de
2434 *stop-the-world* se ve un efecto similar al descripto para ``bigarr`` (aunque
2435 magnificado), pero sorprendentemente el tiempo total de pausa se dispara,
2436 además con una variabilidad sorprendente, cuando se usa marcado concurrente
2437 (pero no *eager allocation*). Una posible explicación podría ser que al
2438 realizarse el *fork*, el sistema operativo muy probablemente entregue el
2439 control del único procesador disponible al resto de los hilos que compiten por
2440 él, por lo que queda mucho tiempo pausado en esa operación aunque realmente no
2441 esté haciendo trabajo alguno (simplemente no tiene tiempo de procesador para
2442 correr). Este efecto se cancela al usar *eager allocation* dado que el
2443 *mutator* nunca se bloquea esperando que el proceso de marcado finalice.
2445 Además se observa una caída importante en la cantidad de recolecciones al
2446 utilizar marcado concurrente. Esto probablemente se deba a que solo un hilo
2447 pide memoria (y por lo tanto dispara recolecciones), mientras los demás hilos
2448 también estén corriendo. Al pausarse todos los hilos por menos tiempo, el
2449 trabajo se hace más rápido (lo que explica la disminución del tiempo total de
2450 ejecución) y son necesarias menos recolecciones, por terminar más rápido
2451 también el hilo que las dispara.
2453 En la :vref:`fig:sol-concpu-4cpu` se pueden ver los resultados al utilizar
2454 4 procesadores, donde el panorama cambia sustancialmente. El efecto mencionado
2455 en el párrafo anterior no se observa más (pues el sistema operativo tiene más
2456 procesadores para asignar a los hilos) pero todos los resultados se vuelven
2457 más variables. Los tiempos de *stop-the-world* y pausa real (salvo por lo
2458 recién mencionado) crecen notablemente, al igual que su variación. No se
2459 encuentra una razón evidente para esto; podría ser un error en la medición
2460 dado que al utilizar todos los procesadores disponibles del *hardware*,
2461 cualquier otro proceso que compita por tiempo de procesador puede afectarla
2464 El tiempo total de ejecución crece considerablemente, como se espera, dado que
2465 el programa aprovecha los múltiples hilos que pueden correr en paralelo en
2466 procesadores diferentes.
2468 Sin embargo, no se encuentra una razón clara para explicar el crecimiento
2469 dramático en la cantidad de recolecciones solo al no usar marcado concurrente
2470 para 4 procesadores.
2474 .. fig:: fig:sol-conalloc-1cpu
2476 Resultados para ``conalloc`` (utilizando 1 procesador).
2478 Resultados para ``conalloc`` (utilizando 1 procesador). Se presenta el
2479 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2480 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2481 ejecución) o 20 corridas (para el resto).
2485 Tiempo de ejecución (seg)
2487 .. image:: plots/time-conalloc-1cpu.pdf
2491 Cantidad de recolecciones
2493 .. image:: plots/ncol-conalloc-1cpu.pdf
2497 Uso máximo de memoria (MiB)
2499 .. image:: plots/mem-conalloc-1cpu.pdf
2503 *Stop-the-world* máximo (seg)
2505 .. image:: plots/stw-conalloc-1cpu.pdf
2509 Pausa real máxima (seg)
2511 .. image:: plots/pause-conalloc-1cpu.pdf
2513 .. fig:: fig:sol-conalloc-4cpu
2515 Resultados para ``conalloc`` (utilizando 4 procesadores).
2517 Resultados para ``conalloc`` (utilizando 4 procesadores). Se presenta el
2518 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2519 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2520 ejecución) o 20 corridas (para el resto).
2524 Tiempo de ejecución (seg)
2526 .. image:: plots/time-conalloc-4cpu.pdf
2530 Cantidad de recolecciones
2532 .. image:: plots/ncol-conalloc-4cpu.pdf
2536 Uso máximo de memoria (MiB)
2538 .. image:: plots/mem-conalloc-4cpu.pdf
2542 *Stop-the-world* máximo (seg)
2544 .. image:: plots/stw-conalloc-4cpu.pdf
2548 Pausa real máxima (seg)
2550 .. image:: plots/pause-conalloc-4cpu.pdf
2552 En la figura :vref:`fig:sol-conalloc-1cpu` se pueden observar los resultados
2553 para ``conalloc`` al utilizar un solo procesador. Los cambios con respecto
2554 a lo observado para ``concpu`` son mínimos. El efecto de la mejoría al usar
2555 marcado concurrente pero no *eager allocation* no se observa más, dado que
2556 ``conalloc`` pide memoria en todos los hilos, se crea un cuello de botella. Se
2557 ve claramente como tampoco baja la cantidad de recolecciones hecha debido
2558 a esto y se invierte la variabilidad entre los tiempos pico de pausa real
2559 y *stop-the-world* (sin una razón obvia, pero probablemente relacionado que
2560 todos los hilos piden memoria).
2562 Al utilizar 4 procesadores (figura :vref:`fig:sol-conalloc-4cpu`), más allá de
2563 las diferencias mencionadas para 1 procesador, no se observan grandes cambios
2564 con respecto a lo observado para ``concpu``, excepto que los tiempos de pausa
2565 (real y *stop-the-world*) son notablemente más pequeños, lo que pareciera
2566 confirmar un error en la medición de ``concpu``.
2570 .. fig:: fig:sol-split-1cpu
2572 Resultados para ``split`` (utilizando 1 procesador).
2574 Resultados para ``split`` (utilizando 1 procesador). Se presenta el mínimos
2575 (en negro), la media centrada entre dos desvíos estándar (en gris), y el
2576 máximo (en blanco) calculados sobre 50 corridas (para tiempo de ejecución)
2577 o 20 corridas (para el resto).
2581 Tiempo de ejecución (seg)
2583 .. image:: plots/time-split-1cpu.pdf
2587 Cantidad de recolecciones
2589 .. image:: plots/ncol-split-1cpu.pdf
2593 Uso máximo de memoria (MiB)
2595 .. image:: plots/mem-split-1cpu.pdf
2599 *Stop-the-world* máximo (seg)
2601 .. image:: plots/stw-split-1cpu.pdf
2605 Pausa real máxima (seg)
2607 .. image:: plots/pause-split-1cpu.pdf
2609 Este es el primer caso donde se aprecia la sustancial mejora proporcionada por
2610 una pequeña optimización, el caché de ``findSize()`` (ver
2611 :ref:`sol_minor_findsize`). En la figura :vref:`fig:sol-split-1cpu` se puede
2612 observar con claridad como, para cualquier configuración de CDGC, hay una
2613 caída notable en el tiempo total de ejecución. Sin embargo, a excepción de
2614 cuando se utiliza *eager allocation*, la cantidad de recolecciones y memoria
2615 usada permanece igual.
2617 La utilización de *eager allocation* mejora (aunque de forma apenas
2618 apreciable) el tiempo de ejecución, la cantidad de recolecciones baja a un
2619 tercio y el tiempo de pausa real cae dramáticamente. Al usar marcado
2620 concurrente ya se observa una caída determinante en el tiempo de
2621 *stop-the-world*. Todo esto sin verse afectado el uso máximo de memoria,
2622 incluso al usar *eager allocation*.
2624 Se omiten los resultados para más de un procesador por ser prácticamente
2625 idénticos para este análisis.
2629 .. fig:: fig:sol-mcore-1cpu
2631 Resultados para ``mcore`` (utilizando 1 procesador).
2633 Resultados para ``mcore`` (utilizando 1 procesador). Se presenta el
2634 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2635 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2636 ejecución) o 20 corridas (para el resto).
2640 Tiempo de ejecución (seg)
2642 .. image:: plots/time-mcore-1cpu.pdf
2646 Cantidad de recolecciones
2648 .. image:: plots/ncol-mcore-1cpu.pdf
2652 Uso máximo de memoria (MiB)
2654 .. image:: plots/mem-mcore-1cpu.pdf
2658 *Stop-the-world* máximo (seg)
2660 .. image:: plots/stw-mcore-1cpu.pdf
2664 Pausa real máxima (seg)
2666 .. image:: plots/pause-mcore-1cpu.pdf
2668 .. fig:: fig:sol-mcore-4cpu
2670 Resultados para ``mcore`` (utilizando 4 procesadores).
2672 Resultados para ``mcore`` (utilizando 4 procesadores). Se presenta el
2673 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2674 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2675 ejecución) o 20 corridas (para el resto).
2679 Tiempo de ejecución (seg)
2681 .. image:: plots/time-mcore-4cpu.pdf
2685 Cantidad de recolecciones
2687 .. image:: plots/ncol-mcore-4cpu.pdf
2691 Uso máximo de memoria (MiB)
2693 .. image:: plots/mem-mcore-4cpu.pdf
2697 *Stop-the-world* máximo (seg)
2699 .. image:: plots/stw-mcore-4cpu.pdf
2703 Pausa real máxima (seg)
2705 .. image:: plots/pause-mcore-4cpu.pdf
2707 El caso de ``mcore`` es interesante por ser, funcionalmente, una combinación
2708 entre ``concpu`` y ``split``, con un agregado extra: el incremento notable de
2709 la competencia por utilizar el recolector entre los múltiples hilos.
2711 Los efectos observados (en la figura :vref:`fig:sol-mcore-1cpu` para
2712 1 procesador y en la figura :vref:`fig:sol-mcore-4cpu` para 4) confirman esto,
2713 al ser una suma de los efectos observados para ``concpu`` y ``split``, con el
2714 agregado de una particularidad extra por la mencionada competencia entre
2715 hilos. A diferencia de ``concpu`` donde el incremento de procesadores resulta
2716 en un decremento en el tiempo total de ejecución, en este caso resulta en una
2717 disminución, dado que se necesita mucha sincronización entre hilos, por
2718 utilizar todos de forma intensiva los servicios del recolector (y por lo tanto
2719 competir por su *lock* global).
2721 Otro efecto común observado es que cuando el tiempo de pausa es muy pequeño
2722 (del orden de los milisegundos), el marcado concurrente suele incrementarlo en
2727 .. fig:: fig:sol-rnddata-1cpu
2729 Resultados para ``rnddata`` (utilizando 1 procesador).
2731 Resultados para ``rnddata`` (utilizando 1 procesador). Se presenta el
2732 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2733 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2734 ejecución) o 20 corridas (para el resto).
2738 Tiempo de ejecución (seg)
2740 .. image:: plots/time-rnddata-1cpu.pdf
2744 Cantidad de recolecciones
2746 .. image:: plots/ncol-rnddata-1cpu.pdf
2750 Uso máximo de memoria (MiB)
2752 .. image:: plots/mem-rnddata-1cpu.pdf
2756 *Stop-the-world* máximo (seg)
2758 .. image:: plots/stw-rnddata-1cpu.pdf
2762 Pausa real máxima (seg)
2764 .. image:: plots/pause-rnddata-1cpu.pdf
2766 En la figura :vref:`fig:sol-rnddata-1cpu` se presentan los resultados para
2767 ``rnddata`` utilizando 1 procesador. Una vez más estamos ante un caso en el
2768 cual se observa claramente la mejoría gracias a una modificación en particular
2769 principalmente. En esta caso es el marcado preciso. Se puede ver claramente
2770 como mejora el tiempo de total de ejecución a algo más que la mitad (en
2771 promedio, aunque se observa una anomalía donde el tiempo baja hasta más de
2772 3 veces). Sin embargo, a menos que se utilice *eager allocation* o *early
2773 collection* (que en este caso prueba ser muy efectivo), la cantidad de
2774 recolecciones aumenta considerablemente.
2776 La explicación puede ser hallada en el consumo de memoria, que baja unas
2777 3 veces en promedio usando marcado preciso que además hace disminuir
2778 drásticamente (unas 10 veces) el tiempo de pausa (real y *stop-the-world*). El
2779 tiempo de *stop-the-world* disminuye unas 10 veces más al usar marcado
2780 concurrente y el tiempo de pausa real al usar *eager allocation*, pero en este
2781 caso el consumo de memoria aumenta también bastante (aunque no tanto como
2782 disminuye el tiempo de pausa, por lo que puede ser un precio que valga la pena
2783 pagar si se necesitan tiempos de pausa muy pequeños).
2785 El aumento en el variación de los tiempos de ejecución al usar marcado preciso
2786 probablemente se debe a lo siguiente: con marcado conservativo, debe estar
2787 sobreviviendo a las recolecciones el total de memoria pedida por el programa,
2788 debido a falsos punteros (por eso no se observa prácticamente variación en el
2789 tiempo de ejecución y memoria máxima consumida); al marcar con precisión
2790 parcial, se logra disminuir mucho la cantidad de falsos punteros, pero el
2791 *stack* y la memoria estática, se sigue marcado de forma conservativa, por lo
2792 tanto dependiendo de los valores (aleatorios) generados por la prueba, aumenta
2793 o disminuye la cantidad de falsos punteros, variando así la cantidad de
2794 memoria consumida y el tiempo de ejecución.
2796 No se muestran los resultados para más de un procesador por ser demasiado
2797 similares a los obtenidos utilizando solo uno.
2801 Los resultados para ``sbtree`` son tan similares a los obtenidos con
2802 ``bigarr`` que directamente se omiten por completo, dado que no aportan ningún
2803 tipo de información nueva. Por un lado es esperable, dado que ambas pruebas se
2804 limitan prácticamente a pedir memoria, la única diferencia es que una pide
2805 objetos grandes y otra objetos pequeños, pero esta diferencia parece no
2806 afectar la forma en la que se comportan los cambios introducidos en este
2810 Resultados para pruebas pequeñas
2811 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2813 A continuación se presentan los resultados obtenidos para las pruebas pequeñas
2814 (ver :ref:`sol_bench_small`). Se recuerda que si bien este conjunto de pruebas
2815 se compone de programas reales, que efectúan una tarea útil, están diseñados
2816 para ejercitar la asignación de memoria y que no son recomendados para evaluar
2817 el desempeño de recolectores de basura. Sin embargo se las utiliza igual por
2818 falta de programas más realistas, por lo que hay que tomarlas como un grado de
2823 .. fig:: fig:sol-bh-1cpu
2825 Resultados para ``bh`` (utilizando 1 procesador).
2827 Resultados para ``bh`` (utilizando 1 procesador). Se presenta el
2828 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2829 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2830 ejecución) o 20 corridas (para el resto).
2834 Tiempo de ejecución (seg)
2836 .. image:: plots/time-bh-1cpu.pdf
2840 Cantidad de recolecciones
2842 .. image:: plots/ncol-bh-1cpu.pdf
2846 Uso máximo de memoria (MiB)
2848 .. image:: plots/mem-bh-1cpu.pdf
2852 *Stop-the-world* máximo (seg)
2854 .. image:: plots/stw-bh-1cpu.pdf
2858 Pausa real máxima (seg)
2860 .. image:: plots/pause-bh-1cpu.pdf
2862 En la figura :vref:`fig:sol-bh-1cpu` se pueden observar los resultados
2863 para ``bh`` al utilizar un solo procesador. Ya en una prueba un poco más
2864 realista se puede observar el efecto positivo del marcado preciso, en especial
2865 en la cantidad de recolecciones efectuadas (aunque no se traduzca en un menor
2866 consumo de memoria).
2868 Sin embargo se observa también un efecto nocivo del marcado preciso en el
2869 consumo de memoria que intuitivamente debería disminuir, pero crece, y de
2870 forma considerable (unas 3 veces en promedio). La razón de esta particularidad
2871 es el incremento en el espacio necesario para almacenar objetos debido a que
2872 el puntero a la información del tipo se guarda al final del bloque (ver
2873 :ref:`sol_precise`). En el cuadro :vref:`t:sol-prec-mem-bh` se puede observar
2874 la cantidad de memoria pedida por el programa, la cantidad de memoria
2875 realmente asignada por el recolector (y la memoria desperdiciada) cuando se
2876 usa marcado conservativo y preciso. Estos valores fueron tomados usando la
2877 opción ``malloc_stats_file`` (ver :ref:`sol_stats`).
2879 .. ftable:: t:sol-prec-mem-bh
2881 Memoria pedida y asignada para ``bh`` según modo de marcado.
2883 Memoria pedida y asignada para ``bh`` según modo de marcado conservativo
2884 o preciso (acumulativo durante toda la vida del programa).
2886 ============== ============== ============== =================
2887 Memoria Pedida (MiB) Asignada (MiB) Desperdicio (MiB)
2888 ============== ============== ============== =================
2889 Conservativo 302.54 354.56 52.02 (15%)
2890 Preciso 302.54 472.26 169.72 (36%)
2891 ============== ============== ============== =================
2893 Más allá de esto, los resultados son muy similares a los obtenidos para
2894 pruebas sintetizadas que se limitan a ejercitar el recolector (como ``bigarr``
2895 y ``sbtree``), lo que habla de lo mucho que también lo hace este pequeño
2898 No se muestran los resultados para más de un procesador por ser extremadamente
2899 similares a los obtenidos utilizando solo uno.
2903 .. fig:: fig:sol-bisort-1cpu
2905 Resultados para ``bisort`` (utilizando 1 procesador).
2907 Resultados para ``bisort`` (utilizando 1 procesador). Se presenta el
2908 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2909 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2910 ejecución) o 20 corridas (para el resto).
2914 Tiempo de ejecución (seg)
2916 .. image:: plots/time-bisort-1cpu.pdf
2920 Cantidad de recolecciones
2922 .. image:: plots/ncol-bisort-1cpu.pdf
2926 Uso máximo de memoria (MiB)
2928 .. image:: plots/mem-bisort-1cpu.pdf
2932 *Stop-the-world* máximo (seg)
2934 .. image:: plots/stw-bisort-1cpu.pdf
2938 Pausa real máxima (seg)
2940 .. image:: plots/pause-bisort-1cpu.pdf
2942 La figura :vref:`fig:sol-bisort-1cpu` muestra los resultados para ``bisort``
2943 al utilizar 1 procesador. En este caso el parecido es con los resultados para
2944 la prueba sintetizada ``split``, con la diferencia que el tiempo de ejecución
2945 total prácticamente no varía entre TBGC y CDGC, ni entre las diferentes
2946 configuraciones del último (evidentemente en este caso no se aprovecha el
2947 caché de ``findSize()``).
2949 Otra diferencia notable es la considerable reducción del tiempo de pausa real
2950 al utilizar *early collection* (más de 3 veces menor en promedio comparado
2951 a cuando se marca conservativamente, y más de 2 veces menor que cuando se hace
2952 de forma precisa), lo que indica que la predicción de cuando se va a necesitar
2953 una recolección es más efectiva que para ``split``.
2955 No se muestran los resultados para más de un procesador por ser extremadamente
2956 similares a los obtenidos utilizando solo uno.
2960 .. fig:: fig:sol-em3d-1cpu
2962 Resultados para ``em3d`` (utilizando 1 procesador).
2964 Resultados para ``em3d`` (utilizando 1 procesador). Se presenta el
2965 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2966 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2967 ejecución) o 20 corridas (para el resto).
2971 Tiempo de ejecución (seg)
2973 .. image:: plots/time-em3d-1cpu.pdf
2977 Cantidad de recolecciones
2979 .. image:: plots/ncol-em3d-1cpu.pdf
2983 Uso máximo de memoria (MiB)
2985 .. image:: plots/mem-em3d-1cpu.pdf
2989 *Stop-the-world* máximo (seg)
2991 .. image:: plots/stw-em3d-1cpu.pdf
2995 Pausa real máxima (seg)
2997 .. image:: plots/pause-em3d-1cpu.pdf
2999 Los resultados para ``em3d`` (figura :vref:`fig:sol-em3d-1cpu`) son
3000 sorprendentemente similares a los de ``bisort``. La única diferencia es que en
3001 este caso el marcado preciso y el uso de *early collection** no parecen
3002 ayudar; por el contrario, aumentan levemente el tiempo de pausa real.
3004 Una vez más no se muestran los resultados para más de un procesador por ser
3005 extremadamente similares a los obtenidos utilizando solo uno.
3009 .. fig:: fig:sol-tsp-1cpu
3011 Resultados para ``tsp`` (utilizando 1 procesador).
3013 Resultados para ``tsp`` (utilizando 1 procesador). Se presenta el
3014 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
3015 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
3016 ejecución) o 20 corridas (para el resto).
3020 Tiempo de ejecución (seg)
3022 .. image:: plots/time-tsp-1cpu.pdf
3026 Cantidad de recolecciones
3028 .. image:: plots/ncol-tsp-1cpu.pdf
3032 Uso máximo de memoria (MiB)
3034 .. image:: plots/mem-tsp-1cpu.pdf
3038 *Stop-the-world* máximo (seg)
3040 .. image:: plots/stw-tsp-1cpu.pdf
3044 Pausa real máxima (seg)
3046 .. image:: plots/pause-tsp-1cpu.pdf
3048 Los resultados para ``tsp`` (figura :vref:`fig:sol-tsp-1cpu`) son
3049 prácticamente idénticos a los de ``bisort``. La única diferencia es que la
3050 reducción del tiempo de pausa real es un poco menor.
3052 Esto confirma en cierta medida la poca utilidad de este juego de pruebas para
3053 medir el rendimiento de un recolector, dado que evidentemente, si bien todas
3054 resuelven problemas diferentes, realizan todas el mismo tipo de trabajo.
3056 Una vez más no se muestran los resultados para más de un procesador por ser
3057 extremadamente similares a los obtenidos utilizando solo uno.
3061 .. fig:: fig:sol-voronoi-1cpu
3063 Resultados para ``voronoi`` (utilizando 1 procesador).
3065 Resultados para ``voronoi`` (utilizando 1 procesador). Se presenta el
3066 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
3067 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
3068 ejecución) o 20 corridas (para el resto).
3072 Tiempo de ejecución (seg)
3074 .. image:: plots/time-voronoi-1cpu.pdf
3078 Cantidad de recolecciones
3080 .. image:: plots/ncol-voronoi-1cpu.pdf
3084 Uso máximo de memoria (MiB)
3086 .. image:: plots/mem-voronoi-1cpu.pdf
3090 *Stop-the-world* máximo (seg)
3092 .. image:: plots/stw-voronoi-1cpu.pdf
3096 Pausa real máxima (seg)
3098 .. image:: plots/pause-voronoi-1cpu.pdf
3100 .. fig:: fig:sol-voronoi-4cpu
3102 Resultados para ``voronoi`` (utilizando 4 procesadores).
3104 Resultados para ``voronoi`` (utilizando 4 procesadores). Se presenta el
3105 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
3106 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
3107 ejecución) o 20 corridas (para el resto).
3111 Tiempo de ejecución (seg)
3113 .. image:: plots/time-voronoi-4cpu.pdf
3117 Cantidad de recolecciones
3119 .. image:: plots/ncol-voronoi-4cpu.pdf
3123 Uso máximo de memoria (MiB)
3125 .. image:: plots/mem-voronoi-4cpu.pdf
3129 *Stop-the-world* máximo (seg)
3131 .. image:: plots/stw-voronoi-4cpu.pdf
3135 Pausa real máxima (seg)
3137 .. image:: plots/pause-voronoi-4cpu.pdf
3139 En la figura :vref:`fig:sol-voronoi-1cpu` se presentan los resultados para
3140 ``voronoi``, probablemente la prueba más interesante de este conjunto de
3143 Por un lado se puede observar una vez más como baja dramáticamente el tiempo
3144 total de ejecución cuando se empieza a utilizar CDGC. Ya se ha visto que esto
3145 es común en programas que se benefician del caché de ``findSize()``, pero en
3146 este caso no parece provenir toda la ganancia solo de ese cambio, dado que
3147 para TBGC se ve una variación entre los resultados muy grande que desaparece
3148 al cambiar a CDGC, esto no puede ser explicado por esa optimización. En
3149 general la disminución de la variación de los resultados hemos visto que está
3150 asociada al incremento en la precisión en el marcado, dado que los falsos
3151 punteros ponen una cuota de aleatoriedad importante. Pero este tampoco parece
3152 ser el caso, ya que no se observan cambios apreciables al pasar a usar marcado
3155 Lo que se observa en esta oportunidad es un caso patológico de un mal factor
3156 de ocupación del *heap* (ver :ref:`sol_ocup`). Lo que muy probablemente está
3157 sucediendo con TBGC es que luego de ejecutar una recolección, se libera muy
3158 poco espacio, entonces luego de un par de asignaciones, es necesaria una nueva
3159 recolección. En este caso es donde dificulta la tarea de analizar los
3160 resultados la falta de métricas para TBGC, dado que no se pueden observar la
3161 cantidad de recolecciones ni de consumo máximo de memoria. Sin embargo es
3162 fácil corroborar esta teoría experimentalmente, gracias a la opción
3163 ``min_free``. Utilizando la ``min_free=0`` para emular el comportamiento de
3164 TBGC (se recuerda que el valor por omisión es ``min_free=5``), se obtiene una
3165 media de 4 segundos, mucho más parecida a lo obtenido para TBGC.
3167 Otra particularidad de esta prueba es que al utilizar *early collection* el
3168 tiempo de pausa real aumenta notablemente al usar un procesador, mientras que
3169 al usar 4 (ver figura :vref:`fig:sol-voronoi-4cpu` disminuye levemente (además
3170 de otros cambios en el nivel de variación, pero en general las medias no
3174 Resultados para pruebas reales
3175 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3177 A continuación se presentan los resultados obtenidos para las pruebas reales
3178 (ver :ref:`sol_bench_real`). Recordamos que solo se pudo halla un programa que
3179 pueda ser utilizado a este fin, Dil_, y que el objetivo principal de este
3180 trabajo se centra alrededor de obtener resultados positivos para este
3181 programa, por lo que a pesar de ser una única prueba, se le presta particular
3186 .. fig:: fig:sol-dil-1cpu
3188 Resultados para ``dil`` (utilizando 1 procesador).
3190 Resultados para ``dil`` (utilizando 1 procesador). Se presenta el
3191 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
3192 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
3193 ejecución) o 20 corridas (para el resto).
3197 Tiempo de ejecución (seg)
3199 .. image:: plots/time-dil-1cpu.pdf
3203 Cantidad de recolecciones
3205 .. image:: plots/ncol-dil-1cpu.pdf
3209 Uso máximo de memoria (MiB)
3211 .. image:: plots/mem-dil-1cpu.pdf
3215 *Stop-the-world* máximo (seg)
3217 .. image:: plots/stw-dil-1cpu.pdf
3221 Pausa real máxima (seg)
3223 .. image:: plots/pause-dil-1cpu.pdf
3225 .. fig:: fig:sol-dil-4cpu
3227 Resultados para ``dil`` (utilizando 4 procesadores).
3229 Resultados para ``dil`` (utilizando 4 procesadores). Se presenta el
3230 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
3231 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
3232 ejecución) o 20 corridas (para el resto).
3236 Tiempo de ejecución (seg)
3238 .. image:: plots/time-dil-4cpu.pdf
3242 Cantidad de recolecciones
3244 .. image:: plots/ncol-dil-4cpu.pdf
3248 Uso máximo de memoria (MiB)
3250 .. image:: plots/mem-dil-4cpu.pdf
3254 *Stop-the-world* máximo (seg)
3256 .. image:: plots/stw-dil-4cpu.pdf
3260 Pausa real máxima (seg)
3262 .. image:: plots/pause-dil-4cpu.pdf
3264 En la figura :vref:`fig:sol-dil-1cpu` se presentan los resultados para
3265 ``dil`` al utilizar un procesador. Una vez más vemos una mejoría inmediata del
3266 tiempo total de ejecución al pasar de TBGC a CDGC, y una vez más se debe
3267 principalmente al mal factor de ocupación del *heap* de TBGC, dado que
3268 utilizando CDGC con la opción ``min_free=0`` se obtiene una media del orden de
3269 los 80 segundos, bastante más alta que el tiempo obtenido para TBGC.
3271 Sin embargo se observa un pequeño incremento del tiempo de ejecución al
3272 introducir marcado preciso, y un incremento bastante más importante (de
3273 alrededor del 30%) en el consumo máximo de memoria. Nuevamente, como pasa con
3274 la prueba ``bh``, el efecto es probablemente producto del incremento en el
3275 espacio necesario para almacenar objetos debido a que el puntero a la
3276 información del tipo se guarda al final del bloque (ver :ref:`sol_precise`).
3277 En el cuadro :vref:`t:sol-prec-mem-dil` se puede observar la diferencia de
3278 memoria desperdiciada entre el modo conservativo y preciso.
3280 El pequeño incremento en el tiempo total de ejecución podría estar dado por la
3281 mayor probabilidad de tener *falsos punteros* debido al incremento del tamaño
3282 del *heap*; se recuerda que el *stack* y memoria estática se siguen marcado de
3283 forma conservativa, incluso en modo preciso.
3285 .. ftable:: t:sol-prec-mem-dil
3287 Memoria pedida y asignada para ``dil`` según modo de marcado.
3289 Memoria pedida y asignada para ``dil`` según modo de marcado conservativo
3290 o preciso (acumulativo durante toda la vida del programa).
3292 ============== ============== ============== =================
3293 Memoria Pedida (MiB) Asignada (MiB) Desperdicio (MiB)
3294 ============== ============== ============== =================
3295 Conservativo 307.48 399.94 92.46 (23%)
3296 Preciso 307.48 460.24 152.76 (33%)
3297 ============== ============== ============== =================
3299 También se puede observar una gran disminución del tiempo total de ejecución
3300 (cerca de un 60%, y más de un 200% comparado con TBGC) alrededor de la mitad)
3301 al empezar a usar *eager allocation*, acompañado como es usual de una baja en
3302 la cantidad de recolecciones realizadas (esta vez mayor, de más de 3 veces)
3303 y de una caída drástica del tiempo de pausa real (alrededor de 40 veces más
3304 pequeño); todo esto con un incremento marginal en el consumo total de memoria
3305 (aproximadamente un 5%). En este caso el uso de *early collection* apenas
3306 ayuda a bajar el tiempo de pausa real en un 20% en promedio aproximadamente.
3307 El tiempo de *stop-the-world* cae dramáticamente al empezar a realizar la fase
3308 de marcado de manera concurrente; es 200 veces más pequeño.
3310 Al utilizar 4 procesadores (ver figura :vref:`fig:sol-dil-4cpu`), hay algunos
3311 pequeños cambios. El tiempo total de ejecución es reducido todavía más (un 20%
3312 que cuando se usa 1 procesador) cuando se utiliza *eager allocation*. Además
3313 al utilizar *early collection*, hay otra pequeña ganancia de alrededor del
3314 10%, tanto para el tiempo total de ejecución como para el tiempo de pausa
3321 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3323 Los avances de este trabajo fueron comunicados regularmente a la comunidad de
3324 D_ a través de un blog [LMTDGC]_ y del grupo de noticias de D_. Los
3325 comentarios hechos sobre el primero son en general positivos y denotan una
3326 buena recepción por parte de la comunidad a las modificaciones propuestas.
3328 Una vez agregado el marcado concurrente se hace un anuncio en el grupo de
3329 noticias que también muestra buenos comentarios y aceptación, en particular
3330 por parte de Sean Kelly, encargado de mantener el *runtime* de `D 2.0`_, que
3331 comienza a trabajar en adaptar el recolector con idea de tal vez incluirlo en
3332 el futuro [NGA19235]_. Poco después Sean Kelly publica una versión preliminar
3333 de la adaptación en la lista de correos que coordina el desarrollo del
3334 *runtime* de `D 2.0`_ [DRT117]_.
3336 También se ha mostrado interés de incluirlo en Tango_, aunque no se han ha
3337 comenzado aún con la adaptación, pero debería ser trivial dado que este
3338 trabajo se desarrolla usando Tango_ (y el recolector está basado en el de
3342 .. include:: links.rst
3344 .. vim: set ts=3 sts=3 sw=3 et tw=78 spelllang=es :