2 .. Acá va lo que decidí hacer en base al análisis anterior y sus razones.
9 ============================================================================
11 Como hemos visto en :ref:`dgc`, la mejora del recolector de basura puede ser
12 abordada desde múltiples flancos, con varias alternativas viables. Por lo
13 tanto, para reducir la cantidad de posibilidades hay que tener en cuenta uno
14 de los principales objetivos de este trabajo: encontrar una solución que tenga
15 una buena probabilidad de ser adoptada por el lenguaje, o alguno de sus
16 compiladores al menos. Para asegurar esto, la solución debe tener un alto
17 grado de aceptación en la comunidad, lo 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ó para
283 paliar el problema de forma razonablemente efectiva [PAN09]_.
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
309 Este programa fue escrito por Oskar Linde y nuevamente hallado__ en el grupo
310 de noticias. Fue construido para mostrar como el hecho de que el recolector
311 sea conservativo puede hacer que al leer datos binarios hayan muchos *falsos
312 punteros* que mantengan vivas celdas que en realidad ya no deberían ser
313 accesibles desde el *root set* del grafo de conectividad.
315 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=46407
317 El código del programa es el siguiente::
319 import tango.math.random.Random;
321 const IT = 125; // number of iterations, each creates an object
322 const BYTES = 1_000_000; // ~1MiB per object
323 const N = 50; // ~50MiB of initial objects
327 C c; // makes the compiler not set NO_SCAN
328 long[BYTES/long.sizeof] data;
332 auto rand = new Random();
335 foreach (ref o; objs) {
337 foreach (ref x; o.data)
340 for (int i = 0; i < IT; ++i) {
342 foreach (ref x; o.data)
344 // do something with the data...
351 Este programa está basado en la prueba de nombre ``binary-trees`` de `The
352 Computer Language Benchmarks Game`__, una colección de 12 programas escritos
353 en alrededor de 30 lenguajes de programación para comparar su eficiencia
354 (medida en tiempo de ejecución, uso de memoria y cantidad de líneas de
355 código). De este juego de programas se utilizó solo ``binary-trees`` por ser
356 el único destinado a ejercitar el manejo de memoria. El programa sólo manipula
357 árboles binarios, creándolos y recorriéndolos inmediatamente (no realiza
358 ningún trabajo útil). La traducción a D_ fue realizada por Andrey Khropov
359 y fue hallada__ en el grupo de noticias.
361 __ http://shootout.alioth.debian.org/
362 __ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=43991
364 El código fuente es el siguiente::
366 import tango.util.Convert;
369 int main(string[] args)
371 int N = args.length > 1 ? to!(int)(args[1]) : 1;
373 int maxDepth = (minDepth + 2) > N ? minDepth + 2 : N;
374 int stretchDepth = maxDepth + 1;
375 int check = TreeNode.BottomUpTree(0, stretchDepth).ItemCheck;
376 TreeNode longLivedTree = TreeNode.BottomUpTree(0, maxDepth);
377 for (int depth = minDepth; depth <= maxDepth; depth += 2) {
378 int iterations = 1 << (maxDepth - depth + minDepth);
380 for (int i = 1; i <= iterations; i++) {
381 check += TreeNode.BottomUpTree(i, depth).ItemCheck;
382 check += TreeNode.BottomUpTree(-i, depth).ItemCheck;
390 TreeNode left, right;
393 this(int item, TreeNode left = null, TreeNode right = null)
400 static TreeNode BottomUpTree(int item, int depth)
403 return new TreeNode(item,
404 BottomUpTree(2 * item - 1, depth - 1),
405 BottomUpTree(2 * item, depth - 1));
406 return new TreeNode(item);
412 return item + left.ItemCheck() - right.ItemCheck();
421 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
423 Todos los pequeños programas utilizados como parte del banco de prueba
424 provienen del `Olden Benchmark`__ [CAR95]_. Estos programas fueron diseñados
425 para probar el lenguaje de programación Olden__; un lenguaje diseñado para
426 paralelizar programas automáticamente en arquitecturas con memoria
427 distribuida. Son programas relativamente pequeños (entre 400 y 1000 líneas de
428 código fuente cada uno) que realizan una tarea secuencial que asigna
429 estructuras de datos dinámicamente. Las estructuras están usualmente
430 organizadas como listas o árboles, y muy raramente como arreglos. Los
431 programas pasan la mayor parte del tiempo alocando datos y el resto usando los
432 datos alocados, por lo que en general están acotados en tiempo por el uso de
433 memoria (y no de procesador).
435 __ http://www.irisa.fr/caps/people/truong/M2COct99/Benchmarks/Olden/Welcome.html
436 __ http://www.martincarlisle.com/olden.html
438 La traducción a D_ fue realizada por Leonardo Maffi y están basadas a su vez
439 en la traducción de este juego de pruebas a Java_, JOlden__ [CMK01]_. En Java_
440 no se recomienda utilizar este conjunto de pruebas para medir la eficiencia
441 del recolector de basura, dado que se han creado mejores pruebas para este
442 propósito, como DaCapo__ [BLA06]_, sin embargo, dada la falta de programas
443 disponibles en general, y de un conjunto de pruebas especialmente diseñado
444 para evaluar el recolector de basura en D_, se decide utilizarlas en este
445 trabajo de todos modos. Sin embargo sus resultados deben ser interpretados con
446 una pizca de sal por lo mencionado anteriormente.
448 __ http://www-ali.cs.umass.edu/DaCapo/benchmarks.html
449 __ http://www.dacapobench.org/
451 En general (salvo para el programa ``voronoï``) está disponible el código
452 fuente portado a D_, Java_ y Python_, e incluso varias versiones con distintas
453 optimizaciones para reducir el consumo de tiempo y memoria. Además provee
454 comparaciones de tiempo entre todas ellas. Los programas utilizados en este
455 banco de pruebas son la versión traducida más literalmente de Java_ a D_, ya
456 que hace un uso más intensivo del recolector que las otras versiones.
458 A continuación se da una pequeña descripción de cada uno de los 5 programas
459 traducidos y los enlaces en donde encontrar el código fuente (y las
460 comparaciones de tiempos estar disponibles).
465 Este programa computa las interacciones gravitatorias entre un número
466 :math:`N` de cuerpos en tiempo :math:`O(N log N)` y está basado en árboles
467 heterogéneos de 8 ramas, según el algoritmo descripto por Barnes & Hut
470 Código fuente disponible en:
471 http://www.fantascienza.net/leonardo/js/dolden_bh.zip
476 Este programa ordena :math:`N` números, donde :math:`N` es una potencia de 2,
477 usando un ordenamiento *Bitonic* adaptativo, un algoritmo paralelo óptimo para
478 computadoras con memoria compartida, según describen Bilardi & Nicolau
479 [BN98]_. Utiliza árboles binarios como principal estructuras de datos.
481 Código fuente disponible en:
482 http://www.fantascienza.net/leonardo/js/dolden_bisort.zip
487 Este programa modela la propagación de ondas electromagnéticas a través de
488 objetos en 3 dimensiones. Realiza un cálculo simple sobre un grafo irregular
489 bipartito (implementado utilizando listas simplemente enlazadas) cuyos nodos
490 representan valores de campo eléctrico y magnético. El algoritmo es el
491 descripto por Culler, et al. [CDG93]_.
493 Código fuente disponible en:
494 http://www.fantascienza.net/leonardo/js/dolden_em3d.zip
499 Este programa implementa una heurística para resolver el problema del viajante
500 (*traveling salesman problem*) utilizando árboles binarios balanceados. El
501 algoritmo utilizado es el descripto por Karp [KAR77]_.
504 Código fuente disponible en:
505 http://www.fantascienza.net/leonardo/js/dolden_tsp.zip
510 Este programa genera un conjunto aleatorio de puntos y computa su diagrama de
511 Voronoï, una construcción geométrica que permite construir una partición del
512 plano euclídeo, utilizando el algoritmo descripto por Guibas & Stolfi [GS85]_.
514 Código fuente disponible en: http://codepad.org/xGDCS3KO
520 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
522 Dil_ (escrito en su mayor parte por Aziz Köksal y publicado bajo licencia
523 GPL_) es, lamentablemente, el único programa real hallado que, a pesar de
524 estar incompleto, es lo suficientemente grande, mantenido y estable como para
525 ser incluido en el banco de pruebas. Se trata de un compilador de D_ escrito
526 en D_ y está incompleto porque no puede generar código (falta implementar el
527 análisis semántico y la generación de código), por lo que es principalmente
528 utilizado para generar documentación a partir del código.
530 El programa está compuesto por:
532 * 32.000 líneas de código fuente (aproximadamente).
533 * 86 módulos (o archivos).
534 * 322 diferentes tipos de datos definidos por el usuario, de los cuales 34 son
535 tipos *livianos* (``struct``) y 288 tipos polimórficos (``class``), de los
536 que 260 son subtipos (sub-clases).
538 Puede observarse entonces que a pesar de ser incompleto, es una pieza de
539 software bastante compleja y de dimensión considerable.
541 Además, al interpretar código fuente se hace un uso intensivo de cadenas de
542 texto que en general presentan problemas muy particulares por poder ser
543 objetos extremadamente pequeños y de tamaños poco convencionales (no múltiplos
544 de palabras, por ejemplo). A su vez, el texto interpretado es convertido a una
545 representación interna en forma de árbol (o *árbol de sintaxis abstracta*)
546 modelado por tipos *livianos* y polimórficos que están organizados en arreglos
547 dinámicos contiguos y asociativos (que usan muchos servicios del recolector),
548 y que finalmente son manipulados para obtener y generar la información
549 necesaria, creando y dejando *morir* objetos constantemente (pero no como única
550 forma de procesamiento, como otras pruebas sintetizadas).
552 Por último, a diferencia de muchos otros programas escritos en D_, que dadas
553 algunas de las ineficiencias del recolector invierten mucho trabajo en limitar
554 su uso, este programa no está escrito pensando en dichas limitaciones, por lo
555 que muestra un funcionamiento muy poco sesgado por estas infortunadas
558 Por todas estas razones, Dil_ es el ejemplar que tal vez mejor sirve a la hora
559 de medir de forma realista los resultados obtenidos o los avances realizados.
560 Si bien, como se ha dicho anteriormente, las demás pruebas del banco pueden
561 ser útiles para encontrar problemas muy particulares, está es la que da una
562 lectura más cercana a la realidad del uso de un recolector.
567 Modificaciones propuestas
568 ----------------------------------------------------------------------------
570 Se decide realizar todas las modificaciones al recolector actual de forma
571 progresiva e incremental, partiendo como base del recolector de la versión
572 0.99.9 de Tango_. Las razones que motivan esta decisión son varias; por un
573 lado es lo más apropiado dados los requerimientos claves mencionados al
574 principio de este capítulo. Por ejemplo, al hacer cambios incrementales es más
575 fácil comprobar que la eficiencia no se aleja mucho del actual con cada
576 modificación y una modificación gradual impone menos resistencia a la
577 aceptación del nuevo recolector.
579 Además la construcción de un recolector de cero es una tarea difícil
580 considerando que un error en el recolector es extremadamente complejo de
581 rastrear, dado que en general el error se detecta en el *mutator* y en una
582 instancia muy posterior al origen real del error. Esto ha sido comprobado de
583 forma práctica, dado que, a modo de ejercicio para interiorizarse en el
584 funcionamiento del *runtime* de D_, primero se ha construido desde cero una
585 implementación de un recolector *naïve*, resultando muy difícil su depuración
586 por las razones mencionadas. Por el contrario, comenzar con un recolector en
587 funcionamiento como base hace más sencillo tanto probar cada pequeña
588 modificación para asegurar que no introduce fallos, como encontrar y reparar
589 los fallos cuando estos se producen, ya que el código incorrecto introducido
590 está bien aislado e identificado.
592 A continuación se hace un recorrido sobre cada una de las mejoras propuestas,
593 y en los casos en los que la mejora propone un cambio algorítmico, se analiza
594 la corrección del algoritmo resultante, partiendo de la base de que el
595 algoritmo tomado como punto de partida es un marcado y barrido que utiliza la
596 abstracción tricolor para hacer la fase de marcado de forma iterativa (ver
597 :ref:`gc_mark_sweep` y :ref:`gc_intro_tricolor`), cuya corrección ya está
598 probada en la literatura preexistente.
604 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
606 Una de las primeras mejoras propuestas es la posibilidad de configurar el
607 recolector de forma más sencilla. El requerimiento mínimo es la posibilidad de
608 configurar el recolector sin necesidad de recompilarlo. La complejidad de esto
609 surge de que el recolector debe ser transparente para el programa del usuario.
611 Configurar el recolector en tiempo de compilación del programa del usuario
612 probablemente requeriría modificar el compilador, y además, si bien es una
613 mejora sustancial a la configuración en tiempo de compilación del recolector,
614 no termina de ser completamente conveniente para realizar pruebas reiteradas
615 con un mismo programa para encontrar los mejores valores de configuración para
616 ese programa en particular.
618 Por otro lado, permitir configurar el recolector en tiempo de ejecución, una
619 vez que su estructura interna ya fue definida y creada, puede ser no solo
620 tedioso y complejo, además ineficiente, por lo tanto esta opción también se
623 Finalmente, lo que parece ser más apropiado para un recolector, es permitir la
624 configuración en tiempo de inicialización. Es decir, configurar el recolectar
625 sin necesidad de recompilar ni el programa del usuario ni el recolector, pero
626 antes de que el programa del usuario inicie, de manera que una vez iniciado el
627 recolector con ciertos parámetros, éstos no cambien nunca más en durante la
630 Este esquema provee la mejor relación entre configurabilidad, conveniencia,
631 eficiencia y simplicidad. Una posibilidad para lograr esto es utilizar
632 parámetros de línea de comandos, sin embargo no parece ni sencillo (proveer
633 una forma de leer los parámetros de línea de comandos requiere cambios en el
634 *runtime*) ni apropiado (el recolector debería ser lo más transparente posible
635 para el programa del usuario).
637 Otra posibilidad es utilizar variables de entorno, que parece ser la opción
638 más sencilla y apropiada. Sencilla porque las variables de entorno pueden ser
639 leídas directamente al inicializar el recolector sin necesidad de cooperación
640 alguna del *runtime*, a través de :manpage:`getenv(3)`. Apropiada porque, si
641 bien el problema de invasión del programa del usuario también existe, es una
642 práctica más frecuente y aceptada la configuración de módulos internos
643 o bibliotecas compartidas a través de variables de entorno.
645 Por último, antes de comenzar a usar este esquema de configuración, se
646 verifica que tomar ciertas decisiones en tiempo de ejecución no impacten en la
647 eficiencia del recolector. Para esto se convierten algunas opciones que antes
648 eran solo seleccionables en tiempo de compilación del recolector para que
649 puedan ser seleccionables en tiempo de inicialización y se comprueba que no
650 hay una penalización apreciable.
655 Especificación de opciones
656 ^^^^^^^^^^^^^^^^^^^^^^^^^^
657 Para especificar opciones de configuración, hay que hacerlo a través de la
658 variable de entorno de nombre :envvar:`D_GC_OPTS`. El valor de esa variable es
659 interpretado de la siguiente manera (en formato similar a :term:`BNF`):
662 D_GC_OPTS: `option` ( ':' `option` )* <lista de opciones>
663 option: `name` [ '=' `value` ]
664 name: `namec` `namec`* <nombre de la opción>
665 value: `valuec`* <valor de la opción>
666 namec: `valuec` - '='
667 valuec: [0x01-0xFF] - ':' <cualquier char salvo '\0' y ':'>
669 Es decir, se compone de una lista de opciones separadas por **:**. Cada opción
670 se especifica con un nombre, opcionalmente seguido por un valor (separados por
673 El valor de una opción puede ser un texto arbitrario (exceptuando los
674 caracteres ``'\0'`` y ``':'`` y de longitud máxima 255), pero cada opción lo
675 interpreta de forma particular. Como caso general, hay opciones booleanas, que
676 toman como valor verdadero un cualquier número distinto de 0 (o si el valor es
677 vació, es decir, solo se indica el nombre de la opción), y como valor falso
678 cualquier otro texto.
680 A continuación se listan las opciones reconocidas por el recolector (indicando
681 el formato del valor de la opción de tener uno especial):
684 Esta es una opción (booleana) disponible en el recolector original, pero
685 que se cambia para que sea configurable en tiempo de inicialización
686 (estando desactivada por omisión). Activa la opción ``MEMSTOMP`` descripta
690 Esta opción es también booleana (desactivada por omisión), está disponible
691 en el recolector original, y se la cambia para sea configurable en tiempo
692 de inicialización. Activa la opción ``SENTINEL`` descripta en
696 Esta opción permite crear una cierta cantidad de *pools* de un tamaño
697 determinado previo a que inicie el programa. Si se especifica solo un
698 número, se crea un *pool* con ese tamaño en MiB. Si, en cambio, se
699 especifica una cadena del tipo ``3x1``, el primer número indica la cantidad
700 de *pools* y el segundo el tamaño en MiB de cada uno (3 *pools* de 1MiB en
701 este caso). Ver :ref:`sol_pre_alloc` para más detalles sobre la utilidad de
705 El valor de esta opción indica el porcentaje mínimo porcentaje del *heap*
706 que debe quedar libre luego de una recolección. Siendo un porcentaje, solo
707 se aceptan valores entre 0 y 100, siendo su valor por omisión 5. Ver
708 :ref:`sol_ocup` para más detalles sobre su propósito.
710 ``malloc_stats_file``
711 Esta opción sirve para especificar un archivo en el cual escribir un
712 reporte de todas la operaciones de pedido de memoria realizadas por el
713 programa (durante su tiempo de vida). Ver :ref:`sol_stats` para más
714 detalles sobre la información provista y el formato del reporte.
716 ``collect_stats_file``
717 Esta opción sirve para especificar un archivo en el cual escribir un
718 reporte de todas las recolecciones hechas durante el tiempo de vida del
719 programa. Ver :ref:`sol_stats` para más detalles sobre la información
720 provista y el formato del reporte.
723 Esta opción booleana permite desactivar el escaneo preciso del *heap*,
724 forzando al recolector a ser completamente conservativo (excepto por los
725 bloques con el atributo ``NO_SCAN`` que siguen sin ser escaneados). Ver
726 :ref:`sol_precise` para más detalles sobre la existencia de esta opción.
729 Esta opción booleana (activada por omisión) permite seleccionar si el
730 recolector debe correr la fase de marcado en paralelo o no (es decir, si el
731 recolector corre de forma concurrente con el *mutator*). Para más detalles
735 Esta opción booleana (activada por omisión), sólo puede estar activa si
736 ``fork`` también está activa y sirve para indicar al recolector que reserve
737 un nuevo *pool* de memoria cuando una petición no puede ser satisfecha,
738 justo antes de lanzar la recolección concurrente. Ver
739 :ref:`sol_eager_alloc` para más detalles sobre el propósito de esta opción.
742 Esta opción booleana (desactivada por omisión), también sólo puede estar
743 activa si ``fork`` está activa y sirve para indicar al recolector que lance
744 una recolección (concurrente) antes de que la memoria libre se termine (la
745 recolección temprana será disparada cuando el porcentaje de memoria libre
746 sea menor a ``min_free``). Ver :ref:`sol_early_collect` para más detalles
747 sobre el propósito de esta opción.
749 Cualquier opción o valor no reconocido es ignorado por el recolector. Se
750 utilizan los valores por omisión de las opciones que no fueron especificadas,
751 o cuyos valores no pudieron ser interpretados correctamente.
753 Para cambiar la configuración del recolector se puede invocar el programa de
754 la siguiente manera (usando un intérprete de comandos del tipo *bourne
759 D_GC_OPTS=conservative:eager_alloc=0:early_collect=1:pre_alloc=2x5 ./programa
761 En este ejemplo, se activan las opciones ``conservative`` y ``early_collect``,
762 se desactiva ``eager_alloc`` y se crean 2 *pools* de 5MiB cada uno al
763 inicializar el recolector.
766 Reestructuración y cambios menores
767 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
769 Si bien se decide no comenzar una implementación desde cero, se ha mostrado
770 (ver :ref:`dgc_bad_code`) que la implementación actual es lo suficientemente
771 desprolija como para complicar su modificación. Es por esto que se hacen
772 algunas reestructuraciones básicas del código, reescribiendo o saneando de
773 forma incremental todas aquellas partes que complican su evolución.
775 Además de las modificaciones puramente estéticas (aunque no por eso menos
776 valuables, ya que la legibilidad y simplicidad del código son un factor
777 fundamental a la hora de ser mantenido o extendido), se hacen otras pequeñas
778 mejoras, que se detallan a continuación.
780 Remoción de memoria *no-encomendada*
781 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
782 Se elimina la distinción entre memoria *encomendada* y *no-encomendada* (ver
783 :ref:`dgc_committed`), pasando a estar *encomendada* toda la memoria
784 administrada por el recolector.
786 Si bien a nivel de eficiencia este cambio no tuvo impacto alguno (cuando en un
787 principio se especuló con que podría dar alguna ganancia en este sentido), se
788 elimina el concepto de memoria *encomendada* para quitar complejidad al
791 Esta mejora no afecta a la corrección del algoritmo, ya que a nivel lógico el
792 recolector solo ve la memoria *encomendada*.
794 .. _sol_minor_findsize:
796 Caché de ``Pool.findSize()``
797 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
798 Se crea un caché de tamaño de bloque para el método ``findSize()`` de un
799 *pool*. Esto acelera considerablemente las operaciones que necesitan pedir el
800 tamaño de un bloque reiteradamente, por ejemplo, al añadir nuevos elementos
801 a un arreglo dinámico. En esencia es una extensión a una de las optimizaciones
802 propuestas por Vladimir Panteleev [PAN09]_, que propone un caché global para
803 todo el recolector en vez de uno por *pool*.
805 Esta mejora tampoco afecta a la corrección del algoritmo, ya que nuevamente no
806 afecta su comportamiento a nivel lógico, solo cambia detalles en la
807 implementación de forma transparentes para el algoritmo de recolección.
809 Optimizaciones sobre ``findPool()``
810 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
811 Al analizar los principales cuellos de botella del recolector, es notoria la
812 cantidad de tiempo que pasa ejecutando la función ``findPool()``, que dado un
813 puntero devuelve el *pool* de memoria al cual pertenece. Es por esto que se
814 minimiza el uso de esta función. Además, dado que los *pools* de memoria están
815 ordenados por el puntero de comienzo del bloque de memoria manejado por el
816 *pool*, se cambia la búsqueda (originalmente lineal) por una búsqueda binaria.
817 Finalmente, dado que la lista de libre está construida almacenando el puntero
818 al siguiente en las mismas celdas que componen la lista, se almacena también
819 el puntero al *pool* al que dicha celda pertenece (dado que la celda más
820 pequeña es de 16 bytes, podemos garantizar que caben dos punteros, incluso
821 para arquitecturas de 64 bits). De esta manera no es necesario usar
822 ``findPool()`` al quitar una celda de la lista de libres.
824 Una vez más, la mejora no afecta la corrección del código.
828 Pre-asignación de memoria
829 ^^^^^^^^^^^^^^^^^^^^^^^^^
830 Esta opción permite crear una cierta cantidad de *pools* de un tamaño
831 determinado previo a que inicie el programa. Normalmente el recolector no
832 reserva memoria hasta que el programa lo pida. Esto puede llegar a evitar
833 que un programa haga muchas recolecciones al comenzar, hasta que haya
834 cargado su conjunto de datos de trabajo.
836 Se han analizado varios valores por omisión pero ninguno es consistentemente
837 mejor que comenzar sin memoria asignada, por lo tanto no se cambia el
838 comportamiento original, pero se agrega una opción (ver ``pre_alloc`` en
839 :ref:`sol_config_spec`) para que el usuario pueda experimentar con cada
840 programa en particular si esta opción es beneficiosa.
842 Esta opción tampoco cambia la corrección del algoritmo de recolección, solo
843 sus condiciones iniciales.
847 Mejora del factor de ocupación del *heap*
848 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
849 El factor de ocupación del *heap* debe ser apropiado por dos razones. Por un
850 lado, si el *heap* está demasiado ocupado todo el tiempo, serán necesarias
851 muchas recolecciones que, aunque pequeñas dado que la memoria utilizada es
852 poca, puede llegar a ser extremadamente ineficiente en casos patológicos (ver
853 :ref:`dgc_bad_ocup`). Por otro lado, si el tamaño del *heap* es extremadamente
854 grande (en comparación con el tamaño real del grupo de trabajo del programa),
855 se harán pocas recolecciones pero cada una es muy costosa, porque el algoritmo
856 de marcado y barrido es :math:`O(\lvert Heap \rvert)` (ver
857 :ref:`gc_mark_sweep`). Además la afinidad del caché va a ser extremadamente
860 Para mantener el factor de ocupación dentro de límites razonables, se agrega
861 la opción ``min_free`` (ver :ref:`sol_config_spec`). Esta opción indica el
862 recolector cual debe ser el porcentaje mínimo del *heap* que debe quedar libre
863 luego de una recolección. En caso de no cumplirse, se pide más memoria al
864 sistema operativo para cumplir este requerimiento. Además, luego de cada
865 recolección se verifica que el tamaño del *heap* no sea mayor a ``min_free``,
866 para evitar que el *heap* crezca de forma descontrolada. Si es mayor
867 a ``min_free`` se intenta minimizar el uso de memoria liberando *pools* que
868 estén completamente desocupados, mientras que el factor de ocupación siga
869 siendo mayor a ``min_free``. Si liberar un *pool* implica pasar ese límite, no
870 se libera y se pasa a analizar el siguiente y así sucesivamente.
872 Esta modificación no afecta a la corrección del algoritmo, ya que no lo afecta
875 Modificaciones descartadas
876 ^^^^^^^^^^^^^^^^^^^^^^^^^^
877 Se realizan varias otras modificaciones, con la esperanza de mejorar la
878 eficiencia del recolector, pero que, al contrario de lo esperado, empeoran la
879 eficiencia o la mejoran de forma muy marginal en comparación con la
880 complejidad agregada.
882 Probablemente el caso más significativo, y por tanto el único que vale la pena
883 mencionar, es la conversión de marcado iterativo a marcado recursivo y luego
884 a un esquema híbrido. Como se describe en :ref:`dgc_bad`, el marcado iterativo
885 tiene sus ventajas, pero tiene desventajas también. Al convertirlo a puramente
886 recursivo, se impracticable por resultar en errores de desbordamiento de pila.
888 Por lo tanto se prueba con un esquema híbrido, poniendo un límite a la
889 recursividad, volviendo al algoritmo iterativo cuando se alcanza este límite.
891 La implementación del algoritmo híbrido consiste en los siguientes cambios
892 sobre el algoritmo original (ver :ref:`dgc_algo_mark`)::
894 function mark_phase() is
895 global more_to_scan = false
896 global depth = 0 // Agregado
898 clear_mark_scan_bits()
901 push_registers_into_stack()
902 thread_self.stack.end = get_stack_top()
904 pop_registers_from_stack()
909 function mark_range(begin, end) is
911 global depth++ // Agregado
913 [pool, page, block] = find_block(pointer)
914 if block is not null and block.mark is false
916 if block.noscan is false
918 if (global depth > MAX_DEPTH) //
919 more_to_scan = true //
921 foreach ptr in block.words //
925 Al analizar los resultados de de esta modificación, se observa una mejoría muy
926 level, para valores de ``MAX_DEPTH`` mayores a cero (en algunos casos bastante
927 mayores) y en general para ``MAX_DEPTH`` cero (es decir, usando el algoritmo
928 de forma completamente iterativa) los resultados son peores, dado que se paga
929 el trabajo extra sin ganancia alguna. En la figura :vref:`fig:sol-mark-rec` se
930 puede ver, por ejemplo, el tiempo total de ejecución de Dil_ al generar la
931 documentación completa del código de Tango_, según varía el valor de
934 .. fig:: fig:sol-mark-rec
936 Análisis de tiempo total de ejecución en función del valor de
939 Tiempo total de ejecución de Dil_ al generar la documentación completa del
940 código de Tango_ en función del valor de ``MAX_DEPTH``. El rombo no
941 pertenece a ningún nivel de recursividad, representa el tiempo de ejecución
942 del algoritmo original (puramente iterativo).
944 .. image:: sol-mark-rec-dil.pdf
947 Dado que aumentar el nivel máximo de recursividad significa un uso mayor del
948 *stack*, y que esto puede impactar en el usuario (si el usuario tuviera un
949 programa que esté al borde de consumir todo el *stack*, el recolector podría
950 hacer fallar al programa de una forma inesperada para el usuario, problema que
951 sería muy difícil de depurar para éste), y que los resultados obtenidos no son
952 rotundamente superiores a los resultados sin esta modificación, se opta por no
953 incluir este cambio. Tampoco vale la pena incluirlo como una opción con valor
954 por omisión 0 porque, como se ha dicho, para este caso el resultado es incluso
955 peor que sin la modificación.
957 Esta modificación mantiene la corrección del recolector dado que tampoco
958 modifica el algoritmo sino su implementación. Además ambos casos extremos son
959 correctos (si ``MAX_DEPTH`` es 0, el algoritmo es puramente iterativo y si
960 pudiera ser infinito resultaría en el algoritmo puramente recursivo).
965 Recolección de estadísticas
966 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
968 Un requerimiento importante, tanto para evaluar los resultados de este trabajo
969 como para analizar el comportamiento de los programas estudiados, es la
970 recolección de estadísticas. Hay muchos aspectos que pueden ser analizados
971 a la hora de evaluar un recolector, y es por esto que se busca que la
972 recolección de datos sea lo más completa posible.
974 Con este objetivo, se decide recolectar datos sobre lo que, probablemente,
975 sean las operaciones más importantes del recolector: asignación de memoria
978 Todos los datos recolectados son almacenados en archivos que se especifican
979 a través de opciones del recolector (ver :ref:`sol_config_spec`). Los archivos
980 especificados debe poder ser escritos (y creados de ser necesario) por el
981 recolector (de otra forma se ignora la opción). El conjunto de datos
982 recolectados son almacenados en formato :term:`CSV` en el archivo, comenzando
983 con una cabecera que indica el significado de cada columna.
985 Los datos recolectados tienen en general 4 tipos de valores diferentes:
988 Se guarda en segundos como número de punto flotante (por ejemplo ``0.12``).
991 Se guarda en forma hexadecimal (por ejemplo ``0xa1b2c3d4``).
994 Se guarda como un número decimal, expresado en bytes (por ejemplo ``32``).
997 Se guarda como el número ``0`` si es falso o ``1`` si es verdadero.
999 Esta modificación mantiene la corrección del recolector dado que no hay cambio
1002 Asignación de memoria
1003 ^^^^^^^^^^^^^^^^^^^^^
1004 La recolección de datos sobre asignación de memoria se activa asignando un
1005 nombre de archivo a la opción ``malloc_stats_file``. Por cada asignación de
1006 memoria pedida por el programa (es decir, por cada llamada a la función
1007 ``gc_malloc()``) se guarda una fila con los siguientes datos:
1009 1. Cantidad de segundos que pasaron desde que empezó el programa (*timestamp*).
1010 2. Tiempo total que tomó la asignación de memoria.
1011 3. Valor del puntero devuelto por la asignación.
1012 4. Tamaño de la memoria pedida por el programa.
1013 5. Si esta petición de memoria disparó una recolección o no.
1014 6. Si debe ejecutarse un *finalizador* sobre el objeto (almacenado en la
1015 memoria pedida) cuando ésta no sea más alcanzable (cuando sea barrido).
1016 7. Si objeto carece de punteros (es decir, no debe ser escaneada).
1017 8. Si objeto no debe ser movido por el recolector.
1018 9. Puntero a la información sobre la ubicación de los punteros del objeto.
1019 10. Tamaño del tipo del objeto.
1020 11. Primera palabra con los bits que indican que palabras del tipo deben ser
1021 escaneados punteros y cuales no (en hexadecimal).
1022 12. Primera palabra con los bits que indican que palabras del tipo son
1023 punteros garantizados (en hexadecimal).
1025 Como puede apreciarse, la mayor parte de esta información sirve más para
1026 analizar el programa que el recolector. Probablemente solo el punto 2 sea de
1027 interés para analizar como se comporta el recolector.
1029 El punto 8 es completamente inútil, ya que el compilador nunca provee esta
1030 información, pero se la deja por si en algún momento comienza a hacerlo. Los
1031 puntos 9 a 12 provee información sobre el tipo del objeto almacenado, útil
1032 para un marcado preciso (ver :ref:`sol_precise`).
1034 El punto 6 indica, indirectamente, cuales de los objetos asignados son
1035 *pesados*, ya que éstos son los únicos que pueden tener un *finalizador*.
1036 Además, a través de los puntos 4 y 10 es posible inferir si lo que va
1037 almacenarse es un objeto solo o un arreglo de objetos.
1039 Recolección de basura
1040 ^^^^^^^^^^^^^^^^^^^^^
1041 Los datos sobre las recolecciones realizadas se guardan al asignar un nombre
1042 de archivo a la opción ``collect_stats_file``. Cada vez que se dispara una
1043 recolección [#solcollect]_ (es decir, cada vez que se llama a la función
1044 ``fullcollect()``) se guarda una fila con los siguientes datos:
1046 1. Cantidad de segundos que pasaron desde que empezó el programa (*timestamp*).
1047 2. Tiempo total que tomó la asignación de memoria que disparó la recolección.
1048 3. Tiempo total que tomó la recolección.
1049 4. Tiempo total que deben pausarse todos los hilos (tiempo de
1051 5. Cantidad de memoria usada antes de la recolección.
1052 6. Cantidad de memoria libre antes de la recolección.
1053 7. Cantidad de memoria desperdiciada antes de la recolección.
1054 8. Cantidad de memoria utilizada por el mismo recolector antes de la
1055 recolección (para sus estructuras internas).
1056 9. Cantidad de memoria usada después de la recolección.
1057 10. Cantidad de memoria libre después de la recolección.
1058 11. Cantidad de memoria desperdiciada [#solwaste]_ después de la recolección.
1059 12. Cantidad de memoria utilizada por el mismo recolector después de la
1062 Si bien el punto 4 parece ser el más importante para un programa que necesita
1063 baja latencia, dado el *lock* global del recolector, el punto 2 es
1064 probablemente el valor más significativo en este aspecto, dado que, a menos
1065 que el programa en cuestión utilice muy poco el recolector en distintos hilos,
1066 los hilos se verán pausados de todas formas cuando necesiten utilizar el
1069 .. [#solcollect] Esto es en el sentido más amplio posible. Por ejemplo, cuando
1070 se utiliza marcado concurrente (ver :ref:`sol_fork`), se guarda esta
1071 información incluso si ya hay una recolección activa, pero el tiempo de
1072 pausa de los hilos será -1 para indicar que en realidad nunca fueron
1075 .. [#solwaste] Memoria *desperdiciada* se refiere a memoria que directamente
1076 no puede utilizarse debido a la fragmentación. Si por ejemplo, se piden 65
1077 bytes de memoria, dada la organización del *heap* en bloques (ver
1078 :ref:`dgc_org`), el recolector asignará un bloque de 128 bytes, por lo
1079 tanto 63 bytes quedarán desperdiciados.
1085 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1087 Para agregar el soporte de marcado preciso se aprovecha el trabajo realizado
1088 por Vincent Lang (ver :ref:`dgc_via_art`) [DBZ3463]_, dado que se basa en `D
1089 1.0`_ y Tango_, al igual que este trabajo. Dado el objetivo y entorno común,
1090 se abre la posibilidad de adaptar sus cambios a este trabajo, utilizando una
1091 versión modificada de DMD_ (dado que los cambios aún no son integrados al
1092 compilador oficial).
1094 .. TODO: Apéndice con parches a DMD y Tango?
1096 Información de tipos provista por el compilador
1097 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1098 Con éstas modificaciones, el compilador en cada asignación le pasa al
1099 recolector información sobre los punteros del tipo para el cual se pide la
1100 memoria. Esta información se pasa como un puntero a un arreglo de palabras con
1101 la estructura mostrada en la figura :vref:`fig:sol-ptrmap` y que se describe
1104 .. fig:: fig:sol-ptrmap
1106 Estructura de la información de tipos provista por el compilador.
1114 +-------------+----------------------------+----------------------------+
1115 | "Tamaño en" | "Bits indicando si la" | "Bits indicando si" |
1116 | "cantidad" | "palabra en una posición" | "la palabra en una" |
1117 | "de" | "debe escanearse como" | "posición es" |
1118 | "palabras" | "si fuera un puntero" | "un puntero" |
1119 +-------------+----------------------------+----------------------------+
1122 +----- 1 -----+------- ceil(N/BPW) --------+------- ceil(N/BPW) --------+
1125 * La primera palabra indica el tamaño, en **cantidad de palabras**, del tipo
1126 para el cual se pide la memoria (:math:`N`).
1127 * Las siguientes :math:`ceil(\frac{N}{BPW})` palabras indican,
1128 como un conjunto de bits, qué palabras deben ser escaneadas por el
1129 recolector como si fueran punteros (donde :math:`BPW` indica la cantidad de
1130 bits por palabra, por ejemplo 32 para x86).
1131 * Las siguientes :math:`ceil(\frac{N}{BPW})` palabras son otro conjunto de
1132 bits indicando qué palabras son realmente punteros.
1134 Los conjuntos de bits guardan la información sobre la primera palabra en el
1135 bit menos significativo. Dada la complejidad de la representación, se ilustra
1136 con un ejemplo. Dada la estructura::
1145 void* begin1; // 1 word
1146 byte[size_t.sizeof * 14 + 1] bytes; // 15 words
1147 // el compilador agrega bytes de "padding" para alinear
1148 void* middle; // 1 word
1149 size_t[14] ints; // 14 words
1150 void* end1; // 1 words
1151 // hasta acá se almacenan los bits en la primera palabra
1152 void* begin2; // 1 words
1158 El compilador genera la estructura que se muestra en la figura
1159 :vref:`fig:sol-ptrmap-example` (asumiendo una arquitectura de 32 bits). Como
1160 puede apreciarse, el miembro ``u``, al ser una unión entre un puntero y un
1161 dato común, el compilador no puede asegurar que lo que se guarda en esa
1162 palabra sea realmente un puntero, pero indica que debe ser escaneado. El
1163 recolector debe debe ser conservativo en este caso, y escanear esa palabra
1164 como si fuera un puntero.
1166 .. fig:: fig:sol-ptrmap-example
1168 Ejemplo de estructura de información de tipos generada para el tipo ``S``.
1175 /---- "bit de 'end1'" -\
1177 | /---- "bit de 'middle'" | "de bits"
1179 | "bits de" | "bits de" /---- "bit de 'begin1'" | "primera"
1180 | "'ints'" | "'bytes'" | | "palabra"
1181 |/------------\|/-------------\| -/
1183 +----------------------------------+
1184 | 00000000000000000000000000100100 | "Tamaño en cantidad de palabras (36)"
1185 +==================================+ --\
1186 | 10000000000000010000000000000001 | | "Bits que indican si hay que"
1187 +----------------------------------+ | "escanear una palabra según"
1188 | 00000000000000000000000000001101 | | "su posición"
1189 +==================================+ --+
1190 | 10000000000000010000000000000001 | | "Bits que indican si hay un"
1191 +----------------------------------+ | "puntero en la palabra según"
1192 | 00000000000000000000000000001001 | | "su posición"
1193 +----------------------------------+ --/
1195 \--------------------------/|||| -\
1196 "bits de relleno" |||| |
1197 |||| | "Significado"
1198 "bit de 's'" |||| | "de bits"
1200 \---------------/||\---- "bit de 'begin2'" | "segunda"
1202 /---------------/\---- "bit de 'i'" |
1206 Si una implementación quisiera mover memoria (ver :ref:`gc_moving`), debería
1207 mantener inmóvil a cualquier objeto que sea apuntado por una palabra de estas
1208 características, ya que no es seguro actualizar la palabra con la nueva
1209 posición el objeto movido. Es por esta razón que se provee desglosada la
1210 información sobre lo que hay que escanear, y lo que es realmente un puntero
1211 (que puede ser actualizado de forma segura por el recolector de ser
1214 Implementación en el recolector
1215 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1216 La implementación está basada en la idea original de David Simcha, pero
1217 partiendo de la implementación de Vincent Lang (que está basada en Tango_)
1218 y consiste en almacenar el puntero a la estructura con la descripción del tipo
1219 generada por el compilador al final del bloque de datos. Este puntero solo se
1220 almacena si el bloque solicitado no tiene el atributo ``NO_SCAN``, dado que en
1221 ese caso no hace falta directamente escanear ninguna palabra del bloque.
1223 En la figura :vref:`fig:sol-ptrmap-blk` se puede ver, como continuación del
1224 ejemplo anterior, como se almacenaría en memoria un objeto del tipo ``S``.
1226 .. fig:: fig:sol-ptrmap-blk
1228 Ejemplo de bloque que almacena un objeto de tipo ``S`` con información de
1235 +------------------------ 256 bytes -----------------------------+
1238 +----------------------------------+-----------------------+-----+
1240 | Objeto | Desperdicio | Ptr |
1242 +----------------------------------+-----------------------+-----+
1245 +------------ 144 bytes -----------+------ 108 bytes ------+- 4 -+
1248 Un problema evidente de este esquema es que si el tamaño de un objeto se
1249 aproxima mucho al tamaño de bloque (difiere en menos de una palabra), el
1250 objeto ocupará el doble de memoria.
1252 El algoritmo de marcado se cambia de la siguiente forma::
1255 global conservative_scan = [1, 1, 0]
1258 function must_scan_word(pos, bits) is
1259 return bits[pos / BITS_PER_WORD] & (1 << (pos % BITS_PER_WORD))
1261 function mark_range(begin, end, ptrmap) is // Modificado
1262 number_of_words_in_type = ptrmap[0] // Agregado
1263 size_t* scan_bits = ptrmap + 1 // Agregado
1266 foreach word_pos in 0..number_of_words_in_type //
1267 if not must_scan_word(n, scan_bits) // Agregado
1269 [pool, page, block] = find_block(pointer)
1270 if block is not null and block.mark is false
1272 if block.noscan is false
1274 global more_to_scan = true
1275 pointer += number_of_words_in_type // Modificado
1277 function mark_heap() is
1278 while global more_to_scan
1279 global more_to_scan = false
1280 foreach pool in heap
1281 foreach page in pool
1282 if page.block_size <= PAGE // saltea FREE y CONTINUATION
1283 foreach block in page
1284 if block.scan is true
1286 if page.block_size is PAGE // obj grande //
1287 begin = cast(byte*) page //
1288 end = find_big_object_end(pool, page) //
1289 else // objeto pequeño //
1290 begin = block.begin //
1291 end = block.end // Modificado
1292 ptrmap = global conservative_scan //
1293 if NO_SCAN not in block.attrs //
1294 end -= size_t.sizeof //
1295 ptrmap = cast(size_t*) *end //
1296 mark_range(begin, end, ptrmap) //
1298 function mark_static_data() is
1299 mark_range(static_data.begin, static_data.end,
1300 global conservative_scan) // Agregado
1302 function mark_stacks() is
1303 foreach thread in threads
1304 mark_range(thread.stack.begin, thread.stack.end,
1305 global conservative_scan) // Agregado
1307 function mark_user_roots() is
1308 foreach root_range in user_roots
1309 mark_range(root_range.begin, root_range.end,
1310 global conservative_scan) // Agregado
1312 Las funciones de asignación de memoria se modifican de forma similar, para
1313 guardar el puntero a la información de tipos. Esta implementación utiliza solo
1314 la información sobre que palabras hay que tratar como punteros (deben ser
1315 escaneadas); la información sobre qué palabras son efectivamente punteros no
1316 se utiliza ya que no se mueven celdas.
1318 El algoritmo sigue siendo correcto, puesto que solamente se dejan de escanear
1319 palabras que el compilador sabe que no pueden ser punteros. Si bien el
1320 lenguaje permite almacenar punteros en una variable que no lo sea, esto es
1321 comportamiento indefinido por lo tanto un programa que lo hace no es
1322 considerado correcto, por lo cual el recolector tampoco debe ser correcto en
1323 esas circunstancias.
1325 Cabe destacar que la información de tipos solo se provee para objetos
1326 almacenados en el *heap*, el área de memoria estática, registros del
1327 procesador y la pila de todos los hilos siguen siendo escaneados de forma
1328 completamente conservativa. Se puede forzar el escaneo puramente conservativo
1329 utilizando la opción ``conservative`` (ver :ref:`sol_config_spec`).
1335 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1337 Finalmente se procede al objetivo primario de este trabajo, hacer que la fase
1338 más costosa del recolector (el marcado) pueda correr de manera concurrente con
1339 el *mutator*, con el objeto principal de disminuir el tiempo de pausa.
1341 Cabe aclarar, una vez más, que si bien los recolectores concurrentes buscan
1342 disminuir solo el tiempo de *stop-the-world*, en este caso es también
1343 fundamental disminuir el tiempo máximo que está tomado el *lock* global, dado
1344 que ese tiempo puede convertirse en una pausa para todos los threads que
1345 requieran servicios del recolector.
1347 Se decide basar la implementación en el *paper* "Non-intrusive Cloning Garbage
1348 Collector with Stock Operating System Support" [RODR97]_ por las siguientes
1349 razones principales:
1351 * Su implementación encaja de forma bastante natural con el diseño del
1352 recolector actual, por lo que requiere pocos cambios, lo que hace más
1353 factible su aceptación.
1354 * Está basado en la llamada al sistema :manpage:`fork(2)`, que no solo está
1355 muy bien soportada (y de manera muy eficiente) en Linux_, debe estar
1356 soportada en cualquier sistema operativo :term:`POSIX`.
1357 * No necesita instrumentar el código incluyendo barreras de memoria para
1358 informar al recolector cuando cambia el grafo de conectividad. Este es un
1359 aspecto fundamental, dada la filosofía de D_ de no pagar el precio de cosas
1360 que no se usan. La penalización en la eficiencia solo se paga cuando corre
1361 el recolector. Este aspecto también es crítico a la hora de evaluar la
1362 aceptación de la solución por parte de la comunidad.
1363 * Dada su sencillez general, no es difícil ofrecer el algoritmo concurrente
1364 como una opción, de manera que el usuario pueda optar por usarlo o no.
1366 Llamada al sistema *fork*
1367 ^^^^^^^^^^^^^^^^^^^^^^^^^
1368 El término *fork* proviene del inglés y significa *tenedor* de manera textual,
1369 pero se lo utiliza como analogía de una bifurcación. La operación crea una
1370 copia (llamada *hijo*) del proceso que la ejecuta (llamado *padre*).
1372 El punto más importante es que se crea un espacio de direcciones de memoria
1373 separado para el proceso hijo y una copia exacta de todos los segmentos de
1374 memoria del proceso padre. Es por esto que cualquier modificación que se haga
1375 en el proceso padre, no se refleja en el proceso hijo (y viceversa), a menos
1376 que la memoria sea compartida entre los procesos de forma explícita.
1378 Esto, sin embargo, no significa que la memoria física sea realmente duplicada;
1379 en general todos los sistemas operativos modernos (como Linux_) utilizan una
1380 técnica llamada *COW* (de *copy-on-write* en inglés, *copiar-al-escribir* en
1381 castellano) que retrasa la copia de memoria hasta que alguno de los dos
1382 procesos escribe en un segmento. Recién en ese momento el sistema operativo
1383 realiza la copia de **ese segmento solamente**. Es por esto que la operación
1384 puede ser muy eficiente, y la copia de memoria es proporcional a la cantidad
1385 de cambios que hayan.
1387 :manpage:`fork(2)` tiene otra propiedad importante de mencionar: detiene todos
1388 los hilos de ejecución en el proceso hijo. Es decir, el proceso hijo se crear
1389 con un solo hilo (el hilo que ejecutó la operación de :manpage:`fork(2)`).
1393 Lo que propone el algoritmo es muy sencillo, utilizar la llamada al sistema
1394 :manpage:`fork(2)` para crear una *fotografía* de la memoria del proceso en un
1395 nuevo proceso. En el proceso padre sigue corriendo el *mutator* y en el
1396 proceso hijo se corre la fase de marcado. El *mutator* puede modificar el
1397 grafo de conectividad pero los cambios quedan aislados el hijo (el marcado),
1398 que tiene una visión consistente e inmutable de la memoria. El sistema
1399 operativo duplica las páginas que modifica el padre bajo demanda, por lo tanto
1400 la cantidad de memoria física realmente copiada es proporcional a la cantidad
1401 y dispersión de los cambios que haga el *mutator*.
1403 La corrección del algoritmo se mantiene gracias a que la siguiente invariante
1406 Cuando una celda se convierte en basura, permanece como basura hasta ser
1407 reciclada por el recolector.
1409 Es decir, el *mutator* no puede *resucitar* una celda *muerta* y esta
1410 invariante se mantiene al correr la fase de marcado sobre una vista inmutable
1411 de la memoria. El único efecto introducido es que el algoritmo toma una
1412 aproximación más conservativa. Es decir, lo que sí puede pasar es que una
1413 celda que pasó a estar *muerta* una vez que la fase de marcado se inició, pero
1414 antes de que ésta termine, la celda no se reciclará hasta la próxima
1415 recolección, dado que este algoritmo no incluye una comunicación entre
1416 *mutator* y recolector para notificar cambios en el grafo de conectividad.
1417 Pero esto no afecta la corrección del algoritmo, ya que un recolector es
1418 correcto cuando nunca recicla celdas *vivas*.
1420 La única comunicación necesaria entre el *mutator* y el recolector son los
1421 bits de marcado (ver :ref:`dgc_impl`), dado que la fase de barrido debe correr
1422 en el proceso padre. No es necesaria ningún tipo de sincronización entre
1423 *mutator* y recolector más allá de que uno espera a que el otro finalice.
1425 Además de almacenar el conjunto de bits ``mark`` en memoria compartida entre
1426 el proceso padre e hijo (necesario para la fase de barrido), las
1427 modificaciones necesarias para hacer la fase de marcado concurrente son las
1428 siguientes [#solforkerr]_::
1430 function collect() is
1432 fflush(null) // evita que se duplique la salida de los FILE* abiertos
1434 if child_pid is 0 // proceso hijo
1436 exit(0) // termina el proceso hijo
1442 function mark_phase() is
1443 global more_to_scan = false
1444 // Borrado: stop_the_world()
1445 clear_mark_scan_bits()
1448 push_registers_into_stack()
1449 thread_self.stack.end = get_stack_top()
1451 pop_registers_from_stack()
1454 // Borrado: start_the_world()
1456 Como se puede observar, el cambio es extremadamente sencillo. Sigue siendo
1457 necesario un tiempo mínimo de pausa (básicamente el tiempo que tarda la
1458 llamada al sistema operativo :manpage:`fork(2)`) para guardar una vista
1459 consistente de los registros del CPU y *stacks* de los hilos. Si bien el
1460 conjunto de bits ``mark`` es compartido por el proceso padre e hijo dado que
1461 es necesario para *comunicar* las fases de marcado y barrido, cabe notar que
1462 nunca son utilizados de forma concurrente (la fase de barrido espera que la
1463 fase de marcado termine antes de usar dichos bits), por lo tanto no necesitan
1464 ningún tipo de sincronización y nunca habrá más de una recolección en proceso
1465 debido al *lock* global del recolector.
1467 A pesar de que con estos cambios el recolector técnicamente corre de forma
1468 concurrente, se puede apreciar que para un programa con un solo hilo el
1469 tiempo máximo de pausa seguirá siendo muy grande, incluso más grande que antes
1470 dado el trabajo extra que impone crear un nuevo proceso y duplicar las páginas
1471 de memoria modificadas. Lo mismo le pasará a cualquier hilo que necesite hacer
1472 uso del recolector mientras hay una recolección en proceso, debido al *lock*
1475 Para bajar este tiempo de pausa se experimenta con dos nuevas mejoras, que se
1476 describen a continuación, cuyo objetivo es correr la fase de marcado de forma
1477 concurrente a **todos** los hilos, incluyendo el hilo que la disparó.
1479 .. [#solforkerr] Se omite el manejo de errores y la activación/desactivación
1480 del marcado concurrente a través de opciones del recolector para facilitar
1481 la comprensión del algoritmo y los cambios realizados. Si devuelve con
1482 error la llamada a ``fork()`` o ``waitpid()``, se vuelve al esquema
1483 *stop-the-world* como si se hubiera desactivado el marcado concurrente
1484 utilizando la opción del recolector ``fork=0``.
1487 .. _sol_eager_alloc:
1489 Creación ansiosa de *pools* (*eager allocation*)
1490 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1491 Esta mejora, que puede ser controlada a través de la opción ``eager_alloc``
1492 (ver :ref:`sol_config_spec`), consiste en crear un nuevo *pool* cuando un
1493 pedido de memoria no puede ser satisfecho, justo después de lanzar la
1494 recolección. Esto permite al recolector satisfacer la petición de memoria
1495 inmediatamente, corriendo la fase de marcado de forma realmente concurrente,
1496 incluso para programas con un solo hilo o programas cuyos hilos usan
1497 frecuentemente servicios del recolector. El precio a pagar es un mayor uso de
1498 memoria de forma temporal (y el trabajo extra de crear y eliminar *pools* más
1499 frecuentemente), pero es esperable que el tiempo máximo de pausa **real** se
1500 vea drásticamente disminuido.
1502 A simple vista las modificaciones necesarias para su implementación parecieran
1503 ser las siguientes::
1509 function mark_is_running() is
1510 return global mark_pid != 0
1512 function collect() is
1513 if mark_is_running() //
1514 finished = try_wait(global mark_pid) //
1515 if finished // Agregado
1522 if child_pid is 0 // proceso hijo
1527 // Borrado: wait(child_pid)
1528 global mark_pid = child_pid
1530 Sin embargo con sólo estas modificaciones el algoritmo deja de ser correcto,
1531 ya que tres cosas problemáticas pueden suceder:
1533 1. Puede llamarse a la función ``minimize()`` mientras hay una fase de marcado
1534 corriendo en paralelo. Esto puede provocar que se libere un *pool* mientras
1535 se lo está usando en la fase de marcado, lo que no sería un problema
1536 (porque el proceso de marcado tiene una copia) si no fuera porque los bits
1537 de marcado, que son compartidos por los procesos, se liberan con el *pool*.
1538 2. Si un bloque libre es asignado después de que la fase de marcado comienza,
1539 pero antes de que termine, ese bloque será barrido dado la función
1540 ``rebuild_free_lists()`` puede reciclar páginas si todos sus bloques tienen
1541 el bit ``freebits`` activo (ver :ref:`dgc_algo_sweep`).
1542 3. El *pool* creado ansiosamente, tendrá sus bits de marcado sin activar, por
1543 lo que en la fase de barrido será interpretado como memoria libre, incluso
1544 cuando puedan estar siendo utilizados por el *mutator*.
1546 El punto 1 sencillamente hace que el programa finalice con una violación de
1547 segmento (en el mejor caso) y 2 y 3 pueden desembocar en la liberación de una
1548 celda alcanzable por el *mutator*.
1550 El punto 1 se resuelve a través de la siguiente modificación::
1552 function minimize() is
1553 if mark_is_running() // Agregado
1558 if page.block_size is not FREE
1566 La resolución del punto 2 es un poco más laboriosa, ya que hay que mantener
1567 actualizado los ``freebits``, de forma que las celdas asignadas después de
1568 empezar la fase de marcado no sean barridas por tener ese bit activo::
1570 function new_big(size) is
1571 number_of_pages = ceil(size / PAGE_SIZE)
1572 pages = find_pages(number_of_pages)
1575 pages = find_pages(number_of_pages)
1578 pool = new_pool(number_of_pages)
1581 pages = assign_pages(pool, number_of_pages)
1582 pages[0].block.free = true // Agregado
1583 pages[0].block_size = PAGE
1584 foreach page in pages[1..end]
1585 page.block_size = CONTINUATION
1588 function assign_page(block_size) is
1589 foreach pool in heap
1590 foreach page in pool
1591 if page.block_size is FREE
1592 page.block_size = block_size
1593 foreach block in page
1594 block.free = true // Agregado
1595 free_lists[page.block_size].link(block)
1597 function mark_phase() is
1598 global more_to_scan = false
1599 // Borrado: clear_mark_scan_bits()
1600 // Borrado: mark_free_lists()
1601 clear_scan_bits() // Agregado
1604 push_registers_into_stack()
1605 thread_self.stack.end = get_stack_top()
1607 pop_registers_from_stack()
1612 function clear_scan_bits() is
1613 // La implementación real limpia los bits en bloques de forma eficiente
1614 foreach pool in heap
1615 foreach page in pool
1616 foreach block in page
1620 function mark_free() is
1621 // La implementación real copia los bits en bloques de forma eficiente
1622 foreach pool in heap
1623 foreach page in pool
1624 foreach block in page
1625 block.mark = block.free
1627 function free_big_object(pool, page) is
1628 pool_end = cast(byte*) pool.pages + (PAGE_SIZE * pool.number_of_pages)
1630 page.block_size = FREE
1631 page.block.free = true // Agregado
1632 page = cast(byte*) page + PAGE_SIZE
1633 while page < pool_end and page.block_size is CONTINUATION
1635 function new(size, attrs) is
1636 block_size = find_block_size(size)
1637 if block_size < PAGE
1638 block = new_small(block_size)
1640 block = new_big(size)
1647 block.free = false // Agregado
1648 return cast(void*) block
1650 funciones new_pool(number_of_pages = 1) is
1651 pool = alloc(pool.sizeof)
1654 pool.number_of_pages = number_of_pages
1655 pool.pages = alloc(number_of_pages * PAGE_SIZE)
1656 if pool.pages is null
1660 foreach page in pool
1661 page.block_size = FREE
1662 foreach block in page //
1663 block.free = true // Agregado
1664 block.mark = true //
1667 Finalmente, el punto número tres puede ser solucionado con el siguiente
1670 funciones new_pool(number_of_pages = 1) is
1671 pool = alloc(pool.sizeof)
1674 pool.number_of_pages = number_of_pages
1675 pool.pages = alloc(number_of_pages * PAGE_SIZE)
1676 if pool.pages is null
1680 foreach page in pool
1681 page.block_size = FREE
1682 foreach block in page // Agregado
1683 block.mark = true //
1686 La solución es conservativa porque, por un lado evita la liberación de *pools*
1687 mientras haya una recolección en curso (lo que puede hacer que el consumo de
1688 memoria sea un poco mayor al requerido) y por otro asegura que, como se
1689 mencionó anteriormente, los cambios hechos al grafo de conectividad luego de
1690 iniciar la fase de marcado y antes de que ésta termine, no serán detectados
1691 por el recolector hasta la próxima recolección (marcar todos los bloques de
1692 un nuevo *pool* como el bit ``mark`` asegura que que la memoria no sea
1693 recolectada por la fase de barrido cuando termine el marcado).
1695 Estas modificaciones son las que hacen que el algoritmo siga siendo correcto,
1696 asegurando que no se van a liberar celdas *vivas* (a expensas de diferir la
1697 liberación de algunas celdas *muertas* por algún tiempo).
1700 .. _sol_early_collect:
1702 Recolección temprana (*early collection*)
1703 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1704 Esta mejora, que puede ser controlada a través de la opción ``early_collect``
1705 (ver :ref:`sol_config_spec`), consiste en lanzar una recolección preventiva,
1706 antes de que una petición de memoria falle. El momento en que se lanza la
1707 recolección es controlado por la opción ``min_free`` (ver :ref:`sol_ocup`).
1709 De esta forma también puede correr de forma realmente concurrente el *mutator*
1710 y el recolector, al menos hasta que se acabe la memoria, en cuyo caso, a menos
1711 que la opción ``eager_alloc`` (ver :ref:`sol_eager_alloc`) también esté
1712 activada, se deberá esperar a que la fase de marcado termine para recuperar
1713 memoria en la fase de barrido.
1715 Para facilitar la comprensión de esta mejora se muestran sólo los cambios
1716 necesarios si no se utiliza la opción ``eager_alloc``::
1718 function collect(early = false) is // Modificado
1719 if mark_is_running()
1720 finished = try_wait(global mark_pid)
1725 else if early // Agregado
1730 if child_pid is 0 // proceso hijo
1736 global mark_pid = child_pid //
1742 function early_collect() is
1743 if not collect_in_progress() and (percent_free < min_free)
1746 function new(size, attrs) is
1747 block_size = find_block_size(size)
1748 if block_size < PAGE
1749 block = new_small(block_size)
1751 block = new_big(size)
1758 early_collect() // Agregado
1759 return cast(void*) block
1761 Es de esperarse que cuando no está activa la opción ``eager_alloc`` por un
1762 lado el tiempo de pausa máximo no sea tan chico como cuando sí lo está (dado
1763 que si la recolección no se lanza de forma suficientemente temprana se va
1764 a tener que esperar que la fase de marcado termine), y por otro que se hagan
1765 más recolecciones de lo necesario (cuando pasa lo contrario, se recolecta más
1766 temprano de lo que se debería). Sin embargo, también es de esperarse que el
1767 consumo de memoria sea un poco menor que al usar la opción ``eager_alloc``.
1769 En cuanto a la corrección del algoritmo, éste solamente presenta los problemas
1770 número 1 y 2 mencionados en :ref:`sol_eager_alloc`, dado que jamás se crean
1771 nuevos *pools* y la solución es la ya presentada, por lo tanto el algoritmo
1772 sigue siendo correcto con los cuidados pertinentes.
1777 ----------------------------------------------------------------------------
1779 Los resultados de las modificación propuestas en la sección anterior (ver
1780 :ref:`sol_mod`) se evalúan utilizando el conjunto de pruebas mencionado en la
1781 sección :ref:`sol_bench`).
1783 En esta sección se describe la forma en la que el conjunto de pruebas es
1784 utilizado, la forma en la que se ejecutan los programas para recolectar dichos
1785 resultados y las métricas principales utilizadas para analizarlos.
1787 A fines prácticos, y haciendo alusión al nombre utilizado por Tango_, en esta
1788 sección se utiliza el nombre **TBGC** (acrónimo para el nombre en inglés
1789 *Tango Basic Garbage Collector*) para hacer referencia al recolector original
1790 provisto por Tango_ 0.99.9 (que, recordamos, es el punto de partida de este
1791 trabajo). Por otro lado, y destacando la principal modificación propuesta por
1792 este trabajo, haremos referencia al recolector resultante de éste utilizando
1793 el nombre **CDGC** (acrónimo para el nombre en inglés *Concurrent D Garbage
1797 Ejecución del conjunto de pruebas
1798 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1800 Dado el indeterminismo inherente a los sistemas operativos de tiempo
1801 compartido modernos, se hace un particular esfuerzo por obtener resultados lo
1802 más estable posible.
1804 Hardware y software utilizado
1805 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1806 Para realizar las pruebas se utiliza el siguiente hardware:
1808 * Procesador Intel(R) Core(TM)2 Quad CPU Q8400 @ 2.66GHz.
1809 * 2GiB de memoria RAM.
1811 El entorno de software es el siguiente:
1813 * Sistema operativo Debian_ Sid (para arquitectura *amd64*).
1815 * DMD_ 1.063 modificado para proveer información de tipos al recolector (ver
1816 :ref:`sol_precise`).
1817 * *Runtime* Tango_ 0.99.9 modificado para utilizar la información de tipos
1818 provista por el compilador modificado.
1820 * Embedded GNU_ C Library 2.11.2.
1822 Si bien el sistema operativo utiliza arquitectura *amd64*, dado que DMD_
1823 todavía no soporta 64 bits, se compila y corren los programas de D_ en 32
1826 Opciones del compilador
1827 ^^^^^^^^^^^^^^^^^^^^^^^
1828 Los programas del conjunto de pruebas se compilan utilizando las siguientes
1829 opciones del compilador DMD_:
1832 Aplica optimizaciones generales.
1835 Aplica la optimización de expansión de funciones. Consiste en sustituir la
1836 llamada a función por el cuerpo de la función (en general solo para
1837 funciones pequeñas).
1840 No genera el código para verificar pre y post-condiciones, invariantes de
1841 representación, operaciones fuera de los límites de un arreglo y
1842 *assert*\ 's en general (ver :ref:`d_dbc`).
1844 Parámetros de los programas
1845 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
1846 Los programas de prueba se ejecutan siempre con los mismos parámetros (a menos
1847 que se especifique lo contrario), que se detallan a continuación.
1854 Procesa 40 veces un archivo de texto plano (de 4MiB de tamaño) [#solbible]_
1855 utilizando 4 hilos (más el principal).
1860 Procesa 40 veces un archivo de texto plano (de 4MiB de tamaño) [#solbible]_
1861 utilizando 4 hilos (más el principal).
1866 Procesa dos veces un archivo de texto plano (de 4MiB de tamaño)
1872 Construyen árboles con profundidad máxima 16.
1877 Computa las interacciones gravitatorias entre 4.000 cuerpos.
1882 Ordena alrededor de 2 millones de números (exactamente :math:`2^21
1886 ``-n 4000 -d 300 -i 74``
1888 Realiza 74 iteraciones para modelar 4.000 nodos con grado 300.
1893 Resuelve el problema del viajante a través de una heurística para un
1899 Se construye un diagrama con 30.000 nodos.
1902 ``ddoc $dst_dir -hl --kandil -version=Tango -version=TangoDoc
1903 -version=Posix -version=linux $tango_files``
1905 Genera la documentación de todo el código fuente de Tango_ 0.99.9, donde
1906 ``$dst_dir`` es el directorio donde almacenar los archivos generados
1907 y ``$tango_files`` es la lista de archivos fuente de Tango_.
1911 El resto de los programas se ejecutan sin parámetros (ver :ref:`sol_bench`
1912 para una descripción detallada sobre cada uno).
1914 .. [#solbible] El archivo contiene la Biblia completa, la versión traducida al
1915 inglés autorizada por el Rey Jaime o Jacobo (*Authorized King James
1916 Version* en inglés). Obtenida de: http://download.o-bible.com:8080/kjv.gz
1918 Recolectores y configuraciones utilizadas
1919 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1920 En general se presentan resultados para TBGC y varias configuraciones de CDGC,
1921 de manera de poder tener una mejor noción de que mejoras y problemas puede
1922 introducir cada una de las modificaciones más importantes.
1924 CDGC se utiliza con siguientes configuraciones:
1929 En modo conservativo. Específicamente, utilizando el juego de opciones::
1931 conservative=1:fork=0:early_collect=0:eager_alloc=0
1934 En modo preciso (ver :ref:`sol_precise`). Específicamente, utilizando el
1937 conservative=0:fork=0:early_collect=0:eager_alloc=0
1940 En modo preciso activando el marcado concurrente (ver :ref:`sol_fork`).
1941 Específicamente, utilizando el juego de opciones::
1943 conservative=0:fork=1:early_collect=0:eager_alloc=0
1946 En modo preciso activando el marcado concurrente con recolección temprana
1947 (ver :ref:`sol_early_collect`). Específicamente, utilizando el juego de
1950 conservative=0:fork=1:early_collect=1:eager_alloc=0
1953 En modo preciso activando el marcado concurrente con creación ansiosa de
1954 *pools* (ver :ref:`sol_eager_alloc`). Específicamente, utilizando el juego
1957 conservative=0:fork=1:early_collect=0:eager_alloc=1
1960 En modo preciso activando el marcado concurrente con recolección temprana
1961 y creación ansiosa de *pools*. Específicamente, utilizando el juego de
1964 conservative=0:fork=1:early_collect=1:eager_alloc=1
1970 Para analizar los resultados se utilizan varias métricas. Las más importantes
1973 * Tiempo total de ejecución.
1974 * Tiempo máximo de *stop-the-world*.
1975 * Tiempo máximo de pausa real.
1976 * Cantidad máxima de memoria utilizada.
1977 * Cantidad total de recolecciones realizadas.
1979 El tiempo total de ejecución es una buena medida del **rendimiento** general
1980 del recolector, mientras que la cantidad total de recolecciones realizadas
1981 suele ser una buena medida de su **eficacia** [#soleficacia]_.
1983 Los tiempos máximos de pausa, *stop-the-world* y real, son una buena medida de
1984 la **latencia** del recolector; el segundo siendo una medida más realista dado
1985 que es raro que los demás hilos no utilicen servicios del recolector mientras
1986 hay una recolección en curso. Esta medida es particularmente importante para
1987 programas que necesiten algún nivel de ejecución en *tiempo-real*.
1989 En general el consumo de tiempo y espacio es un compromiso, cuando se consume
1990 menos tiempo se necesita más espacio y viceversa. La cantidad máxima de
1991 memoria utilizada nos da un parámetro de esta relación.
1993 .. [#soleficacia] Esto no es necesariamente cierto para recolectores con
1994 particiones (ver :ref:`gc_part`) o incrementales (ver :ref:`gc_inc`), dado
1995 que en ese caso podría realizar muchas recolecciones pero cada una muy
1998 Métodología de medición
1999 ^^^^^^^^^^^^^^^^^^^^^^^
2000 Para medir el tiempo total de ejecución se utiliza el comando
2001 :manpage:`time(1)` con la especificación de formato ``%e``, siendo la medición
2002 más realista porque incluye el tiempo de carga del ejecutable, inicialización
2003 del *runtime* de D_ y del recolector.
2005 Todas las demás métricas se obtienen utilizando la salida generada por la
2006 opción ``collect_stats_file`` (ver :ref:`sol_stats`), por lo que no pueden ser
2007 medidos para TBGC. Sin embargo se espera que para esos casos los resultados no
2008 sean muy distintos a CDGC utilizando la configuración **cons** (ver sección
2011 Cabe destacar que las corridas para medir el tiempo total de ejecución no son
2012 las mismas que al utilizar la opción ``collect_stats_file``; cuando se mide el
2013 tiempo de ejecución no se utiliza esa opción porque impone un trabajo extra
2014 importante y perturbaría demasiado la medición del tiempo. Sin embargo, los
2015 tiempos medidos internamente al utilizar la opción ``collect_stats_file`` son
2016 muy precisos, dado que se hace un particular esfuerzo para que no se haga un
2017 trabajo extra mientras se está midiendo el tiempo.
2019 Al obtener el tiempo de *stop-the-world* se ignoran los apariciones del valor
2020 ``-1``, que indica que se solicitó una recolección pero que ya había otra en
2021 curso, por lo que no se pausan los hilos realmente. Como tiempo de pausa real
2022 (ver :ref:`sol_fork` para más detalles sobre la diferencia con el tiempo de
2023 *stop-the-world*) se toma el valor del tiempo que llevó la asignación de
2024 memoria que disparó la recolección.
2026 Para medir la cantidad de memoria máxima se calcula el valor máximo de la
2027 sumatoria de: memoria usada, memoria libre, memoria desperdiciada y memoria
2028 usada por el mismo recolector (es decir, el total de memoria pedida por el
2029 programa al sistema operativo, aunque no toda este siendo utilizada por el
2030 *mutator* realmente).
2032 Por último, la cantidad total de recolecciones realizadas se calcula contando
2033 la cantidad de entradas del archivo generado por ``collect_stats_file``,
2034 ignorando la cabecera y las filas cuyo valor de tiempo de *stop-the-world* es
2035 ``-1``, debido a que en ese caso no se disparó realmente una recolección dado
2036 que ya había una en curso.
2038 Además, ciertas pruebas se corren variando la cantidad de procesadores
2039 utilizados, para medir el impacto de la concurrencia en ambientes con un
2040 procesador solo y con múltiples procesadores. Para esto se utiliza el comando
2041 :manpage:`taskset`, que establece la *afinidad* de un proceso, *atándolo*
2042 a correr en un cierto conjunto de procesadores. Si bien las pruebas se
2043 realizan utilizando 1, 2, 3 y 4 procesadores, los resultados presentados en
2044 general se limitan a 1 y 4 procesadores, ya que no se observan diferencias
2045 sustanciales al utilizar 2 o 3 procesadores con respecto a usar 4 (solamente
2046 se ven de forma más atenuadas las diferencias entre la utilización de
2047 1 o 4 procesadores). Dado que de por sí ya son muchos los datos a procesar
2048 y analizar, agregar más resultados que no aportan información valiosa termina
2049 resultando contraproducente.
2051 En los casos donde se utilizan otro tipo de métricas para evaluar aspectos
2052 particulares sobre alguna modificación se describe como se realiza la medición
2053 donde se utiliza la métrica especial.
2055 Variabilidad de los resultados entre ejecuciones
2056 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2057 Es de esperarse que haya una cierta variación en los resultados entre
2058 corridas, dada la indeterminación inherente a los sistemas operativos de
2059 tiempo compartido, que compiten por los recursos de la computadora.
2061 Para minimizar esta variación se utilizan varias herramientas. En primer
2062 lugar, se corren las pruebas estableciendo máxima prioridad (-19 en Linux_) al
2063 proceso utilizando el comando :manpage:`nice(1)`. La variación en la
2064 frecuencia del reloj los procesadores (para ahorrar energía) puede ser otra
2065 fuente de variación, por lo que se usa el comando :manpage:`cpufreq-set(1)`
2066 para establecer la máxima frecuencia disponible de manera fija.
2068 Sin embargo, a pesar de tomar estas precauciones, se sigue observando una
2069 amplia variabilidad entre corridas. Además se observa una variación más
2070 importante de la esperada no solo en el tiempo, también en el consumo de
2071 memoria, lo que es más extraño. Esta variación se debe principalmente a que
2072 Linux_ asigna el espacio de direcciones a los procesos con una componente
2073 azarosa (por razones de seguridad). Además, por omisión, la llamada al sistema
2074 :manpage:`mmap(2)` asigna direcciones de memoria altas primero, entregando
2075 direcciones más bajas en llamadas subsiguientes [LWN90311]_.
2077 El comando :manpage:`setarch(8)` sirve para controlar éste y otros aspectos de
2078 Linux_. La opción ``-L`` hace que se utilice un esquema de asignación de
2079 direcciones antiguo, que no tiene una componente aleatoria y asigna primero
2080 direcciones bajas. La opción ``-R`` solamente desactiva la componente azarosa
2081 al momento de asignar direcciones.
2083 .. ftable:: t:sol-setarch
2085 Variación entre corridas para TBGC.
2087 Variación entre corridas para TBGC. La medición está efectuada utilizando
2088 los valores máximo, mínimo y media estadística de 20 corridas, utilizando
2089 la siguiente métrica: :math:`\frac{max - min}{\mu}`. La medida podría
2090 realizarse utilizando el desvío estándar en vez de la amplitud máxima, pero
2091 en este cuadro se quiere ilustrar la variación máxima, no la típica.
2095 Del tiempo total de ejecución.
2097 ======== ======== ======== ========
2098 Programa Normal ``-R`` ``-L``
2099 ======== ======== ======== ========
2100 bh 0.185 0.004 0.020
2101 bigarr 0.012 0.002 0.016
2102 bisort 0.006 0.003 0.006
2103 conalloc 0.004 0.004 0.004
2104 concpu 0.272 0.291 0.256
2105 dil 0.198 0.128 0.199
2106 em3d 0.006 0.033 0.029
2107 mcore 0.009 0.009 0.014
2108 rnddata 0.015 0.002 0.011
2109 sbtree 0.012 0.002 0.012
2110 split 0.025 0.000 0.004
2111 tsp 0.071 0.068 0.703
2112 voronoi 0.886 0.003 0.006
2113 ======== ======== ======== ========
2117 Del consumo máximo de memoria.
2119 ======== ======== ======== ========
2120 Programa Normal ``-R`` ``-L``
2121 ======== ======== ======== ========
2122 bh 0.001 0.000 0.001
2123 bigarr 0.001 0.000 0.001
2124 bisort 0.000 0.000 0.000
2125 conalloc 0.753 0.000 0.001
2126 concpu 0.002 0.000 0.001
2127 dil 0.055 0.028 0.013
2128 em3d 0.000 0.001 0.001
2129 mcore 0.447 0.482 0.460
2130 rnddata 0.000 0.000 0.000
2131 sbtree 0.000 0.000 0.000
2132 split 0.000 0.000 0.000
2133 tsp 0.000 0.001 0.000
2134 voronoi 0.001 0.000 0.000
2135 ======== ======== ======== ========
2137 Ambas opciones, reducen notablemente la variación en los resultados (ver
2138 cuadro :vref:`t:sol-setarch`). Esto probablemente se debe a la naturaleza
2139 conservativa del recolector, dado que la probabilidad de tener *falsos
2140 punteros* depende directamente de los valores de las direcciones de memoria,
2141 aunque las pruebas en la que hay concurrencia involucrada, se siguen viendo
2142 grandes variaciones, que probablemente estén vinculadas a problemas de
2143 sincronización que se ven expuestos gracias al indeterminismo inherente a los
2144 programas multi-hilo.
2146 Si bien se obtienen resultados más estables utilizando un esquema diferente al
2147 utilizado por omisión, se decide no hacerlo dado que las mediciones serían
2148 menos realistas. Los usuarios en general no usan esta opción y se presentaría
2149 una visión más acotada sobre el comportamiento de los programas. Sin embargo,
2150 para evaluar el este efecto en los resultados, siempre que sea posible se
2151 analizan los resultados de un gran número de corridas observando
2152 principalmente su mínima, media, máxima y desvío estándar.
2156 Resultados para pruebas sintizadas
2157 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2159 A continuación se presentan los resultados obtenidos para las pruebas
2160 sintetizadas (ver :ref:`sol_bench_synth`). Se recuerda que este conjunto de
2161 resultados es útil para analizar ciertos aspectos puntuales de las
2162 modificaciones propuestas, pero en general distan mucho de como se comporta un
2163 programa real, por lo que los resultados deben ser analizados teniendo esto
2168 .. fig:: fig:sol-bigarr-1cpu
2170 Resultados para ``bigarr`` (utilizando 1 procesador).
2172 Resultados para ``bigarr`` (utilizando 1 procesador). Se presenta el
2173 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2174 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2175 ejecución) o 20 corridas (para el resto).
2179 Tiempo de ejecución (seg)
2181 .. image:: plots/time-bigarr-1cpu.pdf
2185 Cantidad de recolecciones
2187 .. image:: plots/ncol-bigarr-1cpu.pdf
2191 Uso máximo de memoria (MiB)
2193 .. image:: plots/mem-bigarr-1cpu.pdf
2197 *Stop-the-world* máximo (seg)
2199 .. image:: plots/stw-bigarr-1cpu.pdf
2203 Pausa real máxima (seg)
2205 .. image:: plots/pause-bigarr-1cpu.pdf
2207 .. fig:: fig:sol-bigarr-4cpu
2209 Resultados para ``bigarr`` (utilizando 4 procesadores).
2211 Resultados para ``bigarr`` (utilizando 4 procesadores). Se presenta el
2212 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2213 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2214 ejecución) o 20 corridas (para el resto).
2218 Tiempo de ejecución (seg)
2220 .. image:: plots/time-bigarr-4cpu.pdf
2224 Cantidad de recolecciones
2226 .. image:: plots/ncol-bigarr-4cpu.pdf
2230 Uso máximo de memoria (MiB)
2232 .. image:: plots/mem-bigarr-4cpu.pdf
2236 *Stop-the-world* máximo (seg)
2238 .. image:: plots/stw-bigarr-4cpu.pdf
2242 Pausa real máxima (seg)
2244 .. image:: plots/pause-bigarr-4cpu.pdf
2246 En la figura :vref:`fig:sol-bigarr-1cpu` se pueden observar los resultados
2247 para ``bigarr`` al utilizar un solo procesador. En ella se puede notar que el
2248 tiempo total de ejecución en general aumenta al utilizar CDGC, esto es
2249 esperable, dado esta prueba se limitan a usar servicios del recolector. Dado
2250 que esta ejecución utiliza solo un procesador y por lo tanto no se puede sacar
2251 provecho a la concurrencia, es de esperarse que el trabajo extra realizado por
2252 las modificaciones se vea reflejado en los resultados. En la
2253 :vref:`fig:sol-bigarr-4cpu` (resultados al utilizar 4 procesadores) se puede
2254 observar como al usar solamente *eager allocation* se recupera un poco el
2255 tiempo de ejecución, probablemente debido al incremento en la concurrencia
2256 (aunque no se observa el mismo efecto al usar *early collection*).
2258 Observando el tiempo total de ejecución, no se esperaba un incremento tan
2259 notorio al pasar de TBGC a una configuración equivalente de CDGC **cons**,
2260 haciendo un breve análisis de las posibles causas, lo más probable parece ser
2261 el incremento en la complejidad de la fase de marcado dada capacidad para
2262 marcar de forma precisa (aunque no se use la opción, se paga el precio de la
2263 complejidad extra y sin obtener los beneficios). Además se puede observar
2264 como el agregado de precisión al marcado mejora un poco las cosas (donde sí se
2265 obtiene rédito de la complejidad extra en el marcado).
2267 En general se observa que al usar *eager allocation* el consumo de memoria
2268 y los tiempos de pausa se disparan mientras que la cantidad de recolecciones
2269 disminuye drásticamente. Lo que se observa es que el programa es
2270 más veloz pidiendo memoria que recolectándola, por lo que crece mucho el
2271 consumo de memoria. Como consecuencia la fase de barrido (que no corre en
2272 paralelo al *mutator* como la fase de marcado) empieza a ser predominante en
2273 el tiempo de pausa por ser tan grande la cantidad de memoria a barrer. Este
2274 efecto se ve tanto al usar 1 como 4 procesadores, aunque el efecto es mucho
2275 más nocivo al usar 1 debido a la alta variabilidad que impone la competencia
2276 entre el *mutator* y recolector al correr de forma concurrente.
2278 Sin embargo, el tiempo de *stop-the-world* es siempre considerablemente más
2279 pequeño al utilizar marcado concurrente en CDGC, incluso cuando se utiliza
2280 *eager allocation*, aunque en este caso aumenta un poco, también debido al
2281 incremento en el consumo de memoria, ya que el sistema operativo tiene que
2282 copiar tablas de memoria más grandes al efectuar el *fork* (ver
2287 .. fig:: fig:sol-concpu-1cpu
2289 Resultados para ``concpu`` (utilizando 1 procesador).
2291 Resultados para ``concpu`` (utilizando 1 procesador). Se presenta el
2292 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2293 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2294 ejecución) o 20 corridas (para el resto).
2298 Tiempo de ejecución (seg)
2300 .. image:: plots/time-concpu-1cpu.pdf
2304 Cantidad de recolecciones
2306 .. image:: plots/ncol-concpu-1cpu.pdf
2310 Uso máximo de memoria (MiB)
2312 .. image:: plots/mem-concpu-1cpu.pdf
2316 *Stop-the-world* máximo (seg)
2318 .. image:: plots/stw-concpu-1cpu.pdf
2322 Pausa real máxima (seg)
2324 .. image:: plots/pause-concpu-1cpu.pdf
2326 .. fig:: fig:sol-concpu-4cpu
2328 Resultados para ``concpu`` (utilizando 4 procesadores).
2330 Resultados para ``concpu`` (utilizando 4 procesadores). Se presenta el
2331 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2332 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2333 ejecución) o 20 corridas (para el resto).
2337 Tiempo de ejecución (seg)
2339 .. image:: plots/time-concpu-4cpu.pdf
2343 Cantidad de recolecciones
2345 .. image:: plots/ncol-concpu-4cpu.pdf
2349 Uso máximo de memoria (MiB)
2351 .. image:: plots/mem-concpu-4cpu.pdf
2355 *Stop-the-world* máximo (seg)
2357 .. image:: plots/stw-concpu-4cpu.pdf
2361 Pausa real máxima (seg)
2363 .. image:: plots/pause-concpu-4cpu.pdf
2365 En la figura :vref:`fig:sol-concpu-1cpu` se pueden observar los resultados
2366 para ``concpu`` al utilizar un solo procesador. En ella se aprecia que el
2367 tiempo total de ejecución disminuye levemente al usar marcado concurrente
2368 mientras no se utilice *eager allocation* pero aumenta al utilizarlo.
2370 Con respecto a la cantidad de recolecciones, uso máximo de memoria y tiempo de
2371 *stop-the-world* se ve un efecto similar al descripto para ``bigarr`` (aunque
2372 magnificado), pero sorprendentemente el tiempo total de pausa se dispara,
2373 además con una variabilidad sorprendente, cuando se usa marcado concurrente
2374 (pero no *eager allocation*). Una posible explicación podría ser que al
2375 realizarse el *fork*, el sistema operativo muy probablemente entregue el
2376 control del único procesador disponible al resto de los hilos que compiten por
2377 él, por lo que queda mucho tiempo pausado en esa operación aunque realmente no
2378 esté haciendo trabajo alguno (simplemente no tiene tiempo de procesador para
2379 correr). Este efecto se cancela al usar *eager allocation* dado que el
2380 *mutator* nunca se bloquea esperando que el proceso de marcado finalice.
2382 Además se observa una caída importante en la cantidad de recolecciones al
2383 utilizar marcado concurrente. Esto probablemente se deba a que solo un hilo
2384 pide memoria (y por lo tanto dispara recolecciones), mientras los demás hilos
2385 también estén corriendo. Al pausarse todos los hilos por menos tiempo, el
2386 trabajo se hace más rápido (lo que explica la disminución del tiempo total de
2387 ejecución) y son necesarias menos recolecciones, por terminar más rápido
2388 también el hilo que las dispara.
2390 En la :vref:`fig:sol-concpu-4cpu` se pueden ver los resultados al utilizar
2391 4 procesadores, donde el panorama cambia sustancialmente. El efecto mencionado
2392 en el párrafo anterior no se observa más (pues el sistema operativo tiene más
2393 procesadores para asignar a los hilos) pero todos los resultados se vuelven
2394 más variables. Los tiempos de *stop-the-world* y pausa real (salvo por lo
2395 recién mencionado) crecen notablemente, al igual que su variación. No se
2396 encuentra una razón evidente para esto; podría ser un error en la medición
2397 dado que al utilizar todos los procesadores disponibles del *hardware*,
2398 cualquier otro proceso que compita por tiempo de procesador puede afectarla
2401 El tiempo total de ejecución crece considerablemente, como se espera, dado que
2402 el programa aprovecha los múltiples hilos que pueden correr en paralelo en
2403 procesadores diferentes.
2405 Sin embargo, no se encuentra una razón clara para explicar el crecimiento
2406 dramático en la cantidad de recolecciones solo al no usar marcado concurrente
2407 para 4 procesadores.
2411 .. fig:: fig:sol-conalloc-1cpu
2413 Resultados para ``conalloc`` (utilizando 1 procesador).
2415 Resultados para ``conalloc`` (utilizando 1 procesador). Se presenta el
2416 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2417 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2418 ejecución) o 20 corridas (para el resto).
2422 Tiempo de ejecución (seg)
2424 .. image:: plots/time-conalloc-1cpu.pdf
2428 Cantidad de recolecciones
2430 .. image:: plots/ncol-conalloc-1cpu.pdf
2434 Uso máximo de memoria (MiB)
2436 .. image:: plots/mem-conalloc-1cpu.pdf
2440 *Stop-the-world* máximo (seg)
2442 .. image:: plots/stw-conalloc-1cpu.pdf
2446 Pausa real máxima (seg)
2448 .. image:: plots/pause-conalloc-1cpu.pdf
2450 .. fig:: fig:sol-conalloc-4cpu
2452 Resultados para ``conalloc`` (utilizando 4 procesadores).
2454 Resultados para ``conalloc`` (utilizando 4 procesadores). Se presenta el
2455 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2456 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2457 ejecución) o 20 corridas (para el resto).
2461 Tiempo de ejecución (seg)
2463 .. image:: plots/time-conalloc-4cpu.pdf
2467 Cantidad de recolecciones
2469 .. image:: plots/ncol-conalloc-4cpu.pdf
2473 Uso máximo de memoria (MiB)
2475 .. image:: plots/mem-conalloc-4cpu.pdf
2479 *Stop-the-world* máximo (seg)
2481 .. image:: plots/stw-conalloc-4cpu.pdf
2485 Pausa real máxima (seg)
2487 .. image:: plots/pause-conalloc-4cpu.pdf
2489 En la figura :vref:`fig:sol-conalloc-1cpu` se pueden observar los resultados
2490 para ``conalloc`` al utilizar un solo procesador. Los cambios con respecto
2491 a lo observado para ``concpu`` son mínimos. El efecto de la mejoría al usar
2492 marcado concurrente pero no *eager allocation* no se observa más, dado que
2493 ``conalloc`` pide memoria en todos los hilos, se crea un cuello de botella. Se
2494 ve claramente como tampoco baja la cantidad de recolecciones hecha debido
2495 a esto y se invierte la variabilidad entre los tiempos pico de pausa real
2496 y *stop-the-world* (sin una razón obvia, pero probablemente relacionado que
2497 todos los hilos piden memoria).
2499 Al utilizar 4 procesadores (figura :vref:`fig:sol-conalloc-4cpu`), más allá de
2500 las diferencias mencionadas para 1 procesador, no se observan grandes cambios
2501 con respecto a lo observado para ``concpu``, excepto que los tiempos de pausa
2502 (real y *stop-the-world*) son notablemente más pequeños, lo que pareciera
2503 confirmar un error en la medición de ``concpu``.
2507 .. fig:: fig:sol-split-1cpu
2509 Resultados para ``split`` (utilizando 1 procesador).
2511 Resultados para ``split`` (utilizando 1 procesador). Se presenta el mínimos
2512 (en negro), la media centrada entre dos desvíos estándar (en gris), y el
2513 máximo (en blanco) calculados sobre 50 corridas (para tiempo de ejecución)
2514 o 20 corridas (para el resto).
2518 Tiempo de ejecución (seg)
2520 .. image:: plots/time-split-1cpu.pdf
2524 Cantidad de recolecciones
2526 .. image:: plots/ncol-split-1cpu.pdf
2530 Uso máximo de memoria (MiB)
2532 .. image:: plots/mem-split-1cpu.pdf
2536 *Stop-the-world* máximo (seg)
2538 .. image:: plots/stw-split-1cpu.pdf
2542 Pausa real máxima (seg)
2544 .. image:: plots/pause-split-1cpu.pdf
2546 Este es el primer caso donde se aprecia la sustancial mejora proporcionada por
2547 una pequeña optimización, el caché de ``findSize()`` (ver
2548 :ref:`sol_minor_findsize`). En la figura :vref:`fig:sol-split-1cpu` se puede
2549 observar con claridad como, para cualquier configuración de CDGC, hay una
2550 caída notable en el tiempo total de ejecución. Sin embargo, a excepción de
2551 cuando se utiliza *eager allocation*, la cantidad de recolecciones y memoria
2552 usada permanece igual.
2554 La utilización de *eager allocation* mejora (aunque de forma apenas
2555 apreciable) el tiempo de ejecución, la cantidad de recolecciones baja a un
2556 tercio y el tiempo de pausa real cae dramáticamente. Al usar marcado
2557 concurrente ya se observa una caída determinante en el tiempo de
2558 *stop-the-world*. Todo esto sin verse afectado el uso máximo de memoria,
2559 incluso al usar *eager allocation*.
2561 Se omiten los resultados para más de un procesador por ser prácticamente
2562 idénticos para este análisis.
2566 .. fig:: fig:sol-mcore-1cpu
2568 Resultados para ``mcore`` (utilizando 1 procesador).
2570 Resultados para ``mcore`` (utilizando 1 procesador). Se presenta el
2571 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2572 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2573 ejecución) o 20 corridas (para el resto).
2577 Tiempo de ejecución (seg)
2579 .. image:: plots/time-mcore-1cpu.pdf
2583 Cantidad de recolecciones
2585 .. image:: plots/ncol-mcore-1cpu.pdf
2589 Uso máximo de memoria (MiB)
2591 .. image:: plots/mem-mcore-1cpu.pdf
2595 *Stop-the-world* máximo (seg)
2597 .. image:: plots/stw-mcore-1cpu.pdf
2601 Pausa real máxima (seg)
2603 .. image:: plots/pause-mcore-1cpu.pdf
2605 .. fig:: fig:sol-mcore-4cpu
2607 Resultados para ``mcore`` (utilizando 4 procesadores).
2609 Resultados para ``mcore`` (utilizando 4 procesadores). Se presenta el
2610 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2611 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2612 ejecución) o 20 corridas (para el resto).
2616 Tiempo de ejecución (seg)
2618 .. image:: plots/time-mcore-4cpu.pdf
2622 Cantidad de recolecciones
2624 .. image:: plots/ncol-mcore-4cpu.pdf
2628 Uso máximo de memoria (MiB)
2630 .. image:: plots/mem-mcore-4cpu.pdf
2634 *Stop-the-world* máximo (seg)
2636 .. image:: plots/stw-mcore-4cpu.pdf
2640 Pausa real máxima (seg)
2642 .. image:: plots/pause-mcore-4cpu.pdf
2644 El caso de ``mcore`` es interesante por ser, funcionalmente, una combinación
2645 entre ``concpu`` y ``split``, con un agregado extra: el incremento notable de
2646 la competencia por utilizar el recolector entre los múltiples hilos.
2648 Los efectos observados (en la figura :vref:`fig:sol-mcore-1cpu` para
2649 1 procesador y en la figura :vref:`fig:sol-mcore-4cpu` para 4) confirman esto,
2650 al ser una suma de los efectos observados para ``concpu`` y ``split``, con el
2651 agregado de una particularidad extra por la mencionada competencia entre
2652 hilos. A diferencia de ``concpu`` donde el incremento de procesadores resulta
2653 en un decremento en el tiempo total de ejecución, en este caso resulta en una
2654 disminución, dado que se necesita mucha sincronización entre hilos, por
2655 utilizar todos de forma intensiva los servicios del recolector (y por lo tanto
2656 competir por su *lock* global).
2658 Otro efecto común observado es que cuando el tiempo de pausa es muy pequeño
2659 (del orden de los milisegundos), el marcado concurrente suele incrementarlo en
2664 .. fig:: fig:sol-rnddata-1cpu
2666 Resultados para ``rnddata`` (utilizando 1 procesador).
2668 Resultados para ``rnddata`` (utilizando 1 procesador). Se presenta el
2669 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2670 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2671 ejecución) o 20 corridas (para el resto).
2675 Tiempo de ejecución (seg)
2677 .. image:: plots/time-rnddata-1cpu.pdf
2681 Cantidad de recolecciones
2683 .. image:: plots/ncol-rnddata-1cpu.pdf
2687 Uso máximo de memoria (MiB)
2689 .. image:: plots/mem-rnddata-1cpu.pdf
2693 *Stop-the-world* máximo (seg)
2695 .. image:: plots/stw-rnddata-1cpu.pdf
2699 Pausa real máxima (seg)
2701 .. image:: plots/pause-rnddata-1cpu.pdf
2703 En la figura :vref:`fig:sol-rnddata-1cpu` se presentan los resultados para
2704 ``rnddata`` utilizando 1 procesador. Una vez más estamos ante un caso en el
2705 cual se observa claramente la mejoría gracias a una modificación en particular
2706 principalmente. En esta caso es el marcado preciso. Se puede ver claramente
2707 como mejora el tiempo de total de ejecución a algo más que la mitad (en
2708 promedio, aunque se observa una anomalía donde el tiempo baja hasta más de
2709 3 veces). Sin embargo, a menos que se utilice *eager allocation* o *early
2710 collection* (que en este caso prueba ser muy efectivo), la cantidad de
2711 recolecciones aumenta considerablemente.
2713 La explicación puede ser hallada en el consumo de memoria, que baja unas
2714 3 veces en promedio usando marcado preciso que además hace disminuir
2715 drásticamente (unas 10 veces) el tiempo de pausa (real y *stop-the-world*). El
2716 tiempo de *stop-the-world* disminuye unas 10 veces más al usar marcado
2717 concurrente y el tiempo de pausa real al usar *eager allocation*, pero en este
2718 caso el consumo de memoria aumenta también bastante (aunque no tanto como
2719 disminuye el tiempo de pausa, por lo que puede ser un precio que valga la pena
2720 pagar si se necesitan tiempos de pausa muy pequeños).
2722 El aumento en el variación de los tiempos de ejecución al usar marcado preciso
2723 probablemente se debe a lo siguiente: con marcado conservativo, debe estar
2724 sobreviviendo a las recolecciones el total de memoria pedida por el programa,
2725 debido a falsos punteros (por eso no se observa prácticamente variación en el
2726 tiempo de ejecución y memoria máxima consumida); al marcar con precisión
2727 parcial, se logra disminuir mucho la cantidad de falsos punteros, pero el
2728 *stack* y la memoria estática, se sigue marcado de forma conservativa, por lo
2729 tanto dependiendo de los valores (aleatorios) generados por la prueba, aumenta
2730 o disminuye la cantidad de falsos punteros, variando así la cantidad de
2731 memoria consumida y el tiempo de ejecución.
2733 No se muestran los resultados para más de un procesador por ser demasiado
2734 similares a los obtenidos utilizando solo uno.
2738 Los resultados para ``sbtree`` son tan similares a los obtenidos con
2739 ``bigarr`` que directamente se omiten por completo, dado que no aportan ningún
2740 tipo de información nueva. Por un lado es esperable, dado que ambas pruebas se
2741 limitan prácticamente a pedir memoria, la única diferencia es que una pide
2742 objetos grandes y otra objetos pequeños, pero esta diferencia parece no
2743 afectar la forma en la que se comportan los cambios introducidos en este
2747 Resultados para pruebas pequeñas
2748 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2750 A continuación se presentan los resultados obtenidos para las pruebas pequeñas
2751 (ver :ref:`sol_bench_small`). Se recuerda que si bien este conjunto de pruebas
2752 se compone de programas reales, que efectúan una tarea útil, están diseñados
2753 para ejercitar la asignación de memoria y que no son recomendados para evaluar
2754 el desempeño de recolectores de basura. Sin embargo se las utiliza igual por
2755 falta de programas más realistas, por lo que hay que tomarlas como un grado de
2760 .. fig:: fig:sol-bh-1cpu
2762 Resultados para ``bh`` (utilizando 1 procesador).
2764 Resultados para ``bh`` (utilizando 1 procesador). Se presenta el
2765 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2766 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2767 ejecución) o 20 corridas (para el resto).
2771 Tiempo de ejecución (seg)
2773 .. image:: plots/time-bh-1cpu.pdf
2777 Cantidad de recolecciones
2779 .. image:: plots/ncol-bh-1cpu.pdf
2783 Uso máximo de memoria (MiB)
2785 .. image:: plots/mem-bh-1cpu.pdf
2789 *Stop-the-world* máximo (seg)
2791 .. image:: plots/stw-bh-1cpu.pdf
2795 Pausa real máxima (seg)
2797 .. image:: plots/pause-bh-1cpu.pdf
2799 En la figura :vref:`fig:sol-bh-1cpu` se pueden observar los resultados
2800 para ``bh`` al utilizar un solo procesador. Ya en una prueba un poco más
2801 realista se puede observar el efecto positivo del marcado preciso, en especial
2802 en la cantidad de recolecciones efectuadas (aunque no se traduzca en un menor
2803 consumo de memoria).
2805 Sin embargo se observa también un efecto nocivo del marcado preciso en el
2806 consumo de memoria que intuitivamente debería disminuir, pero crece, y de
2807 forma considerable (unas 3 veces en promedio). La razón de esta particularidad
2808 es el incremento en el espacio necesario para almacenar objetos debido a que
2809 el puntero a la información del tipo se guarda al final del bloque (ver
2810 :ref:`sol_precise`). En el cuadro :vref:`t:sol-prec-mem-bh` se puede observar
2811 la cantidad de memoria pedida por el programa, la cantidad de memoria
2812 realmente asignada por el recolector (y la memoria desperdiciada) cuando se
2813 usa marcado conservativo y preciso. Estos valores fueron tomados usando la
2814 opción ``malloc_stats_file`` (ver :ref:`sol_stats`).
2816 .. ftable:: t:sol-prec-mem-bh
2818 Memoria pedida y asignada para ``bh`` según modo de marcado.
2820 Memoria pedida y asignada para ``bh`` según modo de marcado conservativo
2821 o preciso (acumulativo durante toda la vida del programa).
2823 ============== ============== ============== =================
2824 Memoria Pedida (MiB) Asignada (MiB) Desperdicio (MiB)
2825 ============== ============== ============== =================
2826 Conservativo 302.54 354.56 52.02 (15%)
2827 Preciso 302.54 472.26 169.72 (36%)
2828 ============== ============== ============== =================
2830 Más allá de esto, los resultados son muy similares a los obtenidos para
2831 pruebas sintetizadas que se limitan a ejercitar el recolector (como ``bigarr``
2832 y ``sbtree``), lo que habla de lo mucho que también lo hace este pequeño
2835 No se muestran los resultados para más de un procesador por ser extremadamente
2836 similares a los obtenidos utilizando solo uno.
2840 .. fig:: fig:sol-bisort-1cpu
2842 Resultados para ``bisort`` (utilizando 1 procesador).
2844 Resultados para ``bisort`` (utilizando 1 procesador). Se presenta el
2845 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2846 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2847 ejecución) o 20 corridas (para el resto).
2851 Tiempo de ejecución (seg)
2853 .. image:: plots/time-bisort-1cpu.pdf
2857 Cantidad de recolecciones
2859 .. image:: plots/ncol-bisort-1cpu.pdf
2863 Uso máximo de memoria (MiB)
2865 .. image:: plots/mem-bisort-1cpu.pdf
2869 *Stop-the-world* máximo (seg)
2871 .. image:: plots/stw-bisort-1cpu.pdf
2875 Pausa real máxima (seg)
2877 .. image:: plots/pause-bisort-1cpu.pdf
2879 La figura :vref:`fig:sol-bisort-1cpu` muestra los resultados para ``bisort``
2880 al utilizar 1 procesador. En este caso el parecido es con los resultados para
2881 la prueba sintetizada ``split``, con la diferencia que el tiempo de ejecución
2882 total prácticamente no varía entre TBGC y CDGC, ni entre las diferentes
2883 configuraciones del último (evidentemente en este caso no se aprovecha el
2884 caché de ``findSize()``).
2886 Otra diferencia notable es la considerable reducción del tiempo de pausa real
2887 al utilizar *early collection* (más de 3 veces menor en promedio comparado
2888 a cuando se marca conservativamente, y más de 2 veces menor que cuando se hace
2889 de forma precisa), lo que indica que la predicción de cuando se va a necesitar
2890 una recolección es más efectiva que para ``split``.
2892 No se muestran los resultados para más de un procesador por ser extremadamente
2893 similares a los obtenidos utilizando solo uno.
2897 .. fig:: fig:sol-em3d-1cpu
2899 Resultados para ``em3d`` (utilizando 1 procesador).
2901 Resultados para ``em3d`` (utilizando 1 procesador). Se presenta el
2902 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2903 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2904 ejecución) o 20 corridas (para el resto).
2908 Tiempo de ejecución (seg)
2910 .. image:: plots/time-em3d-1cpu.pdf
2914 Cantidad de recolecciones
2916 .. image:: plots/ncol-em3d-1cpu.pdf
2920 Uso máximo de memoria (MiB)
2922 .. image:: plots/mem-em3d-1cpu.pdf
2926 *Stop-the-world* máximo (seg)
2928 .. image:: plots/stw-em3d-1cpu.pdf
2932 Pausa real máxima (seg)
2934 .. image:: plots/pause-em3d-1cpu.pdf
2936 Los resultados para ``em3d`` (figura :vref:`fig:sol-em3d-1cpu`) son
2937 sorprendentemente similares a los de ``bisort``. La única diferencia es que en
2938 este caso el marcado preciso y el uso de *early collection** no parecen
2939 ayudar; por el contrario, aumentan levemente el tiempo de pausa real.
2941 Una vez más no se muestran los resultados para más de un procesador por ser
2942 extremadamente similares a los obtenidos utilizando solo uno.
2946 .. fig:: fig:sol-tsp-1cpu
2948 Resultados para ``tsp`` (utilizando 1 procesador).
2950 Resultados para ``tsp`` (utilizando 1 procesador). Se presenta el
2951 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
2952 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
2953 ejecución) o 20 corridas (para el resto).
2957 Tiempo de ejecución (seg)
2959 .. image:: plots/time-tsp-1cpu.pdf
2963 Cantidad de recolecciones
2965 .. image:: plots/ncol-tsp-1cpu.pdf
2969 Uso máximo de memoria (MiB)
2971 .. image:: plots/mem-tsp-1cpu.pdf
2975 *Stop-the-world* máximo (seg)
2977 .. image:: plots/stw-tsp-1cpu.pdf
2981 Pausa real máxima (seg)
2983 .. image:: plots/pause-tsp-1cpu.pdf
2985 Los resultados para ``tsp`` (figura :vref:`fig:sol-tsp-1cpu`) son
2986 prácticamente idénticos a los de ``bisort``. La única diferencia es que la
2987 reducción del tiempo de pausa real es un poco menor.
2989 Esto confirma en cierta medida la poca utilidad de este juego de pruebas para
2990 medir el rendimiento de un recolector, dado que evidentemente, si bien todas
2991 resuelven problemas diferentes, realizan todas el mismo tipo de trabajo.
2993 Una vez más no se muestran los resultados para más de un procesador por ser
2994 extremadamente similares a los obtenidos utilizando solo uno.
2998 .. fig:: fig:sol-voronoi-1cpu
3000 Resultados para ``voronoi`` (utilizando 1 procesador).
3002 Resultados para ``voronoi`` (utilizando 1 procesador). Se presenta el
3003 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
3004 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
3005 ejecución) o 20 corridas (para el resto).
3009 Tiempo de ejecución (seg)
3011 .. image:: plots/time-voronoi-1cpu.pdf
3015 Cantidad de recolecciones
3017 .. image:: plots/ncol-voronoi-1cpu.pdf
3021 Uso máximo de memoria (MiB)
3023 .. image:: plots/mem-voronoi-1cpu.pdf
3027 *Stop-the-world* máximo (seg)
3029 .. image:: plots/stw-voronoi-1cpu.pdf
3033 Pausa real máxima (seg)
3035 .. image:: plots/pause-voronoi-1cpu.pdf
3037 .. fig:: fig:sol-voronoi-4cpu
3039 Resultados para ``voronoi`` (utilizando 4 procesadores).
3041 Resultados para ``voronoi`` (utilizando 4 procesadores). Se presenta el
3042 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
3043 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
3044 ejecución) o 20 corridas (para el resto).
3048 Tiempo de ejecución (seg)
3050 .. image:: plots/time-voronoi-4cpu.pdf
3054 Cantidad de recolecciones
3056 .. image:: plots/ncol-voronoi-4cpu.pdf
3060 Uso máximo de memoria (MiB)
3062 .. image:: plots/mem-voronoi-4cpu.pdf
3066 *Stop-the-world* máximo (seg)
3068 .. image:: plots/stw-voronoi-4cpu.pdf
3072 Pausa real máxima (seg)
3074 .. image:: plots/pause-voronoi-4cpu.pdf
3076 En la figura :vref:`fig:sol-voronoi-1cpu` se presentan los resultados para
3077 ``voronoi``, probablemente la prueba más interesante de este conjunto de
3080 Por un lado se puede observar una vez más como baja dramáticamente el tiempo
3081 total de ejecución cuando se empieza a utilizar CDGC. Ya se ha visto que esto
3082 es común en programas que se benefician del caché de ``findSize()``, pero en
3083 este caso no parece provenir toda la ganancia solo de ese cambio, dado que
3084 para TBGC se ve una variación entre los resultados muy grande que desaparece
3085 al cambiar a CDGC, esto no puede ser explicado por esa optimización. En
3086 general la disminución de la variación de los resultados hemos visto que está
3087 asociada al incremento en la precisión en el marcado, dado que los falsos
3088 punteros ponen una cuota de aleatoriedad importante. Pero este tampoco parece
3089 ser el caso, ya que no se observan cambios apreciables al pasar a usar marcado
3092 Lo que se observa en esta oportunidad es un caso patológico de un mal factor
3093 de ocupación del *heap* (ver :ref:`sol_ocup`). Lo que muy probablemente está
3094 sucediendo con TBGC es que luego de ejecutar una recolección, se libera muy
3095 poco espacio, entonces luego de un par de asignaciones, es necesaria una nueva
3096 recolección. En este caso es donde dificulta la tarea de analizar los
3097 resultados la falta de métricas para TBGC, dado que no se pueden observar la
3098 cantidad de recolecciones ni de consumo máximo de memoria. Sin embargo es
3099 fácil corroborar esta teoría experimentalmente, gracias a la opción
3100 ``min_free``. Utilizando la ``min_free=0`` para emular el comportamiento de
3101 TBGC (se recuerda que el valor por omisión es ``min_free=5``), se obtiene una
3102 media de 4 segundos, mucho más parecida a lo obtenido para TBGC.
3104 Otra particularidad de esta prueba es que al utilizar *early collection* el
3105 tiempo de pausa real aumenta notablemente al usar un procesador, mientras que
3106 al usar 4 (ver figura :vref:`fig:sol-voronoi-4cpu` disminuye levemente (además
3107 de otros cambios en el nivel de variación, pero en general las medias no
3111 Resultados para pruebas reales
3112 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3114 A continuación se presentan los resultados obtenidos para las pruebas reales
3115 (ver :ref:`sol_bench_real`). Recordamos que solo se pudo halla un programa que
3116 pueda ser utilizado a este fin, Dil_, y que el objetivo principal de este
3117 trabajo se centra alrededor de obtener resultados positivos para este
3118 programa, por lo que a pesar de ser una única prueba, se le presta particular
3123 .. fig:: fig:sol-dil-1cpu
3125 Resultados para ``dil`` (utilizando 1 procesador).
3127 Resultados para ``dil`` (utilizando 1 procesador). Se presenta el
3128 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
3129 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
3130 ejecución) o 20 corridas (para el resto).
3134 Tiempo de ejecución (seg)
3136 .. image:: plots/time-dil-1cpu.pdf
3140 Cantidad de recolecciones
3142 .. image:: plots/ncol-dil-1cpu.pdf
3146 Uso máximo de memoria (MiB)
3148 .. image:: plots/mem-dil-1cpu.pdf
3152 *Stop-the-world* máximo (seg)
3154 .. image:: plots/stw-dil-1cpu.pdf
3158 Pausa real máxima (seg)
3160 .. image:: plots/pause-dil-1cpu.pdf
3162 .. fig:: fig:sol-dil-4cpu
3164 Resultados para ``dil`` (utilizando 4 procesadores).
3166 Resultados para ``dil`` (utilizando 4 procesadores). Se presenta el
3167 mínimos (en negro), la media centrada entre dos desvíos estándar (en gris),
3168 y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de
3169 ejecución) o 20 corridas (para el resto).
3173 Tiempo de ejecución (seg)
3175 .. image:: plots/time-dil-4cpu.pdf
3179 Cantidad de recolecciones
3181 .. image:: plots/ncol-dil-4cpu.pdf
3185 Uso máximo de memoria (MiB)
3187 .. image:: plots/mem-dil-4cpu.pdf
3191 *Stop-the-world* máximo (seg)
3193 .. image:: plots/stw-dil-4cpu.pdf
3197 Pausa real máxima (seg)
3199 .. image:: plots/pause-dil-4cpu.pdf
3201 En la figura :vref:`fig:sol-dil-1cpu` se presentan los resultados para
3202 ``dil`` al utilizar un procesador. Una vez más vemos una mejoría inmediata del
3203 tiempo total de ejecución al pasar de TBGC a CDGC, y una vez más se debe
3204 principalmente al mal factor de ocupación del *heap* de TBGC, dado que
3205 utilizando CDGC con la opción ``min_free=0`` se obtiene una media del orden de
3206 los 80 segundos, bastante más alta que el tiempo obtenido para TBGC.
3208 Sin embargo se observa un pequeño incremento del tiempo de ejecución al
3209 introducir marcado preciso, y un incremento bastante más importante (de
3210 alrededor del 30%) en el consumo máximo de memoria. Nuevamente, como pasa con
3211 la prueba ``bh``, el efecto es probablemente producto del incremento en el
3212 espacio necesario para almacenar objetos debido a que el puntero a la
3213 información del tipo se guarda al final del bloque (ver :ref:`sol_precise`).
3214 En el cuadro :vref:`t:sol-prec-mem-dil` se puede observar la diferencia de
3215 memoria desperdiciada entre el modo conservativo y preciso.
3217 El pequeño incremento en el tiempo total de ejecución podría estar dado por la
3218 mayor probabilidad de tener *falsos punteros* debido al incremento del tamaño
3219 del *heap*; se recuerda que el *stack* y memoria estática se siguen marcado de
3220 forma conservativa, incluso en modo preciso.
3222 .. ftable:: t:sol-prec-mem-dil
3224 Memoria pedida y asignada para ``dil`` según modo de marcado.
3226 Memoria pedida y asignada para ``dil`` según modo de marcado conservativo
3227 o preciso (acumulativo durante toda la vida del programa).
3229 ============== ============== ============== =================
3230 Memoria Pedida (MiB) Asignada (MiB) Desperdicio (MiB)
3231 ============== ============== ============== =================
3232 Conservativo 307.48 399.94 92.46 (23%)
3233 Preciso 307.48 460.24 152.76 (33%)
3234 ============== ============== ============== =================
3236 También se puede observar una gran disminución del tiempo total de ejecución
3237 (cerca de un 60%, y más de un 200% comparado con TBGC) alrededor de la mitad)
3238 al empezar a usar *eager allocation*, acompañado como es usual de una baja en
3239 la cantidad de recolecciones realizadas (esta vez mayor, de más de 3 veces)
3240 y de una caída drástica del tiempo de pausa real (alrededor de 40 veces más
3241 pequeño); todo esto con un incremento marginal en el consumo total de memoria
3242 (aproximadamente un 5%). En este caso el uso de *early collection* apenas
3243 ayuda a bajar el tiempo de pausa real en un 20% en promedio aproximadamente.
3244 El tiempo de *stop-the-world* cae dramáticamente al empezar a realizar la fase
3245 de marcado de manera concurrente; es 200 veces más pequeño.
3247 Al utilizar 4 procesadores (ver figura :vref:`fig:sol-dil-4cpu`), hay algunos
3248 pequeños cambios. El tiempo total de ejecución es reducido todavía más (un 20%
3249 que cuando se usa 1 procesador) cuando se utiliza *eager allocation*. Además
3250 al utilizar *early collection*, hay otra pequeña ganancia de alrededor del
3251 10%, tanto para el tiempo total de ejecución como para el tiempo de pausa
3258 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3260 Los avances de este trabajo fueron comunicados regularmente a la comunidad de
3261 D_ a través de un blog [LMTDGC]_ y del grupo de noticias de D_. Los
3262 comentarios hechos sobre el primero son en general positivos y denotan una
3263 buena recepción por parte de la comunidad a las modificaciones propuestas.
3265 Una vez agregado el marcado concurrente se hace un anuncio en el grupo de
3266 noticias que también muestra buenos comentarios y aceptación, en particular
3267 por parte de Sean Kelly, encargado de mantener el *runtime* de `D 2.0`_, que
3268 comienza a trabajar en adaptar el recolector con idea de tal vez incluirlo en
3269 el futuro [NGA19235]_. Poco después Sean Kelly publica una versión preliminar
3270 de la adaptación en la lista de correos que coordina el desarrollo del
3271 *runtime* de `D 2.0`_ [DRT117]_.
3273 También se ha mostrado interés de incluirlo en Tango_, aunque no se han ha
3274 comenzado aún con la adaptación, pero debería ser trivial dado que este
3275 trabajo se desarrolla usando Tango_ (y el recolector está basado en el de
3279 .. include:: links.rst
3281 .. vim: set ts=3 sts=3 sw=3 et tw=78 spelllang=es :