X-Git-Url: https://git.llucax.com/z.facultad/75.00/informe.git/blobdiff_plain/e06143e98714199eba6570381554d812fd86556d..409ef528d2b45bdcbcd6868b3d0f82c1edf8e748:/source/solucion.rst diff --git a/source/solucion.rst b/source/solucion.rst index 37062cb..074278a 100644 --- a/source/solucion.rst +++ b/source/solucion.rst @@ -1,6 +1,6 @@ .. Acá va lo que decidí hacer en base al análisis anterior y sus razones. - ESTADO: SIN EMPEZAR + ESTADO: TERMINADO .. _solucion: @@ -8,44 +8,3279 @@ Solución adoptada ============================================================================ -TODO +Como hemos visto en :ref:`dgc`, la mejora del recolector de basura puede ser +abordada desde múltiples flancos, con varias alternativas viables. Por lo +tanto, para reducir la cantidad de posibilidades hay que tener en cuenta uno +de los principales objetivos de este trabajo: encontrar una solución que tenga +una buena probabilidad de ser adoptada por el lenguaje, o alguno de sus +compiladores al menos. Para asegurar esto, la solución debe tener un alto +grado de aceptación en la comunidad, lo que implica algunos puntos claves: +* La eficiencia general de la solución no debe ser notablemente peor, en + ningún aspecto, que la implementación actual. +* Los cambios no deben ser drásticos. +* La solución debe atacar de forma efectiva al menos uno de los problemas + principales del recolector actual. +Bajo estos requerimientos, se concluye que probablemente el área más fértil +para explorar sea la falta de concurrencia por cumplir todos estos puntos: -Recolector naive de referencia ----------------------------------------------------------------------------- +* Si bien hay evidencia en la literatura sobre el incremento del tiempo de + ejecución total de ejecución de un programa al usar algoritmos concurrentes, + éste no es, en general, muy grande comparativamente. +* Existen algoritmos de recolección concurrente que no requieren ningún grado + de cooperación por parte del lenguaje o el compilador. +* La falta de concurrencia y los largos tiempos de pausa es una de las + críticas más frecuentes al recolector actual por parte de la comunidad. + +A pesar de ser la concurrencia la veta principal a explorar en este trabajo, +se intenta abordar los demás problemas planteados siempre que sea posible +hacerlo sin alejarse demasiado del objetivo principal. -TODO +.. highlight:: d +.. _sol_bench: -Set de benchmarks y recolección de estadísticas +Banco de pruebas ---------------------------------------------------------------------------- -TODO +Teniendo en cuenta que uno de los objetivos principales es no empeorar la +eficiencia general de forma notable, la confección de un banco de pruebas es +un aspecto fundamental, para poder comprobar con cada cambio que la eficiencia +final no se vea notablemente afectada. +La confección de un banco de pruebas no es una tarea trivial, mucho menos para +un lenguaje con el nivel de fragmentación que tuvo D_ (que hace que a fines +prácticos hayan 3 versiones del lenguaje compitiendo), y cuya masa crítica de +usuarios es de aficionados que usualmente abandonan los proyectos, quedando +obsoletos rápidamente. +Con el objetivo de confeccionar este banco de pruebas, desde el comienzo del +trabajo se han recolectado (usando como fuente principalmente el grupo de +noticias de D_ [#benchmod]_) programas triviales sintetizados con el único +propósito de mostrar problemas con el recolector de basura. Otros programas de +este estilo fueron escritos explícitamente para este trabajo. -Reescritura del GC actual ----------------------------------------------------------------------------- +Además se han recolectado [#benchmod]_ algunos pequeños programas portados de +otros lenguajes de programación, que si bien son pequeños y tienen como +objetivo ejercitar el recolector de basura, son programas reales que resuelven +un problema concreto, lo que otorga un juego de pruebas un poco más amplio que +los programas triviales. -TODO +.. [#benchmod] Cabe destacar que en general todos los programas recolectados + han sido modificados levemente para ajustarlos mejor a las necesidades del + banco de prueba (entre las modificaciones más frecuentes se encuentran la + conversión de Phobos_ a Tango_ y la eliminación de mensajes por salida + estándar). +Pero probablemente lo más importante para confeccionar un banco de pruebas +verdaderamente útil es disponer de programas reales, que hayan sido diseñados +con el único objetivo de hacer su trabajo, sin pensar en como impacta el +recolector sobre ellos (ni ellos sobre el recolector). Estos programas proveen +las pruebas más realistas y amplias. Desgraciadamente no hay muchos programas +reales escritos en D_ disponibles públicamente, y no se encontró en la +comunidad tampoco una muestra de voluntad por compartir programas privados +para usar como banco de pruebas en este trabajo. +Por lo tanto el banco de pruebas que se conformó como una mezcla de estas tres +grandes categorías. -Conversión a "cloning GC" ----------------------------------------------------------------------------- -TODO +.. _sol_bench_synth: +Pruebas sintetizadas +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Este es el juego de programas triviales, escritos con el único objetivo de +ejercitar un área particular y acotada del recolector. -Resultados + +``bigarr`` +^^^^^^^^^^ +Su objetivo es ejercitar la manipulación de arreglos de tamaño considerable +que almacenan objetos de tamaño pequeño o mediano. Esta prueba fue hallada__ +en el grupo de noticias de D_ y escrita por Babele Dunnit y aunque +originalmente fue concebido para mostrar un problema con la concatenación de +arreglos (como se aprecia en la sentencia ``version(loseMemory)``), ejercita +los aspectos más utilizados del del recolector: manipulación de arreglos +y petición e memoria. Es una de las pruebas que más estresa al recolector ya +que todo el trabajo que realiza el programa es utilizar servicios de éste. + +El código fuente del programa es el siguiente:: + + const IT = 300; + const N1 = 20_000; + const N2 = 40_000; + + class Individual + { + Individual[20] children; + } + + class Population + { + void grow() + { + foreach (inout individual; individuals) + individual = new Individual; + } + Individual[N1] individuals; + } + + version = loseMemory; + + int main(char[][] args) + { + + Population testPop1 = new Population; + Population testPop2 = new Population; + Individual[N2] indi; + for (int i = 0; i < IT; i++) { + testPop1.grow(); + testPop2.grow(); + version (loseMemory) { + indi[] = testPop1.individuals ~ testPop2.individuals; + } + version (everythingOk) { + indi[0 .. N1] = testPop1.individuals; + indi[N1 .. N2] = testPop2.individuals; + } + } + return 0; + } + +__ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=54084 + + +``concpu`` y ``conalloc`` +^^^^^^^^^^^^^^^^^^^^^^^^^ +Estos dos programas fueron escritos especialmente para este trabajo con el fin +de ejercitar la interacción entre el recolector y un *mutator* con varios +hilos. La única diferencia entre ellos es que ``concpu`` lanza hilos que hacen +trabajar de forma intensiva el procesador pero que no utilizan servicios del +recolector, salvo en el hilo principal, mientras que ``conalloc`` utiliza +servicios del recolector en todos los hilos lanzados. + +El objetivo de estos programas es medir el impacto de las pausas del +recolector. Se espera medir dos tipos de pausa principales, por un lado el +tiempo máximo de pausa real, que puede involucrar a más de un hilo y por otro +el tiempo de *stop-the-world*, es decir, el tiempo en que los hilos son +efectivamente pausados por el recolector para tomar una *foto* de la pila +y registros para agregarlos al *root set*. + +Se espera ``concpu`` sea capaz de explotar cualquier reducción en el tiempo de +*stop-the-world*, ya que los hilos solo son interrumpidos por este tipo de +pausa. Por otro lado, se espera que ``conalloc`` sea afectado por el tiempo +máximo de pausa, que podrían sufrir los hilos incluso cuando el *mundo* sigue +su marcha, debido al *lock* global del recolector y que los hilos usan +servicios de éste. + +El código de ``concpu`` es el siguiente:: + + import tango.core.Thread: Thread; + import tango.core.Atomic: Atomic; + import tango.io.device.File: File; + import tango.util.digest.Sha512: Sha512; + import tango.util.Convert: to; + + auto N = 100; + auto NT = 2; + ubyte[] BYTES; + Atomic!(int) running; + + void main(char[][] args) + { + auto fname = args[0]; + if (args.length > 3) + fname = args[3]; + if (args.length > 2) + NT = to!(int)(args[2]); + if (args.length > 1) + N = to!(int)(args[1]); + N /= NT; + running.store(NT); + BYTES = cast(ubyte[]) File.get(fname); + auto threads = new Thread[NT]; + foreach(ref thread; threads) { + thread = new Thread(&doSha); + thread.start(); + } + while (running.load()) { + auto a = new void[](BYTES.length / 4); + a[] = cast(void[]) BYTES[]; + Thread.yield(); + } + foreach(thread; threads) + thread.join(); + } + + void doSha() + { + auto sha = new Sha512; + for (size_t i = 0; i < N; i++) + sha.update(BYTES); + running.decrement(); + } + +El código de ``conalloc`` es igual excepto por la función ``doSha()``, que es +de la siguiente manera:: + + void doSha() + { + for (size_t i = 0; i < N; i++) { + auto sha = new Sha512; + sha.update(BYTES); + } + running.decrement(); + } + + +``mcore`` +^^^^^^^^^ +Escrito por David Schima y también hallado__ en el grupo de noticias de D_, +este programa pretende mostrar como afecta el *lock* global del recolector +en ambientes *multi-core*, incluso cuando a simple vista parecen no utilizarse +servicios del recolector:: + + import tango.core.Thread; + + void main() + { + enum { nThreads = 4 }; + auto threads = new Thread[nThreads]; + foreach (ref thread; threads) { + thread = new Thread(&doAppending); + thread.start(); + } + foreach (thread; threads) + thread.join(); + } + + void doAppending() + { + uint[] arr; + for (size_t i = 0; i < 1_000_000; i++) + arr ~= i; + } + +__ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=103563 + +El secreto está en que la concatenación de arreglos utiliza por detrás +servicios del recolector, por lo tanto un programa multi-hilo en el cual los +hilos (aparentemente) no comparten ningún estado, se puede ver +considerablemente afectado por el recolector (siendo este efecto más visible +en ambientes *multi-core* por el nivel de sincronización extra que significa +a nivel de *hardware*). Cabe destacar que, sin embargo, en Linux_ no es tan +notorio. + + +``split`` +^^^^^^^^^ +Este programa trivial lee un archivo de texto y genera un arreglo de cadenas +de texto resultantes de partir el texto en palabras. Fue escrito por Leonardo +Maffi y también hallado__ en el grupo de noticias de D_. Su objetivo era +mostrar lo ineficiente que puede ser concatenar datos a un mismo arreglo +repetidas veces y ha desembocado en una pequeña optimización que sirvió para +paliar el problema de forma razonablemente efectiva [PAN09]_. + +El código es el siguiente:: + + import tango.io.device.File: File; + import tango.text.Util: delimit; + import tango.util.Convert: to; + + int main(char[][] args) { + if (args.length < 2) + return 1; + auto txt = cast(byte[]) File.get(args[1]); + auto n = (args.length > 2) ? to!(uint)(args[2]) : 1; + if (n < 1) + n = 1; + while (--n) + txt ~= txt; + auto words = delimit!(byte)(txt, cast(byte[]) " \t\n\r"); + return !words.length; + } + +__ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=67673 + + +``rnddata`` +^^^^^^^^^^^ +Este programa fue escrito por Oskar Linde y nuevamente hallado__ en el grupo +de noticias. Fue construido para mostrar como el hecho de que el recolector +sea conservativo puede hacer que al leer datos binarios hayan muchos *falsos +punteros* que mantengan vivas celdas que en realidad ya no deberían ser +accesibles desde el *root set* del grafo de conectividad. + +__ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=46407 + +El código del programa es el siguiente:: + + import tango.math.random.Random; + + const IT = 125; // number of iterations, each creates an object + const BYTES = 1_000_000; // ~1MiB per object + const N = 50; // ~50MiB of initial objects + + class C + { + C c; // makes the compiler not set NO_SCAN + long[BYTES/long.sizeof] data; + } + + void main() { + auto rand = new Random(); + C[] objs; + objs.length = N; + foreach (ref o; objs) { + o = new C; + foreach (ref x; o.data) + rand(x); + } + for (int i = 0; i < IT; ++i) { + C o = new C; + foreach (ref x; o.data) + rand(x); + // do something with the data... + } + } + + +``sbtree`` +^^^^^^^^^^ +Este programa está basado en la prueba de nombre ``binary-trees`` de `The +Computer Language Benchmarks Game`__, una colección de 12 programas escritos +en alrededor de 30 lenguajes de programación para comparar su eficiencia +(medida en tiempo de ejecución, uso de memoria y cantidad de líneas de +código). De este juego de programas se utilizó solo ``binary-trees`` por ser +el único destinado a ejercitar el manejo de memoria. El programa sólo manipula +árboles binarios, creándolos y recorriéndolos inmediatamente (no realiza +ningún trabajo útil). La traducción a D_ fue realizada por Andrey Khropov +y fue hallada__ en el grupo de noticias. + +__ http://shootout.alioth.debian.org/ +__ http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=43991 + +El código fuente es el siguiente:: + + import tango.util.Convert; + alias char[] string; + + int main(string[] args) + { + int N = args.length > 1 ? to!(int)(args[1]) : 1; + int minDepth = 4; + int maxDepth = (minDepth + 2) > N ? minDepth + 2 : N; + int stretchDepth = maxDepth + 1; + int check = TreeNode.BottomUpTree(0, stretchDepth).ItemCheck; + TreeNode longLivedTree = TreeNode.BottomUpTree(0, maxDepth); + for (int depth = minDepth; depth <= maxDepth; depth += 2) { + int iterations = 1 << (maxDepth - depth + minDepth); + check = 0; + for (int i = 1; i <= iterations; i++) { + check += TreeNode.BottomUpTree(i, depth).ItemCheck; + check += TreeNode.BottomUpTree(-i, depth).ItemCheck; + } + } + return 0; + } + + class TreeNode + { + TreeNode left, right; + int item; + + this(int item, TreeNode left = null, TreeNode right = null) + { + this.item = item; + this.left = left; + this.right = right; + } + + static TreeNode BottomUpTree(int item, int depth) + { + if (depth > 0) + return new TreeNode(item, + BottomUpTree(2 * item - 1, depth - 1), + BottomUpTree(2 * item, depth - 1)); + return new TreeNode(item); + } + + int ItemCheck() + { + if (left) + return item + left.ItemCheck() - right.ItemCheck(); + return item; + } + } + + +.. _sol_bench_small: + +Programas pequeños +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Todos los pequeños programas utilizados como parte del banco de prueba +provienen del `Olden Benchmark`__ [CAR95]_. Estos programas fueron diseñados +para probar el lenguaje de programación Olden__; un lenguaje diseñado para +paralelizar programas automáticamente en arquitecturas con memoria +distribuida. Son programas relativamente pequeños (entre 400 y 1000 líneas de +código fuente cada uno) que realizan una tarea secuencial que asigna +estructuras de datos dinámicamente. Las estructuras están usualmente +organizadas como listas o árboles, y muy raramente como arreglos. Los +programas pasan la mayor parte del tiempo alocando datos y el resto usando los +datos alocados, por lo que en general están acotados en tiempo por el uso de +memoria (y no de procesador). + +__ http://www.irisa.fr/caps/people/truong/M2COct99/Benchmarks/Olden/Welcome.html +__ http://www.martincarlisle.com/olden.html + +La traducción a D_ fue realizada por Leonardo Maffi y están basadas a su vez +en la traducción de este juego de pruebas a Java_, JOlden__ [CMK01]_. En Java_ +no se recomienda utilizar este conjunto de pruebas para medir la eficiencia +del recolector de basura, dado que se han creado mejores pruebas para este +propósito, como DaCapo__ [BLA06]_, sin embargo, dada la falta de programas +disponibles en general, y de un conjunto de pruebas especialmente diseñado +para evaluar el recolector de basura en D_, se decide utilizarlas en este +trabajo de todos modos. Sin embargo sus resultados deben ser interpretados con +una pizca de sal por lo mencionado anteriormente. + +__ http://www-ali.cs.umass.edu/DaCapo/benchmarks.html +__ http://www.dacapobench.org/ + +En general (salvo para el programa ``voronoï``) está disponible el código +fuente portado a D_, Java_ y Python_, e incluso varias versiones con distintas +optimizaciones para reducir el consumo de tiempo y memoria. Además provee +comparaciones de tiempo entre todas ellas. Los programas utilizados en este +banco de pruebas son la versión traducida más literalmente de Java_ a D_, ya +que hace un uso más intensivo del recolector que las otras versiones. + +A continuación se da una pequeña descripción de cada uno de los 5 programas +traducidos y los enlaces en donde encontrar el código fuente (y las +comparaciones de tiempos estar disponibles). + + +``bh`` +^^^^^^ +Este programa computa las interacciones gravitatorias entre un número +:math:`N` de cuerpos en tiempo :math:`O(N log N)` y está basado en árboles +heterogéneos de 8 ramas, según el algoritmo descripto por Barnes & Hut +[BH86]_. + +Código fuente disponible en: +http://www.fantascienza.net/leonardo/js/dolden_bh.zip + + +``bisort`` +^^^^^^^^^^ +Este programa ordena :math:`N` números, donde :math:`N` es una potencia de 2, +usando un ordenamiento *Bitonic* adaptativo, un algoritmo paralelo óptimo para +computadoras con memoria compartida, según describen Bilardi & Nicolau +[BN98]_. Utiliza árboles binarios como principal estructuras de datos. + +Código fuente disponible en: +http://www.fantascienza.net/leonardo/js/dolden_bisort.zip + + +``em3d`` +^^^^^^^^ +Este programa modela la propagación de ondas electromagnéticas a través de +objetos en 3 dimensiones. Realiza un cálculo simple sobre un grafo irregular +bipartito (implementado utilizando listas simplemente enlazadas) cuyos nodos +representan valores de campo eléctrico y magnético. El algoritmo es el +descripto por Culler, et al. [CDG93]_. + +Código fuente disponible en: +http://www.fantascienza.net/leonardo/js/dolden_em3d.zip + + +``tsp`` +^^^^^^^ +Este programa implementa una heurística para resolver el problema del viajante +(*traveling salesman problem*) utilizando árboles binarios balanceados. El +algoritmo utilizado es el descripto por Karp [KAR77]_. + + +Código fuente disponible en: +http://www.fantascienza.net/leonardo/js/dolden_tsp.zip + + +``voronoï`` +^^^^^^^^^^^ +Este programa genera un conjunto aleatorio de puntos y computa su diagrama de +Voronoï, una construcción geométrica que permite construir una partición del +plano euclídeo, utilizando el algoritmo descripto por Guibas & Stolfi [GS85]_. + +Código fuente disponible en: http://codepad.org/xGDCS3KO + + +.. _sol_bench_real: + +Programas *reales* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Dil_ (escrito en su mayor parte por Aziz Köksal y publicado bajo licencia +GPL_) es, lamentablemente, el único programa real hallado que, a pesar de +estar incompleto, es lo suficientemente grande, mantenido y estable como para +ser incluido en el banco de pruebas. Se trata de un compilador de D_ escrito +en D_ y está incompleto porque no puede generar código (falta implementar el +análisis semántico y la generación de código), por lo que es principalmente +utilizado para generar documentación a partir del código. + +El programa está compuesto por: + +* 32.000 líneas de código fuente (aproximadamente). +* 86 módulos (o archivos). +* 322 diferentes tipos de datos definidos por el usuario, de los cuales 34 son + tipos *livianos* (``struct``) y 288 tipos polimórficos (``class``), de los + que 260 son subtipos (sub-clases). + +Puede observarse entonces que a pesar de ser incompleto, es una pieza de +software bastante compleja y de dimensión considerable. + +Además, al interpretar código fuente se hace un uso intensivo de cadenas de +texto que en general presentan problemas muy particulares por poder ser +objetos extremadamente pequeños y de tamaños poco convencionales (no múltiplos +de palabras, por ejemplo). A su vez, el texto interpretado es convertido a una +representación interna en forma de árbol (o *árbol de sintaxis abstracta*) +modelado por tipos *livianos* y polimórficos que están organizados en arreglos +dinámicos contiguos y asociativos (que usan muchos servicios del recolector), +y que finalmente son manipulados para obtener y generar la información +necesaria, creando y dejando *morir* objetos constantemente (pero no como única +forma de procesamiento, como otras pruebas sintetizadas). + +Por último, a diferencia de muchos otros programas escritos en D_, que dadas +algunas de las ineficiencias del recolector invierten mucho trabajo en limitar +su uso, este programa no está escrito pensando en dichas limitaciones, por lo +que muestra un funcionamiento muy poco sesgado por estas infortunadas +circunstancias. + +Por todas estas razones, Dil_ es el ejemplar que tal vez mejor sirve a la hora +de medir de forma realista los resultados obtenidos o los avances realizados. +Si bien, como se ha dicho anteriormente, las demás pruebas del banco pueden +ser útiles para encontrar problemas muy particulares, está es la que da una +lectura más cercana a la realidad del uso de un recolector. + + +.. highlight:: pcode + +.. _sol_mod: + +Modificaciones propuestas ---------------------------------------------------------------------------- -TODO +Se decide realizar todas las modificaciones al recolector actual de forma +progresiva e incremental, partiendo como base del recolector de la versión +0.99.9 de Tango_. Las razones que motivan esta decisión son varias; por un +lado es lo más apropiado dados los requerimientos claves mencionados al +principio de este capítulo. Por ejemplo, al hacer cambios incrementales es más +fácil comprobar que la eficiencia no se aleja mucho del actual con cada +modificación y una modificación gradual impone menos resistencia a la +aceptación del nuevo recolector. + +Además la construcción de un recolector de cero es una tarea difícil +considerando que un error en el recolector es extremadamente complejo de +rastrear, dado que en general el error se detecta en el *mutator* y en una +instancia muy posterior al origen real del error. Esto ha sido comprobado de +forma práctica, dado que, a modo de ejercicio para interiorizarse en el +funcionamiento del *runtime* de D_, primero se ha construido desde cero una +implementación de un recolector *naïve*, resultando muy difícil su depuración +por las razones mencionadas. Por el contrario, comenzar con un recolector en +funcionamiento como base hace más sencillo tanto probar cada pequeña +modificación para asegurar que no introduce fallos, como encontrar y reparar +los fallos cuando estos se producen, ya que el código incorrecto introducido +está bien aislado e identificado. + +A continuación se hace un recorrido sobre cada una de las mejoras propuestas, +y en los casos en los que la mejora propone un cambio algorítmico, se analiza +la corrección del algoritmo resultante, partiendo de la base de que el +algoritmo tomado como punto de partida es un marcado y barrido que utiliza la +abstracción tricolor para hacer la fase de marcado de forma iterativa (ver +:ref:`gc_mark_sweep` y :ref:`gc_intro_tricolor`), cuya corrección ya está +probada en la literatura preexistente. + + +.. _sol_config: + +Configurabilidad +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Una de las primeras mejoras propuestas es la posibilidad de configurar el +recolector de forma más sencilla. El requerimiento mínimo es la posibilidad de +configurar el recolector sin necesidad de recompilarlo. La complejidad de esto +surge de que el recolector debe ser transparente para el programa del usuario. + +Configurar el recolector en tiempo de compilación del programa del usuario +probablemente requeriría modificar el compilador, y además, si bien es una +mejora sustancial a la configuración en tiempo de compilación del recolector, +no termina de ser completamente conveniente para realizar pruebas reiteradas +con un mismo programa para encontrar los mejores valores de configuración para +ese programa en particular. + +Por otro lado, permitir configurar el recolector en tiempo de ejecución, una +vez que su estructura interna ya fue definida y creada, puede ser no solo +tedioso y complejo, además ineficiente, por lo tanto esta opción también se +descarta. + +Finalmente, lo que parece ser más apropiado para un recolector, es permitir la +configuración en tiempo de inicialización. Es decir, configurar el recolectar +sin necesidad de recompilar ni el programa del usuario ni el recolector, pero +antes de que el programa del usuario inicie, de manera que una vez iniciado el +recolector con ciertos parámetros, éstos no cambien nunca más en durante la +vida del programa. + +Este esquema provee la mejor relación entre configurabilidad, conveniencia, +eficiencia y simplicidad. Una posibilidad para lograr esto es utilizar +parámetros de línea de comandos, sin embargo no parece ni sencillo (proveer +una forma de leer los parámetros de línea de comandos requiere cambios en el +*runtime*) ni apropiado (el recolector debería ser lo más transparente posible +para el programa del usuario). + +Otra posibilidad es utilizar variables de entorno, que parece ser la opción +más sencilla y apropiada. Sencilla porque las variables de entorno pueden ser +leídas directamente al inicializar el recolector sin necesidad de cooperación +alguna del *runtime*, a través de :manpage:`getenv(3)`. Apropiada porque, si +bien el problema de invasión del programa del usuario también existe, es una +práctica más frecuente y aceptada la configuración de módulos internos +o bibliotecas compartidas a través de variables de entorno. + +Por último, antes de comenzar a usar este esquema de configuración, se +verifica que tomar ciertas decisiones en tiempo de ejecución no impacten en la +eficiencia del recolector. Para esto se convierten algunas opciones que antes +eran solo seleccionables en tiempo de compilación del recolector para que +puedan ser seleccionables en tiempo de inicialización y se comprueba que no +hay una penalización apreciable. + + +.. _sol_config_spec: + +Especificación de opciones +^^^^^^^^^^^^^^^^^^^^^^^^^^ +Para especificar opciones de configuración, hay que hacerlo a través de la +variable de entorno de nombre :envvar:`D_GC_OPTS`. El valor de esa variable es +interpretado de la siguiente manera (en formato similar a :term:`BNF`): + +.. productionlist:: + D_GC_OPTS: `option` ( ':' `option` )* + option: `name` [ '=' `value` ] + name: `namec` `namec`* + value: `valuec`* + namec: `valuec` - '=' + valuec: [0x01-0xFF] - ':' + +Es decir, se compone de una lista de opciones separadas por **:**. Cada opción +se especifica con un nombre, opcionalmente seguido por un valor (separados por +**=**). + +El valor de una opción puede ser un texto arbitrario (exceptuando los +caracteres ``'\0'`` y ``':'`` y de longitud máxima 255), pero cada opción lo +interpreta de forma particular. Como caso general, hay opciones booleanas, que +toman como valor verdadero un cualquier número distinto de 0 (o si el valor es +vació, es decir, solo se indica el nombre de la opción), y como valor falso +cualquier otro texto. + +A continuación se listan las opciones reconocidas por el recolector (indicando +el formato del valor de la opción de tener uno especial): + +``mem_stomp`` + Esta es una opción (booleana) disponible en el recolector original, pero + que se cambia para que sea configurable en tiempo de inicialización + (estando desactivada por omisión). Activa la opción ``MEMSTOMP`` descripta + en :ref:`dgc_debug`. + +``sentinel`` + Esta opción es también booleana (desactivada por omisión), está disponible + en el recolector original, y se la cambia para sea configurable en tiempo + de inicialización. Activa la opción ``SENTINEL`` descripta en + :ref:`dgc_debug`. + +``pre_alloc`` + Esta opción permite crear una cierta cantidad de *pools* de un tamaño + determinado previo a que inicie el programa. Si se especifica solo un + número, se crea un *pool* con ese tamaño en MiB. Si, en cambio, se + especifica una cadena del tipo ``3x1``, el primer número indica la cantidad + de *pools* y el segundo el tamaño en MiB de cada uno (3 *pools* de 1MiB en + este caso). Ver :ref:`sol_pre_alloc` para más detalles sobre la utilidad de + esta opción. + +``min_free`` + El valor de esta opción indica el porcentaje mínimo porcentaje del *heap* + que debe quedar libre luego de una recolección. Siendo un porcentaje, solo + se aceptan valores entre 0 y 100, siendo su valor por omisión 5. Ver + :ref:`sol_ocup` para más detalles sobre su propósito. + +``malloc_stats_file`` + Esta opción sirve para especificar un archivo en el cual escribir un + reporte de todas la operaciones de pedido de memoria realizadas por el + programa (durante su tiempo de vida). Ver :ref:`sol_stats` para más + detalles sobre la información provista y el formato del reporte. + +``collect_stats_file`` + Esta opción sirve para especificar un archivo en el cual escribir un + reporte de todas las recolecciones hechas durante el tiempo de vida del + programa. Ver :ref:`sol_stats` para más detalles sobre la información + provista y el formato del reporte. + +``conservative`` + Esta opción booleana permite desactivar el escaneo preciso del *heap*, + forzando al recolector a ser completamente conservativo (excepto por los + bloques con el atributo ``NO_SCAN`` que siguen sin ser escaneados). Ver + :ref:`sol_precise` para más detalles sobre la existencia de esta opción. + +``fork`` + Esta opción booleana (activada por omisión) permite seleccionar si el + recolector debe correr la fase de marcado en paralelo o no (es decir, si el + recolector corre de forma concurrente con el *mutator*). Para más detalles + ver :ref:`sol_fork`. + +``eager_alloc`` + Esta opción booleana (activada por omisión), sólo puede estar activa si + ``fork`` también está activa y sirve para indicar al recolector que reserve + un nuevo *pool* de memoria cuando una petición no puede ser satisfecha, + justo antes de lanzar la recolección concurrente. Ver + :ref:`sol_eager_alloc` para más detalles sobre el propósito de esta opción. + +``early_collect`` + Esta opción booleana (desactivada por omisión), también sólo puede estar + activa si ``fork`` está activa y sirve para indicar al recolector que lance + una recolección (concurrente) antes de que la memoria libre se termine (la + recolección temprana será disparada cuando el porcentaje de memoria libre + sea menor a ``min_free``). Ver :ref:`sol_early_collect` para más detalles + sobre el propósito de esta opción. + +Cualquier opción o valor no reconocido es ignorado por el recolector. Se +utilizan los valores por omisión de las opciones que no fueron especificadas, +o cuyos valores no pudieron ser interpretados correctamente. + +Para cambiar la configuración del recolector se puede invocar el programa de +la siguiente manera (usando un intérprete de comandos del tipo *bourne +shell*): + +.. code-block:: none + + D_GC_OPTS=conservative:eager_alloc=0:early_collect=1:pre_alloc=2x5 ./programa + +En este ejemplo, se activan las opciones ``conservative`` y ``early_collect``, +se desactiva ``eager_alloc`` y se crean 2 *pools* de 5MiB cada uno al +inicializar el recolector. + + +Reestructuración y cambios menores +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Si bien se decide no comenzar una implementación desde cero, se ha mostrado +(ver :ref:`dgc_bad_code`) que la implementación actual es lo suficientemente +desprolija como para complicar su modificación. Es por esto que se hacen +algunas reestructuraciones básicas del código, reescribiendo o saneando de +forma incremental todas aquellas partes que complican su evolución. + +Además de las modificaciones puramente estéticas (aunque no por eso menos +valuables, ya que la legibilidad y simplicidad del código son un factor +fundamental a la hora de ser mantenido o extendido), se hacen otras pequeñas +mejoras, que se detallan a continuación. + +Remoción de memoria *no-encomendada* +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Se elimina la distinción entre memoria *encomendada* y *no-encomendada* (ver +:ref:`dgc_committed`), pasando a estar *encomendada* toda la memoria +administrada por el recolector. + +Si bien a nivel de eficiencia este cambio no tuvo impacto alguno (cuando en un +principio se especuló con que podría dar alguna ganancia en este sentido), se +elimina el concepto de memoria *encomendada* para quitar complejidad al +código. + +Esta mejora no afecta a la corrección del algoritmo, ya que a nivel lógico el +recolector solo ve la memoria *encomendada*. + +.. _sol_minor_findsize: + +Caché de ``Pool.findSize()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Se crea un caché de tamaño de bloque para el método ``findSize()`` de un +*pool*. Esto acelera considerablemente las operaciones que necesitan pedir el +tamaño de un bloque reiteradamente, por ejemplo, al añadir nuevos elementos +a un arreglo dinámico. En esencia es una extensión a una de las optimizaciones +propuestas por Vladimir Panteleev [PAN09]_, que propone un caché global para +todo el recolector en vez de uno por *pool*. + +Esta mejora tampoco afecta a la corrección del algoritmo, ya que nuevamente no +afecta su comportamiento a nivel lógico, solo cambia detalles en la +implementación de forma transparentes para el algoritmo de recolección. + +Optimizaciones sobre ``findPool()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Al analizar los principales cuellos de botella del recolector, es notoria la +cantidad de tiempo que pasa ejecutando la función ``findPool()``, que dado un +puntero devuelve el *pool* de memoria al cual pertenece. Es por esto que se +minimiza el uso de esta función. Además, dado que los *pools* de memoria están +ordenados por el puntero de comienzo del bloque de memoria manejado por el +*pool*, se cambia la búsqueda (originalmente lineal) por una búsqueda binaria. +Finalmente, dado que la lista de libre está construida almacenando el puntero +al siguiente en las mismas celdas que componen la lista, se almacena también +el puntero al *pool* al que dicha celda pertenece (dado que la celda más +pequeña es de 16 bytes, podemos garantizar que caben dos punteros, incluso +para arquitecturas de 64 bits). De esta manera no es necesario usar +``findPool()`` al quitar una celda de la lista de libres. + +Una vez más, la mejora no afecta la corrección del código. + +.. _sol_pre_alloc: + +Pre-asignación de memoria +^^^^^^^^^^^^^^^^^^^^^^^^^ +Esta opción permite crear una cierta cantidad de *pools* de un tamaño +determinado previo a que inicie el programa. Normalmente el recolector no +reserva memoria hasta que el programa lo pida. Esto puede llegar a evitar +que un programa haga muchas recolecciones al comenzar, hasta que haya +cargado su conjunto de datos de trabajo. + +Se han analizado varios valores por omisión pero ninguno es consistentemente +mejor que comenzar sin memoria asignada, por lo tanto no se cambia el +comportamiento original, pero se agrega una opción (ver ``pre_alloc`` en +:ref:`sol_config_spec`) para que el usuario pueda experimentar con cada +programa en particular si esta opción es beneficiosa. + +Esta opción tampoco cambia la corrección del algoritmo de recolección, solo +sus condiciones iniciales. + +.. _sol_ocup: + +Mejora del factor de ocupación del *heap* +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +El factor de ocupación del *heap* debe ser apropiado por dos razones. Por un +lado, si el *heap* está demasiado ocupado todo el tiempo, serán necesarias +muchas recolecciones que, aunque pequeñas dado que la memoria utilizada es +poca, puede llegar a ser extremadamente ineficiente en casos patológicos (ver +:ref:`dgc_bad_ocup`). Por otro lado, si el tamaño del *heap* es extremadamente +grande (en comparación con el tamaño real del grupo de trabajo del programa), +se harán pocas recolecciones pero cada una es muy costosa, porque el algoritmo +de marcado y barrido es :math:`O(\lvert Heap \rvert)` (ver +:ref:`gc_mark_sweep`). Además la afinidad del caché va a ser extremadamente +pobre. + +Para mantener el factor de ocupación dentro de límites razonables, se agrega +la opción ``min_free`` (ver :ref:`sol_config_spec`). Esta opción indica el +recolector cual debe ser el porcentaje mínimo del *heap* que debe quedar libre +luego de una recolección. En caso de no cumplirse, se pide más memoria al +sistema operativo para cumplir este requerimiento. Además, luego de cada +recolección se verifica que el tamaño del *heap* no sea mayor a ``min_free``, +para evitar que el *heap* crezca de forma descontrolada. Si es mayor +a ``min_free`` se intenta minimizar el uso de memoria liberando *pools* que +estén completamente desocupados, mientras que el factor de ocupación siga +siendo mayor a ``min_free``. Si liberar un *pool* implica pasar ese límite, no +se libera y se pasa a analizar el siguiente y así sucesivamente. + +Esta modificación no afecta a la corrección del algoritmo, ya que no lo afecta +directamente. + +Modificaciones descartadas +^^^^^^^^^^^^^^^^^^^^^^^^^^ +Se realizan varias otras modificaciones, con la esperanza de mejorar la +eficiencia del recolector, pero que, al contrario de lo esperado, empeoran la +eficiencia o la mejoran de forma muy marginal en comparación con la +complejidad agregada. + +Probablemente el caso más significativo, y por tanto el único que vale la pena +mencionar, es la conversión de marcado iterativo a marcado recursivo y luego +a un esquema híbrido. Como se describe en :ref:`dgc_bad`, el marcado iterativo +tiene sus ventajas, pero tiene desventajas también. Al convertirlo a puramente +recursivo, es impracticable por resultar en errores de desbordamiento de pila. + +Por lo tanto se prueba con un esquema híbrido, poniendo un límite a la +recursividad, volviendo al algoritmo iterativo cuando se alcanza este límite. + +La implementación del algoritmo híbrido consiste en los siguientes cambios +sobre el algoritmo original (ver :ref:`dgc_algo_mark`):: + + function mark_phase() is + global more_to_scan = false + global depth = 0 // Agregado + stop_the_world() + clear_mark_scan_bits() + mark_free_lists() + mark_static_data() + push_registers_into_stack() + thread_self.stack.end = get_stack_top() + mark_stacks() + pop_registers_from_stack() + mark_user_roots() + mark_heap() + start_the_world() + + function mark_range(begin, end) is + pointer = begin + global depth++ // Agregado + while pointer < end + [pool, page, block] = find_block(pointer) + if block is not null and block.mark is false + block.mark = true + if block.noscan is false + block.scan = true + if (global depth > MAX_DEPTH) // + more_to_scan = true // + else // Agregado + foreach ptr in block.words // + mark(ptr) // + global depth-- // + +Al analizar los resultados de de esta modificación, se observa una mejoría muy +level, para valores de ``MAX_DEPTH`` mayores a cero (en algunos casos bastante +mayores) y en general para ``MAX_DEPTH`` cero (es decir, usando el algoritmo +de forma completamente iterativa) los resultados son peores, dado que se paga +el trabajo extra sin ganancia alguna. En la figura :vref:`fig:sol-mark-rec` se +puede ver, por ejemplo, el tiempo total de ejecución de Dil_ al generar la +documentación completa del código de Tango_, según varía el valor de +``MAX_DEPTH``. + +.. flt:: fig:sol-mark-rec + + Análisis de tiempo total de ejecución en función del valor de + ``MAX_DEPTH`` + + Tiempo total de ejecución de Dil_ al generar la documentación completa del + código de Tango_ en función del valor de ``MAX_DEPTH``. El rombo no + pertenece a ningún nivel de recursividad, representa el tiempo de ejecución + del algoritmo original (puramente iterativo). + + .. image:: sol-mark-rec-dil.pdf + + +Dado que aumentar el nivel máximo de recursividad significa un uso mayor del +*stack*, y que esto puede impactar en el usuario (si el usuario tuviera un +programa que esté al borde de consumir todo el *stack*, el recolector podría +hacer fallar al programa de una forma inesperada para el usuario, problema que +sería muy difícil de depurar para éste), y que los resultados obtenidos no son +rotundamente superiores a los resultados sin esta modificación, se opta por no +incluir este cambio. Tampoco vale la pena incluirlo como una opción con valor +por omisión 0 porque, como se ha dicho, para este caso el resultado es incluso +peor que sin la modificación. + +Esta modificación mantiene la corrección del recolector dado que tampoco +modifica el algoritmo sino su implementación. Además ambos casos extremos son +correctos (si ``MAX_DEPTH`` es 0, el algoritmo es puramente iterativo y si +pudiera ser infinito resultaría en el algoritmo puramente recursivo). + + +.. _sol_stats: + +Recolección de estadísticas +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Un requerimiento importante, tanto para evaluar los resultados de este trabajo +como para analizar el comportamiento de los programas estudiados, es la +recolección de estadísticas. Hay muchos aspectos que pueden ser analizados +a la hora de evaluar un recolector, y es por esto que se busca que la +recolección de datos sea lo más completa posible. + +Con este objetivo, se decide recolectar datos sobre lo que, probablemente, +sean las operaciones más importantes del recolector: asignación de memoria +y recolección. + +Todos los datos recolectados son almacenados en archivos que se especifican +a través de opciones del recolector (ver :ref:`sol_config_spec`). Los archivos +especificados debe poder ser escritos (y creados de ser necesario) por el +recolector (de otra forma se ignora la opción). El conjunto de datos +recolectados son almacenados en formato :term:`CSV` en el archivo, comenzando +con una cabecera que indica el significado de cada columna. + +Los datos recolectados tienen en general 4 tipos de valores diferentes: + +Tiempo + Se guarda en segundos como número de punto flotante (por ejemplo ``0.12``). + +Puntero + Se guarda en forma hexadecimal (por ejemplo ``0xa1b2c3d4``). + +Tamaño + Se guarda como un número decimal, expresado en bytes (por ejemplo ``32``). + +Indicador + Se guarda como el número ``0`` si es falso o ``1`` si es verdadero. + +Esta modificación mantiene la corrección del recolector dado que no hay cambio +algorítmico alguno. + +Asignación de memoria +^^^^^^^^^^^^^^^^^^^^^ +La recolección de datos sobre asignación de memoria se activa asignando un +nombre de archivo a la opción ``malloc_stats_file``. Por cada asignación de +memoria pedida por el programa (es decir, por cada llamada a la función +``gc_malloc()``) se guarda una fila con los siguientes datos: + +1. Cantidad de segundos que pasaron desde que empezó el programa (*timestamp*). +2. Tiempo total que tomó la asignación de memoria. +3. Valor del puntero devuelto por la asignación. +4. Tamaño de la memoria pedida por el programa. +5. Si esta petición de memoria disparó una recolección o no. +6. Si debe ejecutarse un *finalizador* sobre el objeto (almacenado en la + memoria pedida) cuando ésta no sea más alcanzable (cuando sea barrido). +7. Si objeto carece de punteros (es decir, no debe ser escaneada). +8. Si objeto no debe ser movido por el recolector. +9. Puntero a la información sobre la ubicación de los punteros del objeto. +10. Tamaño del tipo del objeto. +11. Primera palabra con los bits que indican que palabras del tipo deben ser + escaneados punteros y cuales no (en hexadecimal). +12. Primera palabra con los bits que indican que palabras del tipo son + punteros garantizados (en hexadecimal). + +Como puede apreciarse, la mayor parte de esta información sirve más para +analizar el programa que el recolector. Probablemente solo el punto 2 sea de +interés para analizar como se comporta el recolector. + +El punto 8 es completamente inútil, ya que el compilador nunca provee esta +información, pero se la deja por si en algún momento comienza a hacerlo. Los +puntos 9 a 12 provee información sobre el tipo del objeto almacenado, útil +para un marcado preciso (ver :ref:`sol_precise`). + +El punto 6 indica, indirectamente, cuales de los objetos asignados son +*pesados*, ya que éstos son los únicos que pueden tener un *finalizador*. +Además, a través de los puntos 4 y 10 es posible inferir si lo que va +almacenarse es un objeto solo o un arreglo de objetos. + +Recolección de basura +^^^^^^^^^^^^^^^^^^^^^ +Los datos sobre las recolecciones realizadas se guardan al asignar un nombre +de archivo a la opción ``collect_stats_file``. Cada vez que se dispara una +recolección [#solcollect]_ (es decir, cada vez que se llama a la función +``fullcollect()``) se guarda una fila con los siguientes datos: + +1. Cantidad de segundos que pasaron desde que empezó el programa (*timestamp*). +2. Tiempo total que tomó la asignación de memoria que disparó la recolección. +3. Tiempo total que tomó la recolección. +4. Tiempo total que deben pausarse todos los hilos (tiempo de + *stop-the-world*). +5. Cantidad de memoria usada antes de la recolección. +6. Cantidad de memoria libre antes de la recolección. +7. Cantidad de memoria desperdiciada antes de la recolección. +8. Cantidad de memoria utilizada por el mismo recolector antes de la + recolección (para sus estructuras internas). +9. Cantidad de memoria usada después de la recolección. +10. Cantidad de memoria libre después de la recolección. +11. Cantidad de memoria desperdiciada [#solwaste]_ después de la recolección. +12. Cantidad de memoria utilizada por el mismo recolector después de la + recolección. + +Si bien el punto 4 parece ser el más importante para un programa que necesita +baja latencia, dado el *lock* global del recolector, el punto 2 es +probablemente el valor más significativo en este aspecto, dado que, a menos +que el programa en cuestión utilice muy poco el recolector en distintos hilos, +los hilos se verán pausados de todas formas cuando necesiten utilizar el +recolector. + +.. [#solcollect] Esto es en el sentido más amplio posible. Por ejemplo, cuando + se utiliza marcado concurrente (ver :ref:`sol_fork`), se guarda esta + información incluso si ya hay una recolección activa, pero el tiempo de + pausa de los hilos será -1 para indicar que en realidad nunca fueron + pausados. + +.. [#solwaste] Memoria *desperdiciada* se refiere a memoria que directamente + no puede utilizarse debido a la fragmentación. Si por ejemplo, se piden 65 + bytes de memoria, dada la organización del *heap* en bloques (ver + :ref:`dgc_org`), el recolector asignará un bloque de 128 bytes, por lo + tanto 63 bytes quedarán desperdiciados. + + +.. _sol_precise: + +Marcado preciso +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Para agregar el soporte de marcado preciso se aprovecha el trabajo realizado +por Vincent Lang (ver :ref:`dgc_via_art`) [DBZ3463]_, dado que se basa en `D +1.0`_ y Tango_, al igual que este trabajo. Dado el objetivo y entorno común, +se abre la posibilidad de adaptar sus cambios a este trabajo, utilizando una +versión modificada de DMD_ (dado que los cambios aún no son integrados al +compilador oficial). + +.. TODO: Apéndice con parches a DMD y Tango? + +Información de tipos provista por el compilador +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Con éstas modificaciones, el compilador en cada asignación le pasa al +recolector información sobre los punteros del tipo para el cual se pide la +memoria. Esta información se pasa como un puntero a un arreglo de palabras con +la estructura mostrada en la figura :vref:`fig:sol-ptrmap` y que se describe +a continuación. + +.. flt:: fig:sol-ptrmap + :type: table + + Estructura de la información de tipos provista por el compilador + + .. aafig:: + :scale: 110 + + /----- ptrmap + | + V + +-------------+----------------------------+----------------------------+ + | "Tamaño en" | "Bits indicando si la" | "Bits indicando si" | + | "cantidad" | "palabra en una posición" | "la palabra en una" | + | "de" | "debe escanearse como" | "posición es" | + | "palabras" | "si fuera un puntero" | "un puntero" | + +-------------+----------------------------+----------------------------+ + + | | | | + +----- 1 -----+------- ceil(N/BPW) --------+------- ceil(N/BPW) --------+ + | | | | + +* La primera palabra indica el tamaño, en **cantidad de palabras**, del tipo + para el cual se pide la memoria (:math:`N`). +* Las siguientes :math:`ceil(\frac{N}{BPW})` palabras indican, + como un conjunto de bits, qué palabras deben ser escaneadas por el + recolector como si fueran punteros (donde :math:`BPW` indica la cantidad de + bits por palabra, por ejemplo 32 para x86). +* Las siguientes :math:`ceil(\frac{N}{BPW})` palabras son otro conjunto de + bits indicando qué palabras son realmente punteros. + +Los conjuntos de bits guardan la información sobre la primera palabra en el +bit menos significativo. Dada la complejidad de la representación, se ilustra +con un ejemplo. Dada la estructura: + +.. code-block:: d + + union U { + ubyte ub; + void* ptr; + } + + struct S + { + void* begin1; // 1 word + byte[size_t.sizeof * 14 + 1] bytes; // 15 words + // el compilador agrega bytes de "padding" para alinear + void* middle; // 1 word + size_t[14] ints; // 14 words + void* end1; // 1 words + // hasta acá se almacenan los bits en la primera palabra + void* begin2; // 1 words + int i; // 1 word + U u; // 1 word + S* s; // 1 word + } + +El compilador genera la estructura que se muestra en la figura +:vref:`fig:sol-ptrmap-example` (asumiendo una arquitectura de 32 bits). Como +puede apreciarse, el miembro ``u``, al ser una unión entre un puntero y un +dato común, el compilador no puede asegurar que lo que se guarda en esa +palabra sea realmente un puntero, pero indica que debe ser escaneado. El +recolector debe debe ser conservativo en este caso, y escanear esa palabra +como si fuera un puntero. + +.. flt:: fig:sol-ptrmap-example + + Ejemplo de estructura de información de tipos generada para el tipo ``S`` + + .. aafig:: + :textual: + :aspect: 55 + :scale: 110 + + /---- "bit de 'end1'" -\ + | | "Significado" + | /---- "bit de 'middle'" | "de bits" + | | | "en la" + | "bits de" | "bits de" /---- "bit de 'begin1'" | "primera" + | "'ints'" | "'bytes'" | | "palabra" + |/------------\|/-------------\| -/ + V| |V| |V + +----------------------------------+ + | 00000000000000000000000000100100 | "Tamaño en cantidad de palabras (36)" + +==================================+ --\ + | 10000000000000010000000000000001 | | "Bits que indican si hay que" + +----------------------------------+ | "escanear una palabra según" + | 00000000000000000000000000001101 | | "su posición" + +==================================+ --+ + | 10000000000000010000000000000001 | | "Bits que indican si hay un" + +----------------------------------+ | "puntero en la palabra según" + | 00000000000000000000000000001001 | | "su posición" + +----------------------------------+ --/ + | |AAAA + \--------------------------/|||| -\ + "bits de relleno" |||| | + |||| | "Significado" + "bit de 's'" |||| | "de bits" + | |||| | "en la" + \---------------/||\---- "bit de 'begin2'" | "segunda" + || | "palabra" + /---------------/\---- "bit de 'i'" | + | | + "bit de 'u'" -/ + +Si una implementación quisiera mover memoria (ver :ref:`gc_moving`), debería +mantener inmóvil a cualquier objeto que sea apuntado por una palabra de estas +características, ya que no es seguro actualizar la palabra con la nueva +posición el objeto movido. Es por esta razón que se provee desglosada la +información sobre lo que hay que escanear, y lo que es realmente un puntero +(que puede ser actualizado de forma segura por el recolector de ser +necesario). + +Implementación en el recolector +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +La implementación está basada en la idea original de David Simcha, pero +partiendo de la implementación de Vincent Lang (que está basada en Tango_) +y consiste en almacenar el puntero a la estructura con la descripción del tipo +generada por el compilador al final del bloque de datos. Este puntero solo se +almacena si el bloque solicitado no tiene el atributo ``NO_SCAN``, dado que en +ese caso no hace falta directamente escanear ninguna palabra del bloque. + +En la figura :vref:`fig:sol-ptrmap-blk` se puede ver, como continuación del +ejemplo anterior, como se almacenaría en memoria un objeto del tipo ``S``. + +.. flt:: fig:sol-ptrmap-blk + + Ejemplo de bloque que almacena un objeto de tipo ``S`` con información de + tipo + + .. aafig:: + :scale: 110 + + | | + +------------------------ 256 bytes -----------------------------+ + | | + + +----------------------------------+-----------------------+-----+ + | | | | + | Objeto | Desperdicio | Ptr | + | | | | + +----------------------------------+-----------------------+-----+ + + | | | | + +------------ 144 bytes -----------+------ 108 bytes ------+- 4 -+ + | | | | + +Un problema evidente de este esquema es que si el tamaño de un objeto se +aproxima mucho al tamaño de bloque (difiere en menos de una palabra), el +objeto ocupará el doble de memoria. + +El algoritmo de marcado se cambia de la siguiente forma:: + + // Agregado + global conservative_scan = [1, 1, 0] + + // Agregado + function must_scan_word(pos, bits) is + return bits[pos / BITS_PER_WORD] & (1 << (pos % BITS_PER_WORD)) + + function mark_range(begin, end, ptrmap) is // Modificado + number_of_words_in_type = ptrmap[0] // Agregado + size_t* scan_bits = ptrmap + 1 // Agregado + pointer = begin + while pointer < end + foreach word_pos in 0..number_of_words_in_type // + if not must_scan_word(n, scan_bits) // Agregado + continue // + [pool, page, block] = find_block(pointer) + if block is not null and block.mark is false + block.mark = true + if block.noscan is false + block.scan = true + global more_to_scan = true + pointer += number_of_words_in_type // Modificado + + function mark_heap() is + while global more_to_scan + global more_to_scan = false + foreach pool in heap + foreach page in pool + if page.block_size <= PAGE // saltea FREE y CONTINUATION + foreach block in page + if block.scan is true + block.scan = false + if page.block_size is PAGE // obj grande // + begin = cast(byte*) page // + end = find_big_object_end(pool, page) // + else // objeto pequeño // + begin = block.begin // + end = block.end // Modificado + ptrmap = global conservative_scan // + if NO_SCAN not in block.attrs // + end -= size_t.sizeof // + ptrmap = cast(size_t*) *end // + mark_range(begin, end, ptrmap) // + + function mark_static_data() is + mark_range(static_data.begin, static_data.end, + global conservative_scan) // Agregado + + function mark_stacks() is + foreach thread in threads + mark_range(thread.stack.begin, thread.stack.end, + global conservative_scan) // Agregado + + function mark_user_roots() is + foreach root_range in user_roots + mark_range(root_range.begin, root_range.end, + global conservative_scan) // Agregado + +Las funciones de asignación de memoria se modifican de forma similar, para +guardar el puntero a la información de tipos. Esta implementación utiliza solo +la información sobre que palabras hay que tratar como punteros (deben ser +escaneadas); la información sobre qué palabras son efectivamente punteros no +se utiliza ya que no se mueven celdas. + +El algoritmo sigue siendo correcto, puesto que solamente se dejan de escanear +palabras que el compilador sabe que no pueden ser punteros. Si bien el +lenguaje permite almacenar punteros en una variable que no lo sea, esto es +comportamiento indefinido por lo tanto un programa que lo hace no es +considerado correcto, por lo cual el recolector tampoco debe ser correcto en +esas circunstancias. + +Cabe destacar que la información de tipos solo se provee para objetos +almacenados en el *heap*, el área de memoria estática, registros del +procesador y la pila de todos los hilos siguen siendo escaneados de forma +completamente conservativa. Se puede forzar el escaneo puramente conservativo +utilizando la opción ``conservative`` (ver :ref:`sol_config_spec`). + + +.. _sol_fork: + +Marcado concurrente +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Finalmente se procede al objetivo primario de este trabajo, hacer que la fase +más costosa del recolector (el marcado) pueda correr de manera concurrente con +el *mutator*, con el objeto principal de disminuir el tiempo de pausa. + +Cabe aclarar, una vez más, que si bien los recolectores concurrentes buscan +disminuir solo el tiempo de *stop-the-world*, en este caso es también +fundamental disminuir el tiempo máximo que está tomado el *lock* global, dado +que ese tiempo puede convertirse en una pausa para todos los threads que +requieran servicios del recolector. + +Se decide basar la implementación en el *paper* "Non-intrusive Cloning Garbage +Collector with Stock Operating System Support" [RODR97]_ por las siguientes +razones principales: + +* Su implementación encaja de forma bastante natural con el diseño del + recolector actual, por lo que requiere pocos cambios, lo que hace más + factible su aceptación. +* Está basado en la llamada al sistema :manpage:`fork(2)`, que no solo está + muy bien soportada (y de manera muy eficiente) en Linux_, debe estar + soportada en cualquier sistema operativo :term:`POSIX`. +* No necesita instrumentar el código incluyendo barreras de memoria para + informar al recolector cuando cambia el grafo de conectividad. Este es un + aspecto fundamental, dada la filosofía de D_ de no pagar el precio de cosas + que no se usan. La penalización en la eficiencia solo se paga cuando corre + el recolector. Este aspecto también es crítico a la hora de evaluar la + aceptación de la solución por parte de la comunidad. +* Dada su sencillez general, no es difícil ofrecer el algoritmo concurrente + como una opción, de manera que el usuario pueda optar por usarlo o no. + +Llamada al sistema *fork* +^^^^^^^^^^^^^^^^^^^^^^^^^ +El término *fork* proviene del inglés y significa *tenedor* de manera textual, +pero se lo utiliza como analogía de una bifurcación. La operación crea una +copia (llamada *hijo*) del proceso que la ejecuta (llamado *padre*). + +El punto más importante es que se crea un espacio de direcciones de memoria +separado para el proceso hijo y una copia exacta de todos los segmentos de +memoria del proceso padre. Es por esto que cualquier modificación que se haga +en el proceso padre, no se refleja en el proceso hijo (y viceversa), a menos +que la memoria sea compartida entre los procesos de forma explícita. + +Esto, sin embargo, no significa que la memoria física sea realmente duplicada; +en general todos los sistemas operativos modernos (como Linux_) utilizan una +técnica llamada *COW* (de *copy-on-write* en inglés, *copiar-al-escribir* en +castellano) que retrasa la copia de memoria hasta que alguno de los dos +procesos escribe en un segmento. Recién en ese momento el sistema operativo +realiza la copia de **ese segmento solamente**. Es por esto que la operación +puede ser muy eficiente, y la copia de memoria es proporcional a la cantidad +de cambios que hayan. + +:manpage:`fork(2)` tiene otra propiedad importante de mencionar: detiene todos +los hilos de ejecución en el proceso hijo. Es decir, el proceso hijo se crear +con un solo hilo (el hilo que ejecutó la operación de :manpage:`fork(2)`). + +Algoritmo +^^^^^^^^^ +Lo que propone el algoritmo es muy sencillo, utilizar la llamada al sistema +:manpage:`fork(2)` para crear una *fotografía* de la memoria del proceso en un +nuevo proceso. En el proceso padre sigue corriendo el *mutator* y en el +proceso hijo se corre la fase de marcado. El *mutator* puede modificar el +grafo de conectividad pero los cambios quedan aislados el hijo (el marcado), +que tiene una visión consistente e inmutable de la memoria. El sistema +operativo duplica las páginas que modifica el padre bajo demanda, por lo tanto +la cantidad de memoria física realmente copiada es proporcional a la cantidad +y dispersión de los cambios que haga el *mutator*. + +La corrección del algoritmo se mantiene gracias a que la siguiente invariante +se preserva: + + Cuando una celda se convierte en basura, permanece como basura hasta ser + reciclada por el recolector. + +Es decir, el *mutator* no puede *resucitar* una celda *muerta* y esta +invariante se mantiene al correr la fase de marcado sobre una vista inmutable +de la memoria. El único efecto introducido es que el algoritmo toma una +aproximación más conservativa. Es decir, lo que sí puede pasar es que una +celda que pasó a estar *muerta* una vez que la fase de marcado se inició, pero +antes de que ésta termine, la celda no se reciclará hasta la próxima +recolección, dado que este algoritmo no incluye una comunicación entre +*mutator* y recolector para notificar cambios en el grafo de conectividad. +Pero esto no afecta la corrección del algoritmo, ya que un recolector es +correcto cuando nunca recicla celdas *vivas*. + +La única comunicación necesaria entre el *mutator* y el recolector son los +bits de marcado (ver :ref:`dgc_impl`), dado que la fase de barrido debe correr +en el proceso padre. No es necesaria ningún tipo de sincronización entre +*mutator* y recolector más allá de que uno espera a que el otro finalice. + +Además de almacenar el conjunto de bits ``mark`` en memoria compartida entre +el proceso padre e hijo (necesario para la fase de barrido), las +modificaciones necesarias para hacer la fase de marcado concurrente son las +siguientes [#solforkerr]_:: + + function collect() is + stop_the_world() + fflush(null) // evita que se duplique la salida de los FILE* abiertos + child_pid = fork() + if child_pid is 0 // proceso hijo + mark_phase() + exit(0) // termina el proceso hijo + // proceso padre + start_the_world() + wait(child_pid) + sweep() + + function mark_phase() is + global more_to_scan = false + // Borrado: stop_the_world() + clear_mark_scan_bits() + mark_free_lists() + mark_static_data() + push_registers_into_stack() + thread_self.stack.end = get_stack_top() + mark_stacks() + pop_registers_from_stack() + mark_user_roots() + mark_heap() + // Borrado: start_the_world() + +Como se puede observar, el cambio es extremadamente sencillo. Sigue siendo +necesario un tiempo mínimo de pausa (básicamente el tiempo que tarda la +llamada al sistema operativo :manpage:`fork(2)`) para guardar una vista +consistente de los registros del CPU y *stacks* de los hilos. Si bien el +conjunto de bits ``mark`` es compartido por el proceso padre e hijo dado que +es necesario para *comunicar* las fases de marcado y barrido, cabe notar que +nunca son utilizados de forma concurrente (la fase de barrido espera que la +fase de marcado termine antes de usar dichos bits), por lo tanto no necesitan +ningún tipo de sincronización y nunca habrá más de una recolección en proceso +debido al *lock* global del recolector. + +A pesar de que con estos cambios el recolector técnicamente corre de forma +concurrente, se puede apreciar que para un programa con un solo hilo el +tiempo máximo de pausa seguirá siendo muy grande, incluso más grande que antes +dado el trabajo extra que impone crear un nuevo proceso y duplicar las páginas +de memoria modificadas. Lo mismo le pasará a cualquier hilo que necesite hacer +uso del recolector mientras hay una recolección en proceso, debido al *lock* +global. + +Para bajar este tiempo de pausa se experimenta con dos nuevas mejoras, que se +describen a continuación, cuyo objetivo es correr la fase de marcado de forma +concurrente a **todos** los hilos, incluyendo el hilo que la disparó. + +.. [#solforkerr] Se omite el manejo de errores y la activación/desactivación + del marcado concurrente a través de opciones del recolector para facilitar + la comprensión del algoritmo y los cambios realizados. Si devuelve con + error la llamada a ``fork()`` o ``waitpid()``, se vuelve al esquema + *stop-the-world* como si se hubiera desactivado el marcado concurrente + utilizando la opción del recolector ``fork=0``. + + +.. _sol_eager_alloc: + +Creación ansiosa de *pools* (*eager allocation*) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Esta mejora, que puede ser controlada a través de la opción ``eager_alloc`` +(ver :ref:`sol_config_spec`), consiste en crear un nuevo *pool* cuando un +pedido de memoria no puede ser satisfecho, justo después de lanzar la +recolección. Esto permite al recolector satisfacer la petición de memoria +inmediatamente, corriendo la fase de marcado de forma realmente concurrente, +incluso para programas con un solo hilo o programas cuyos hilos usan +frecuentemente servicios del recolector. El precio a pagar es un mayor uso de +memoria de forma temporal (y el trabajo extra de crear y eliminar *pools* más +frecuentemente), pero es esperable que el tiempo máximo de pausa **real** se +vea drásticamente disminuido. + +A simple vista las modificaciones necesarias para su implementación parecieran +ser las siguientes:: + + // Agregado + global mark_pid = 0 + + // Agregado + function mark_is_running() is + return global mark_pid != 0 + + function collect() is + if mark_is_running() // + finished = try_wait(global mark_pid) // + if finished // Agregado + mark_pid = 0 // + sweep() // + return // + stop_the_world() + child_pid = fork() + fflush(null) + if child_pid is 0 // proceso hijo + mark_phase() + exit(0) + // proceso padre + start_the_world() + // Borrado: wait(child_pid) + global mark_pid = child_pid + +Sin embargo con sólo estas modificaciones el algoritmo deja de ser correcto, +ya que tres cosas problemáticas pueden suceder: + +1. Puede llamarse a la función ``minimize()`` mientras hay una fase de marcado + corriendo en paralelo. Esto puede provocar que se libere un *pool* mientras + se lo está usando en la fase de marcado, lo que no sería un problema + (porque el proceso de marcado tiene una copia) si no fuera porque los bits + de marcado, que son compartidos por los procesos, se liberan con el *pool*. +2. Si un bloque libre es asignado después de que la fase de marcado comienza, + pero antes de que termine, ese bloque será barrido dado la función + ``rebuild_free_lists()`` puede reciclar páginas si todos sus bloques tienen + el bit ``freebits`` activo (ver :ref:`dgc_algo_sweep`). +3. El *pool* creado ansiosamente, tendrá sus bits de marcado sin activar, por + lo que en la fase de barrido será interpretado como memoria libre, incluso + cuando puedan estar siendo utilizados por el *mutator*. + +El punto 1 sencillamente hace que el programa finalice con una violación de +segmento (en el mejor caso) y 2 y 3 pueden desembocar en la liberación de una +celda alcanzable por el *mutator*. + +El punto 1 se resuelve a través de la siguiente modificación:: + + function minimize() is + if mark_is_running() // Agregado + return // + for pool in heap + all_free = true + for page in pool + if page.block_size is not FREE + all_free = false + break + if all_free is true + free(pool.pages) + free(pool) + heap.remove(pool) + +La resolución del punto 2 es un poco más laboriosa, ya que hay que mantener +actualizado los ``freebits``, de forma que las celdas asignadas después de +empezar la fase de marcado no sean barridas por tener ese bit activo:: + + function new_big(size) is + number_of_pages = ceil(size / PAGE_SIZE) + pages = find_pages(number_of_pages) + if pages is null + collect() + pages = find_pages(number_of_pages) + if pages is null + minimize() + pool = new_pool(number_of_pages) + if pool is null + return null + pages = assign_pages(pool, number_of_pages) + pages[0].block.free = true // Agregado + pages[0].block_size = PAGE + foreach page in pages[1 .. end] + page.block_size = CONTINUATION + return pages[0] + + function assign_page(block_size) is + foreach pool in heap + foreach page in pool + if page.block_size is FREE + page.block_size = block_size + foreach block in page + block.free = true // Agregado + free_lists[page.block_size].link(block) + + function mark_phase() is + global more_to_scan = false + // Borrado: clear_mark_scan_bits() + // Borrado: mark_free_lists() + clear_scan_bits() // Agregado + mark_free() // + mark_static_data() + push_registers_into_stack() + thread_self.stack.end = get_stack_top() + mark_stacks() + pop_registers_from_stack() + mark_user_roots() + mark_heap() + + // Agregado + function clear_scan_bits() is + // La implementación real limpia los bits en bloques de forma eficiente + foreach pool in heap + foreach page in pool + foreach block in page + block.scan = false + + // Agregado + function mark_free() is + // La implementación real copia los bits en bloques de forma eficiente + foreach pool in heap + foreach page in pool + foreach block in page + block.mark = block.free + + function free_big_object(pool, page) is + pool_end = cast(byte*) pool.pages + (PAGE_SIZE * pool.number_of_pages) + do + page.block_size = FREE + page.block.free = true // Agregado + page = cast(byte*) page + PAGE_SIZE + while page < pool_end and page.block_size is CONTINUATION + + function new(size, attrs) is + block_size = find_block_size(size) + if block_size < PAGE + block = new_small(block_size) + else + block = new_big(size) + if block is null + throw out_of_memory + if final in attrs + block.final = true + if noscan in attrs + block.noscan = true + block.free = false // Agregado + return cast(void*) block + + funciones new_pool(number_of_pages = 1) is + pool = alloc(pool.sizeof) + if pool is null + return null + pool.number_of_pages = number_of_pages + pool.pages = alloc(number_of_pages * PAGE_SIZE) + if pool.pages is null + free(pool) + return null + heap.add(pool) + foreach page in pool + page.block_size = FREE + foreach block in page // + block.free = true // Agregado + block.mark = true // + return pool + +Finalmente, el punto número tres puede ser solucionado con el siguiente +pequeño cambio:: + + funciones new_pool(number_of_pages = 1) is + pool = alloc(pool.sizeof) + if pool is null + return null + pool.number_of_pages = number_of_pages + pool.pages = alloc(number_of_pages * PAGE_SIZE) + if pool.pages is null + free(pool) + return null + heap.add(pool) + foreach page in pool + page.block_size = FREE + foreach block in page // Agregado + block.mark = true // + return pool + +La solución es conservativa porque, por un lado evita la liberación de *pools* +mientras haya una recolección en curso (lo que puede hacer que el consumo de +memoria sea un poco mayor al requerido) y por otro asegura que, como se +mencionó anteriormente, los cambios hechos al grafo de conectividad luego de +iniciar la fase de marcado y antes de que ésta termine, no serán detectados +por el recolector hasta la próxima recolección (marcar todos los bloques de +un nuevo *pool* como el bit ``mark`` asegura que que la memoria no sea +recolectada por la fase de barrido cuando termine el marcado). + +Estas modificaciones son las que hacen que el algoritmo siga siendo correcto, +asegurando que no se van a liberar celdas *vivas* (a expensas de diferir la +liberación de algunas celdas *muertas* por algún tiempo). + + +.. _sol_early_collect: + +Recolección temprana (*early collection*) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Esta mejora, que puede ser controlada a través de la opción ``early_collect`` +(ver :ref:`sol_config_spec`), consiste en lanzar una recolección preventiva, +antes de que una petición de memoria falle. El momento en que se lanza la +recolección es controlado por la opción ``min_free`` (ver :ref:`sol_ocup`). + +De esta forma también puede correr de forma realmente concurrente el *mutator* +y el recolector, al menos hasta que se acabe la memoria, en cuyo caso, a menos +que la opción ``eager_alloc`` (ver :ref:`sol_eager_alloc`) también esté +activada, se deberá esperar a que la fase de marcado termine para recuperar +memoria en la fase de barrido. + +Para facilitar la comprensión de esta mejora se muestran sólo los cambios +necesarios si no se utiliza la opción ``eager_alloc``:: + + function collect(early = false) is // Modificado + if mark_is_running() + finished = try_wait(global mark_pid) + if finished + mark_pid = 0 + sweep() + return // + else if early // Agregado + return // + stop_the_world() + fflush(null) + child_pid = fork() + if child_pid is 0 // proceso hijo + mark_phase() + exit(0) + // proceso padre + start_the_world() + if early // + global mark_pid = child_pid // + else // Agregado + wait(child_pid) // + sweep() // + + // Agregado + function early_collect() is + if not collect_in_progress() and (percent_free < min_free) + collect(true) + + function new(size, attrs) is + block_size = find_block_size(size) + if block_size < PAGE + block = new_small(block_size) + else + block = new_big(size) + if block is null + throw out_of_memory + if final in attrs + block.final = true + if noscan in attrs + block.noscan = true + early_collect() // Agregado + return cast(void*) block + +Es de esperarse que cuando no está activa la opción ``eager_alloc`` por un +lado el tiempo de pausa máximo no sea tan chico como cuando sí lo está (dado +que si la recolección no se lanza de forma suficientemente temprana se va +a tener que esperar que la fase de marcado termine), y por otro que se hagan +más recolecciones de lo necesario (cuando pasa lo contrario, se recolecta más +temprano de lo que se debería). Sin embargo, también es de esperarse que el +consumo de memoria sea un poco menor que al usar la opción ``eager_alloc``. + +En cuanto a la corrección del algoritmo, éste solamente presenta los problemas +número 1 y 2 mencionados en :ref:`sol_eager_alloc`, dado que jamás se crean +nuevos *pools* y la solución es la ya presentada, por lo tanto el algoritmo +sigue siendo correcto con los cuidados pertinentes. + + + +Resultados +---------------------------------------------------------------------------- + +Los resultados de las modificación propuestas en la sección anterior (ver +:ref:`sol_mod`) se evalúan utilizando el conjunto de pruebas mencionado en la +sección :ref:`sol_bench`). + +En esta sección se describe la forma en la que el conjunto de pruebas es +utilizado, la forma en la que se ejecutan los programas para recolectar dichos +resultados y las métricas principales utilizadas para analizarlos. + +A fines prácticos, y haciendo alusión al nombre utilizado por Tango_, en esta +sección se utiliza el nombre **TBGC** (acrónimo para el nombre en inglés +*Tango Basic Garbage Collector*) para hacer referencia al recolector original +provisto por Tango_ 0.99.9 (que, recordamos, es el punto de partida de este +trabajo). Por otro lado, y destacando la principal modificación propuesta por +este trabajo, haremos referencia al recolector resultante de éste utilizando +el nombre **CDGC** (acrónimo para el nombre en inglés *Concurrent D Garbage +Collector*). + + +Ejecución del conjunto de pruebas +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Dado el indeterminismo inherente a los sistemas operativos de tiempo +compartido modernos, se hace un particular esfuerzo por obtener resultados lo +más estable posible. + +Hardware y software utilizado +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Para realizar las pruebas se utiliza el siguiente hardware: + +* Procesador Intel(R) Core(TM)2 Quad CPU Q8400 @ 2.66GHz. +* 2GiB de memoria RAM. + +El entorno de software es el siguiente: + +* Sistema operativo Debian_ Sid (para arquitectura *amd64*). +* Linux_ 2.6.35.7. +* DMD_ 1.063 modificado para proveer información de tipos al recolector (ver + :ref:`sol_precise`). +* *Runtime* Tango_ 0.99.9 modificado para utilizar la información de tipos + provista por el compilador modificado. +* GCC_ 4.4.5. +* Embedded GNU_ C Library 2.11.2. + +Si bien el sistema operativo utiliza arquitectura *amd64*, dado que DMD_ +todavía no soporta 64 bits, se compila y corren los programas de D_ en 32 +bits. + +Opciones del compilador +^^^^^^^^^^^^^^^^^^^^^^^ +Los programas del conjunto de pruebas se compilan utilizando las siguientes +opciones del compilador DMD_: + +``-O`` + Aplica optimizaciones generales. + +``-inline`` + Aplica la optimización de expansión de funciones. Consiste en sustituir la + llamada a función por el cuerpo de la función (en general solo para + funciones pequeñas). + +``-release`` + No genera el código para verificar pre y post-condiciones, invariantes de + representación, operaciones fuera de los límites de un arreglo y + *assert*\ 's en general (ver :ref:`d_dbc`). + +Parámetros de los programas +^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Los programas de prueba se ejecutan siempre con los mismos parámetros (a menos +que se especifique lo contrario), que se detallan a continuación. + +.. highlight:: none + +``conalloc`` + ``40 4 bible.txt`` + + Procesa 40 veces un archivo de texto plano (de 4MiB de tamaño) [#solbible]_ + utilizando 4 hilos (más el principal). + +``concpu`` + ``40 4 bible.txt`` + + Procesa 40 veces un archivo de texto plano (de 4MiB de tamaño) [#solbible]_ + utilizando 4 hilos (más el principal). + +``split`` + ``bible.txt 2`` + + Procesa dos veces un archivo de texto plano (de 4MiB de tamaño) + [#solbible]_. + +``sbtree`` + ``16`` + + Construyen árboles con profundidad máxima 16. + +``bh`` + ``-b 4000`` + + Computa las interacciones gravitatorias entre 4.000 cuerpos. + +``bisort`` + ``-s 2097151`` + + Ordena alrededor de 2 millones de números (exactamente :math:`2^21 + = 2097151`). + +``em3d`` + ``-n 4000 -d 300 -i 74`` + + Realiza 74 iteraciones para modelar 4.000 nodos con grado 300. + +``tsp`` + ``-c 1000000`` + + Resuelve el problema del viajante a través de una heurística para un + millón de ciudades. + +``voronoi`` + ``-n 30000`` + + Se construye un diagrama con 30.000 nodos. + +``dil`` + ``ddoc $dst_dir -hl --kandil -version=Tango -version=TangoDoc + -version=Posix -version=linux $tango_files`` + + Genera la documentación de todo el código fuente de Tango_ 0.99.9, donde + ``$dst_dir`` es el directorio donde almacenar los archivos generados + y ``$tango_files`` es la lista de archivos fuente de Tango_. + +El resto de los programas se ejecutan sin parámetros (ver :ref:`sol_bench` +para una descripción detallada sobre cada uno). + +.. [#solbible] El archivo contiene la Biblia completa, la versión traducida al + inglés autorizada por el Rey Jaime o Jacobo (*Authorized King James + Version* en inglés). Obtenida de: http://download.o-bible.com:8080/kjv.gz + +Recolectores y configuraciones utilizadas +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +En general se presentan resultados para TBGC y varias configuraciones de CDGC, +de manera de poder tener una mejor noción de que mejoras y problemas puede +introducir cada una de las modificaciones más importantes. + +CDGC se utiliza con siguientes configuraciones: + +.. highlight:: none + +cons + En modo conservativo. Específicamente, utilizando el juego de opciones:: + + conservative=1:fork=0:early_collect=0:eager_alloc=0 + +prec + En modo preciso (ver :ref:`sol_precise`). Específicamente, utilizando el + juego de opciones:: + + conservative=0:fork=0:early_collect=0:eager_alloc=0 + +fork + En modo preciso activando el marcado concurrente (ver :ref:`sol_fork`). + Específicamente, utilizando el juego de opciones:: + + conservative=0:fork=1:early_collect=0:eager_alloc=0 + +ecol + En modo preciso activando el marcado concurrente con recolección temprana + (ver :ref:`sol_early_collect`). Específicamente, utilizando el juego de + opciones:: + + conservative=0:fork=1:early_collect=1:eager_alloc=0 + +eall + En modo preciso activando el marcado concurrente con creación ansiosa de + *pools* (ver :ref:`sol_eager_alloc`). Específicamente, utilizando el juego + de opciones:: + + conservative=0:fork=1:early_collect=0:eager_alloc=1 + +todo + En modo preciso activando el marcado concurrente con recolección temprana + y creación ansiosa de *pools*. Específicamente, utilizando el juego de + opciones:: + + conservative=0:fork=1:early_collect=1:eager_alloc=1 + +Métricas utilizadas +^^^^^^^^^^^^^^^^^^^ +Para analizar los resultados se utilizan varias métricas. Las más importantes +son: + +* Tiempo total de ejecución. +* Tiempo máximo de *stop-the-world*. +* Tiempo máximo de pausa real. +* Cantidad máxima de memoria utilizada. +* Cantidad total de recolecciones realizadas. + +El tiempo total de ejecución es una buena medida del **rendimiento** general +del recolector, mientras que la cantidad total de recolecciones realizadas +suele ser una buena medida de su **eficacia** [#soleficacia]_. + +Los tiempos máximos de pausa, *stop-the-world* y real, son una buena medida de +la **latencia** del recolector; el segundo siendo una medida más realista dado +que es raro que los demás hilos no utilicen servicios del recolector mientras +hay una recolección en curso. Esta medida es particularmente importante para +programas que necesiten algún nivel de ejecución en *tiempo-real*. + +En general el consumo de tiempo y espacio es un compromiso, cuando se consume +menos tiempo se necesita más espacio y viceversa. La cantidad máxima de +memoria utilizada nos da un parámetro de esta relación. + +.. [#soleficacia] Esto no es necesariamente cierto para recolectores con + particiones (ver :ref:`gc_part`) o incrementales (ver :ref:`gc_inc`), dado + que en ese caso podría realizar muchas recolecciones pero cada una muy + velozmente. + +Métodología de medición +^^^^^^^^^^^^^^^^^^^^^^^ +Para medir el tiempo total de ejecución se utiliza el comando +:manpage:`time(1)` con la especificación de formato ``%e``, siendo la medición +más realista porque incluye el tiempo de carga del ejecutable, inicialización +del *runtime* de D_ y del recolector. + +Todas las demás métricas se obtienen utilizando la salida generada por la +opción ``collect_stats_file`` (ver :ref:`sol_stats`), por lo que no pueden ser +medidos para TBGC. Sin embargo se espera que para esos casos los resultados no +sean muy distintos a CDGC utilizando la configuración **cons** (ver sección +anterior). + +Cabe destacar que las corridas para medir el tiempo total de ejecución no son +las mismas que al utilizar la opción ``collect_stats_file``; cuando se mide el +tiempo de ejecución no se utiliza esa opción porque impone un trabajo extra +importante y perturbaría demasiado la medición del tiempo. Sin embargo, los +tiempos medidos internamente al utilizar la opción ``collect_stats_file`` son +muy precisos, dado que se hace un particular esfuerzo para que no se haga un +trabajo extra mientras se está midiendo el tiempo. + +Al obtener el tiempo de *stop-the-world* se ignoran los apariciones del valor +``-1``, que indica que se solicitó una recolección pero que ya había otra en +curso, por lo que no se pausan los hilos realmente. Como tiempo de pausa real +(ver :ref:`sol_fork` para más detalles sobre la diferencia con el tiempo de +*stop-the-world*) se toma el valor del tiempo que llevó la asignación de +memoria que disparó la recolección. + +Para medir la cantidad de memoria máxima se calcula el valor máximo de la +sumatoria de: memoria usada, memoria libre, memoria desperdiciada y memoria +usada por el mismo recolector (es decir, el total de memoria pedida por el +programa al sistema operativo, aunque no toda este siendo utilizada por el +*mutator* realmente). + +Por último, la cantidad total de recolecciones realizadas se calcula contando +la cantidad de entradas del archivo generado por ``collect_stats_file``, +ignorando la cabecera y las filas cuyo valor de tiempo de *stop-the-world* es +``-1``, debido a que en ese caso no se disparó realmente una recolección dado +que ya había una en curso. + +Además, ciertas pruebas se corren variando la cantidad de procesadores +utilizados, para medir el impacto de la concurrencia en ambientes con un +procesador solo y con múltiples procesadores. Para esto se utiliza el comando +:manpage:`taskset`, que establece la *afinidad* de un proceso, *atándolo* +a correr en un cierto conjunto de procesadores. Si bien las pruebas se +realizan utilizando 1, 2, 3 y 4 procesadores, los resultados presentados en +general se limitan a 1 y 4 procesadores, ya que no se observan diferencias +sustanciales al utilizar 2 o 3 procesadores con respecto a usar 4 (solamente +se ven de forma más atenuadas las diferencias entre la utilización de +1 o 4 procesadores). Dado que de por sí ya son muchos los datos a procesar +y analizar, agregar más resultados que no aportan información valiosa termina +resultando contraproducente. + +En los casos donde se utilizan otro tipo de métricas para evaluar aspectos +particulares sobre alguna modificación se describe como se realiza la medición +donde se utiliza la métrica especial. + +Variabilidad de los resultados entre ejecuciones +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Es de esperarse que haya una cierta variación en los resultados entre +corridas, dada la indeterminación inherente a los sistemas operativos de +tiempo compartido, que compiten por los recursos de la computadora. + +Para minimizar esta variación se utilizan varias herramientas. En primer +lugar, se corren las pruebas estableciendo máxima prioridad (-19 en Linux_) al +proceso utilizando el comando :manpage:`nice(1)`. La variación en la +frecuencia del reloj los procesadores (para ahorrar energía) puede ser otra +fuente de variación, por lo que se usa el comando :manpage:`cpufreq-set(1)` +para establecer la máxima frecuencia disponible de manera fija. + +Sin embargo, a pesar de tomar estas precauciones, se sigue observando una +amplia variabilidad entre corridas. Además se observa una variación más +importante de la esperada no solo en el tiempo, también en el consumo de +memoria, lo que es más extraño. Esta variación se debe principalmente a que +Linux_ asigna el espacio de direcciones a los procesos con una componente +azarosa (por razones de seguridad). Además, por omisión, la llamada al sistema +:manpage:`mmap(2)` asigna direcciones de memoria altas primero, entregando +direcciones más bajas en llamadas subsiguientes [LWN90311]_. + +El comando :manpage:`setarch(8)` sirve para controlar éste y otros aspectos de +Linux_. La opción ``-L`` hace que se utilice un esquema de asignación de +direcciones antiguo, que no tiene una componente aleatoria y asigna primero +direcciones bajas. La opción ``-R`` solamente desactiva la componente azarosa +al momento de asignar direcciones. + +.. flt:: t:sol-setarch + :type: table + + Variación entre corridas para TBGC + + Variación entre corridas para TBGC. La medición está efectuada utilizando + los valores máximo, mínimo y media estadística de 20 corridas, utilizando + la siguiente métrica: :math:`\frac{max - min}{\mu}`. La medida podría + realizarse utilizando el desvío estándar en vez de la amplitud máxima, pero + en este cuadro se quiere ilustrar la variación máxima, no la típica. + + .. subflt:: + + Del tiempo total de ejecución. + + ======== ======== ======== ======== + Programa Normal ``-R`` ``-L`` + ======== ======== ======== ======== + bh 0.185 0.004 0.020 + bigarr 0.012 0.002 0.016 + bisort 0.006 0.003 0.006 + conalloc 0.004 0.004 0.004 + concpu 0.272 0.291 0.256 + dil 0.198 0.128 0.199 + em3d 0.006 0.033 0.029 + mcore 0.009 0.009 0.014 + rnddata 0.015 0.002 0.011 + sbtree 0.012 0.002 0.012 + split 0.025 0.000 0.004 + tsp 0.071 0.068 0.703 + voronoi 0.886 0.003 0.006 + ======== ======== ======== ======== + + .. subflt:: + + Del consumo máximo de memoria. + + ======== ======== ======== ======== + Programa Normal ``-R`` ``-L`` + ======== ======== ======== ======== + bh 0.001 0.000 0.001 + bigarr 0.001 0.000 0.001 + bisort 0.000 0.000 0.000 + conalloc 0.753 0.000 0.001 + concpu 0.002 0.000 0.001 + dil 0.055 0.028 0.013 + em3d 0.000 0.001 0.001 + mcore 0.447 0.482 0.460 + rnddata 0.000 0.000 0.000 + sbtree 0.000 0.000 0.000 + split 0.000 0.000 0.000 + tsp 0.000 0.001 0.000 + voronoi 0.001 0.000 0.000 + ======== ======== ======== ======== + +Ambas opciones, reducen notablemente la variación en los resultados (ver +cuadro :vref:`t:sol-setarch`). Esto probablemente se debe a la naturaleza +conservativa del recolector, dado que la probabilidad de tener *falsos +punteros* depende directamente de los valores de las direcciones de memoria, +aunque las pruebas en la que hay concurrencia involucrada, se siguen viendo +grandes variaciones, que probablemente estén vinculadas a problemas de +sincronización que se ven expuestos gracias al indeterminismo inherente a los +programas multi-hilo. + +Si bien se obtienen resultados más estables utilizando un esquema diferente al +utilizado por omisión, se decide no hacerlo dado que las mediciones serían +menos realistas. Los usuarios en general no usan esta opción y se presentaría +una visión más acotada sobre el comportamiento de los programas. Sin embargo, +para evaluar el este efecto en los resultados, siempre que sea posible se +analizan los resultados de un gran número de corridas observando +principalmente su mínima, media, máxima y desvío estándar. + + + +Resultados para pruebas sintizadas +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A continuación se presentan los resultados obtenidos para las pruebas +sintetizadas (ver :ref:`sol_bench_synth`). Se recuerda que este conjunto de +resultados es útil para analizar ciertos aspectos puntuales de las +modificaciones propuestas, pero en general distan mucho de como se comporta un +programa real, por lo que los resultados deben ser analizados teniendo esto +presente. + +.. flt:: fig:sol-bigarr-1cpu + + Resultados para ``bigarr`` (utilizando 1 procesador) + + Resultados para ``bigarr`` (utilizando 1 procesador). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-bigarr-1cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-bigarr-1cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-bigarr-1cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-bigarr-1cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-bigarr-1cpu.pdf + +.. flt:: fig:sol-bigarr-4cpu + + Resultados para ``bigarr`` (utilizando 4 procesadores) + + Resultados para ``bigarr`` (utilizando 4 procesadores). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-bigarr-4cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-bigarr-4cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-bigarr-4cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-bigarr-4cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-bigarr-4cpu.pdf + +.. flt:: fig:sol-concpu-1cpu + + Resultados para ``concpu`` (utilizando 1 procesador) + + Resultados para ``concpu`` (utilizando 1 procesador). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-concpu-1cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-concpu-1cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-concpu-1cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-concpu-1cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-concpu-1cpu.pdf + +.. flt:: fig:sol-concpu-4cpu + + Resultados para ``concpu`` (utilizando 4 procesadores) + + Resultados para ``concpu`` (utilizando 4 procesadores). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-concpu-4cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-concpu-4cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-concpu-4cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-concpu-4cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-concpu-4cpu.pdf + +.. flt:: fig:sol-conalloc-1cpu + + Resultados para ``conalloc`` (utilizando 1 procesador) + + Resultados para ``conalloc`` (utilizando 1 procesador). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-conalloc-1cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-conalloc-1cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-conalloc-1cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-conalloc-1cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-conalloc-1cpu.pdf + +.. flt:: fig:sol-conalloc-4cpu + + Resultados para ``conalloc`` (utilizando 4 procesadores) + + Resultados para ``conalloc`` (utilizando 4 procesadores). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-conalloc-4cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-conalloc-4cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-conalloc-4cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-conalloc-4cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-conalloc-4cpu.pdf + +.. flt:: fig:sol-split-1cpu + + Resultados para ``split`` (utilizando 1 procesador) + + Resultados para ``split`` (utilizando 1 procesador). Se presenta el mínimos + (en negro), la media centrada entre dos desvíos estándar (en gris), y el + máximo (en blanco) calculados sobre 50 corridas (para tiempo de ejecución) + o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-split-1cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-split-1cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-split-1cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-split-1cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-split-1cpu.pdf + +.. flt:: fig:sol-mcore-1cpu + + Resultados para ``mcore`` (utilizando 1 procesador) + + Resultados para ``mcore`` (utilizando 1 procesador). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-mcore-1cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-mcore-1cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-mcore-1cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-mcore-1cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-mcore-1cpu.pdf + +.. flt:: fig:sol-mcore-4cpu + + Resultados para ``mcore`` (utilizando 4 procesadores) + + Resultados para ``mcore`` (utilizando 4 procesadores). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-mcore-4cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-mcore-4cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-mcore-4cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-mcore-4cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-mcore-4cpu.pdf + +.. flt:: fig:sol-rnddata-1cpu + + Resultados para ``rnddata`` (utilizando 1 procesador) + + Resultados para ``rnddata`` (utilizando 1 procesador). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-rnddata-1cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-rnddata-1cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-rnddata-1cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-rnddata-1cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-rnddata-1cpu.pdf + +``bigarr`` +^^^^^^^^^^ +En la figura :vref:`fig:sol-bigarr-1cpu` se pueden observar los resultados +para ``bigarr`` al utilizar un solo procesador. En ella se puede notar que el +tiempo total de ejecución en general aumenta al utilizar CDGC, esto es +esperable, dado esta prueba se limitan a usar servicios del recolector. Dado +que esta ejecución utiliza solo un procesador y por lo tanto no se puede sacar +provecho a la concurrencia, es de esperarse que el trabajo extra realizado por +las modificaciones se vea reflejado en los resultados. En la +:vref:`fig:sol-bigarr-4cpu` (resultados al utilizar 4 procesadores) se puede +observar como al usar solamente *eager allocation* se recupera un poco el +tiempo de ejecución, probablemente debido al incremento en la concurrencia +(aunque no se observa el mismo efecto al usar *early collection*). + +Observando el tiempo total de ejecución, no se esperaba un incremento tan +notorio al pasar de TBGC a una configuración equivalente de CDGC **cons**, +haciendo un breve análisis de las posibles causas, lo más probable parece ser +el incremento en la complejidad de la fase de marcado dada capacidad para +marcar de forma precisa (aunque no se use la opción, se paga el precio de la +complejidad extra y sin obtener los beneficios). Además se puede observar +como el agregado de precisión al marcado mejora un poco las cosas (donde sí se +obtiene rédito de la complejidad extra en el marcado). + +En general se observa que al usar *eager allocation* el consumo de memoria +y los tiempos de pausa se disparan mientras que la cantidad de recolecciones +disminuye drásticamente. Lo que se observa es que el programa es +más veloz pidiendo memoria que recolectándola, por lo que crece mucho el +consumo de memoria. Como consecuencia la fase de barrido (que no corre en +paralelo al *mutator* como la fase de marcado) empieza a ser predominante en +el tiempo de pausa por ser tan grande la cantidad de memoria a barrer. Este +efecto se ve tanto al usar 1 como 4 procesadores, aunque el efecto es mucho +más nocivo al usar 1 debido a la alta variabilidad que impone la competencia +entre el *mutator* y recolector al correr de forma concurrente. + +Sin embargo, el tiempo de *stop-the-world* es siempre considerablemente más +pequeño al utilizar marcado concurrente en CDGC, incluso cuando se utiliza +*eager allocation*, aunque en este caso aumenta un poco, también debido al +incremento en el consumo de memoria, ya que el sistema operativo tiene que +copiar tablas de memoria más grandes al efectuar el *fork* (ver +:ref:`sol_fork`). + +``concpu`` +^^^^^^^^^^ +En la figura :vref:`fig:sol-concpu-1cpu` se pueden observar los resultados +para ``concpu`` al utilizar un solo procesador. En ella se aprecia que el +tiempo total de ejecución disminuye levemente al usar marcado concurrente +mientras no se utilice *eager allocation* pero aumenta al utilizarlo. + +Con respecto a la cantidad de recolecciones, uso máximo de memoria y tiempo de +*stop-the-world* se ve un efecto similar al descripto para ``bigarr`` (aunque +magnificado), pero sorprendentemente el tiempo total de pausa se dispara, +además con una variabilidad sorprendente, cuando se usa marcado concurrente +(pero no *eager allocation*). Una posible explicación podría ser que al +realizarse el *fork*, el sistema operativo muy probablemente entregue el +control del único procesador disponible al resto de los hilos que compiten por +él, por lo que queda mucho tiempo pausado en esa operación aunque realmente no +esté haciendo trabajo alguno (simplemente no tiene tiempo de procesador para +correr). Este efecto se cancela al usar *eager allocation* dado que el +*mutator* nunca se bloquea esperando que el proceso de marcado finalice. + +Además se observa una caída importante en la cantidad de recolecciones al +utilizar marcado concurrente. Esto probablemente se deba a que solo un hilo +pide memoria (y por lo tanto dispara recolecciones), mientras los demás hilos +también estén corriendo. Al pausarse todos los hilos por menos tiempo, el +trabajo se hace más rápido (lo que explica la disminución del tiempo total de +ejecución) y son necesarias menos recolecciones, por terminar más rápido +también el hilo que las dispara. + +En la :vref:`fig:sol-concpu-4cpu` se pueden ver los resultados al utilizar +4 procesadores, donde el panorama cambia sustancialmente. El efecto mencionado +en el párrafo anterior no se observa más (pues el sistema operativo tiene más +procesadores para asignar a los hilos) pero todos los resultados se vuelven +más variables. Los tiempos de *stop-the-world* y pausa real (salvo por lo +recién mencionado) crecen notablemente, al igual que su variación. No se +encuentra una razón evidente para esto; podría ser un error en la medición +dado que al utilizar todos los procesadores disponibles del *hardware*, +cualquier otro proceso que compita por tiempo de procesador puede afectarla +más fácilmente. + +El tiempo total de ejecución crece considerablemente, como se espera, dado que +el programa aprovecha los múltiples hilos que pueden correr en paralelo en +procesadores diferentes. + +Sin embargo, no se encuentra una razón clara para explicar el crecimiento +dramático en la cantidad de recolecciones solo al no usar marcado concurrente +para 4 procesadores. + +``conalloc`` +^^^^^^^^^^^^ +En la figura :vref:`fig:sol-conalloc-1cpu` se pueden observar los resultados +para ``conalloc`` al utilizar un solo procesador. Los cambios con respecto +a lo observado para ``concpu`` son mínimos. El efecto de la mejoría al usar +marcado concurrente pero no *eager allocation* no se observa más, dado que +``conalloc`` pide memoria en todos los hilos, se crea un cuello de botella. Se +ve claramente como tampoco baja la cantidad de recolecciones hecha debido +a esto y se invierte la variabilidad entre los tiempos pico de pausa real +y *stop-the-world* (sin una razón obvia, pero probablemente relacionado que +todos los hilos piden memoria). + +Al utilizar 4 procesadores (figura :vref:`fig:sol-conalloc-4cpu`), más allá de +las diferencias mencionadas para 1 procesador, no se observan grandes cambios +con respecto a lo observado para ``concpu``, excepto que los tiempos de pausa +(real y *stop-the-world*) son notablemente más pequeños, lo que pareciera +confirmar un error en la medición de ``concpu``. + +``split`` +^^^^^^^^^ +Este es el primer caso donde se aprecia la sustancial mejora proporcionada por +una pequeña optimización, el caché de ``findSize()`` (ver +:ref:`sol_minor_findsize`). En la figura :vref:`fig:sol-split-1cpu` se puede +observar con claridad como, para cualquier configuración de CDGC, hay una +caída notable en el tiempo total de ejecución. Sin embargo, a excepción de +cuando se utiliza *eager allocation*, la cantidad de recolecciones y memoria +usada permanece igual. + +La utilización de *eager allocation* mejora (aunque de forma apenas +apreciable) el tiempo de ejecución, la cantidad de recolecciones baja a un +tercio y el tiempo de pausa real cae dramáticamente. Al usar marcado +concurrente ya se observa una caída determinante en el tiempo de +*stop-the-world*. Todo esto sin verse afectado el uso máximo de memoria, +incluso al usar *eager allocation*. + +Se omiten los resultados para más de un procesador por ser prácticamente +idénticos para este análisis. + +``mcore`` +^^^^^^^^^ +El caso de ``mcore`` es interesante por ser, funcionalmente, una combinación +entre ``concpu`` y ``split``, con un agregado extra: el incremento notable de +la competencia por utilizar el recolector entre los múltiples hilos. + +Los efectos observados (en la figura :vref:`fig:sol-mcore-1cpu` para +1 procesador y en la figura :vref:`fig:sol-mcore-4cpu` para 4) confirman esto, +al ser una suma de los efectos observados para ``concpu`` y ``split``, con el +agregado de una particularidad extra por la mencionada competencia entre +hilos. A diferencia de ``concpu`` donde el incremento de procesadores resulta +en un decremento en el tiempo total de ejecución, en este caso resulta en una +disminución, dado que se necesita mucha sincronización entre hilos, por +utilizar todos de forma intensiva los servicios del recolector (y por lo tanto +competir por su *lock* global). + +Otro efecto común observado es que cuando el tiempo de pausa es muy pequeño +(del orden de los milisegundos), el marcado concurrente suele incrementarlo en +vez de disminuirlo. + +``rnddata`` +^^^^^^^^^^^ +En la figura :vref:`fig:sol-rnddata-1cpu` se presentan los resultados para +``rnddata`` utilizando 1 procesador. Una vez más estamos ante un caso en el +cual se observa claramente la mejoría gracias a una modificación en particular +principalmente. En esta caso es el marcado preciso. Se puede ver claramente +como mejora el tiempo de total de ejecución a algo más que la mitad (en +promedio, aunque se observa una anomalía donde el tiempo baja hasta más de +3 veces). Sin embargo, a menos que se utilice *eager allocation* o *early +collection* (que en este caso prueba ser muy efectivo), la cantidad de +recolecciones aumenta considerablemente. + +La explicación puede ser hallada en el consumo de memoria, que baja unas +3 veces en promedio usando marcado preciso que además hace disminuir +drásticamente (unas 10 veces) el tiempo de pausa (real y *stop-the-world*). El +tiempo de *stop-the-world* disminuye unas 10 veces más al usar marcado +concurrente y el tiempo de pausa real al usar *eager allocation*, pero en este +caso el consumo de memoria aumenta también bastante (aunque no tanto como +disminuye el tiempo de pausa, por lo que puede ser un precio que valga la pena +pagar si se necesitan tiempos de pausa muy pequeños). + +El aumento en el variación de los tiempos de ejecución al usar marcado preciso +probablemente se debe a lo siguiente: con marcado conservativo, debe estar +sobreviviendo a las recolecciones el total de memoria pedida por el programa, +debido a falsos punteros (por eso no se observa prácticamente variación en el +tiempo de ejecución y memoria máxima consumida); al marcar con precisión +parcial, se logra disminuir mucho la cantidad de falsos punteros, pero el +*stack* y la memoria estática, se sigue marcado de forma conservativa, por lo +tanto dependiendo de los valores (aleatorios) generados por la prueba, aumenta +o disminuye la cantidad de falsos punteros, variando así la cantidad de +memoria consumida y el tiempo de ejecución. + +No se muestran los resultados para más de un procesador por ser demasiado +similares a los obtenidos utilizando solo uno. + +``sbtree`` +^^^^^^^^^^ +Los resultados para ``sbtree`` son tan similares a los obtenidos con +``bigarr`` que directamente se omiten por completo, dado que no aportan ningún +tipo de información nueva. Por un lado es esperable, dado que ambas pruebas se +limitan prácticamente a pedir memoria, la única diferencia es que una pide +objetos grandes y otra objetos pequeños, pero esta diferencia parece no +afectar la forma en la que se comportan los cambios introducidos en este +trabajo. + + +Resultados para pruebas pequeñas +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. flt:: fig:sol-bh-1cpu + + Resultados para ``bh`` (utilizando 1 procesador) + + Resultados para ``bh`` (utilizando 1 procesador). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-bh-1cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-bh-1cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-bh-1cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-bh-1cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-bh-1cpu.pdf + +A continuación se presentan los resultados obtenidos para las pruebas pequeñas +(ver :ref:`sol_bench_small`). Se recuerda que si bien este conjunto de pruebas +se compone de programas reales, que efectúan una tarea útil, están diseñados +para ejercitar la asignación de memoria y que no son recomendados para evaluar +el desempeño de recolectores de basura. Sin embargo se las utiliza igual por +falta de programas más realistas, por lo que hay que tomarlas como un grado de +suspicacia. + +``bh`` +^^^^^^ +.. flt:: t:sol-prec-mem-bh + :type: table + + Memoria pedida y asignada para ``bh`` según modo de marcado + + Memoria pedida y asignada para ``bh`` según modo de marcado conservativo + o preciso (acumulativo durante toda la vida del programa). + + ============== ============== ============== ================= + Memoria Pedida (MiB) Asignada (MiB) Desperdicio (MiB) + ============== ============== ============== ================= + Conservativo 302.54 354.56 52.02 (15%) + Preciso 302.54 472.26 169.72 (36%) + ============== ============== ============== ================= + +En la figura :vref:`fig:sol-bh-1cpu` se pueden observar los resultados +para ``bh`` al utilizar un solo procesador. Ya en una prueba un poco más +realista se puede observar el efecto positivo del marcado preciso, en especial +en la cantidad de recolecciones efectuadas (aunque no se traduzca en un menor +consumo de memoria). + +.. flt:: fig:sol-bisort-1cpu + + Resultados para ``bisort`` (utilizando 1 procesador) + + Resultados para ``bisort`` (utilizando 1 procesador). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-bisort-1cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-bisort-1cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-bisort-1cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-bisort-1cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-bisort-1cpu.pdf + +Sin embargo se observa también un efecto nocivo del marcado preciso en el +consumo de memoria que intuitivamente debería disminuir, pero crece, y de +forma considerable (unas 3 veces en promedio). La razón de esta particularidad +es el incremento en el espacio necesario para almacenar objetos debido a que +el puntero a la información del tipo se guarda al final del bloque (ver +:ref:`sol_precise`). En el cuadro :vref:`t:sol-prec-mem-bh` se puede observar +la cantidad de memoria pedida por el programa, la cantidad de memoria +realmente asignada por el recolector (y la memoria desperdiciada) cuando se +usa marcado conservativo y preciso. Estos valores fueron tomados usando la +opción ``malloc_stats_file`` (ver :ref:`sol_stats`). + +.. flt:: fig:sol-em3d-1cpu + + Resultados para ``em3d`` (utilizando 1 procesador) + + Resultados para ``em3d`` (utilizando 1 procesador). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-em3d-1cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-em3d-1cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-em3d-1cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-em3d-1cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-em3d-1cpu.pdf + +Más allá de esto, los resultados son muy similares a los obtenidos para +pruebas sintetizadas que se limitan a ejercitar el recolector (como ``bigarr`` +y ``sbtree``), lo que habla de lo mucho que también lo hace este pequeño +programa. + +No se muestran los resultados para más de un procesador por ser extremadamente +similares a los obtenidos utilizando solo uno. + +``bisort`` +^^^^^^^^^^ +La figura :vref:`fig:sol-bisort-1cpu` muestra los resultados para ``bisort`` +al utilizar 1 procesador. En este caso el parecido es con los resultados para +la prueba sintetizada ``split``, con la diferencia que el tiempo de ejecución +total prácticamente no varía entre TBGC y CDGC, ni entre las diferentes +configuraciones del último (evidentemente en este caso no se aprovecha el +caché de ``findSize()``). + +.. flt:: fig:sol-tsp-1cpu + + Resultados para ``tsp`` (utilizando 1 procesador) + + Resultados para ``tsp`` (utilizando 1 procesador). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-tsp-1cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-tsp-1cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-tsp-1cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-tsp-1cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-tsp-1cpu.pdf + +Otra diferencia notable es la considerable reducción del tiempo de pausa real +al utilizar *early collection* (más de 3 veces menor en promedio comparado +a cuando se marca de forma conservativa, y más de 2 veces menor que cuando se +hace de forma precisa), lo que indica que la predicción de cuando se va +a necesitar una recolección es más efectiva que para ``split``. + +No se muestran los resultados para más de un procesador por ser extremadamente +similares a los obtenidos utilizando solo uno. + +``em3d`` +^^^^^^^^ +Los resultados para ``em3d`` (figura :vref:`fig:sol-em3d-1cpu`) son +sorprendentemente similares a los de ``bisort``. La única diferencia es que en +este caso el marcado preciso y el uso de *early collection** no parecen +ayudar; por el contrario, aumentan levemente el tiempo de pausa real. + +.. flt:: fig:sol-voronoi-1cpu + + Resultados para ``voronoi`` (utilizando 1 procesador) + + Resultados para ``voronoi`` (utilizando 1 procesador). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-voronoi-1cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-voronoi-1cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-voronoi-1cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-voronoi-1cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-voronoi-1cpu.pdf + +.. flt:: fig:sol-voronoi-4cpu + + Resultados para ``voronoi`` (utilizando 4 procesadores) + + Resultados para ``voronoi`` (utilizando 4 procesadores). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-voronoi-4cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-voronoi-4cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-voronoi-4cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-voronoi-4cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-voronoi-4cpu.pdf + +Una vez más no se muestran los resultados para más de un procesador por ser +extremadamente similares a los obtenidos utilizando solo uno. + +``tsp`` +^^^^^^^^ +Los resultados para ``tsp`` (figura :vref:`fig:sol-tsp-1cpu`) son +prácticamente idénticos a los de ``bisort``. La única diferencia es que la +reducción del tiempo de pausa real es un poco menor. + +Esto confirma en cierta medida la poca utilidad de este juego de pruebas para +medir el rendimiento de un recolector, dado que evidentemente, si bien todas +resuelven problemas diferentes, realizan todas el mismo tipo de trabajo. + +Una vez más no se muestran los resultados para más de un procesador por ser +extremadamente similares a los obtenidos utilizando solo uno. + +``voronoi`` +^^^^^^^^^^^ +En la figura :vref:`fig:sol-voronoi-1cpu` se presentan los resultados para +``voronoi``, probablemente la prueba más interesante de este conjunto de +pruebas pequeñas. + +Por un lado se puede observar una vez más como baja dramáticamente el tiempo +total de ejecución cuando se empieza a utilizar CDGC. Ya se ha visto que esto +es común en programas que se benefician del caché de ``findSize()``, pero en +este caso no parece provenir toda la ganancia solo de ese cambio, dado que +para TBGC se ve una variación entre los resultados muy grande que desaparece +al cambiar a CDGC, esto no puede ser explicado por esa optimización. En +general la disminución de la variación de los resultados hemos visto que está +asociada al incremento en la precisión en el marcado, dado que los falsos +punteros ponen una cuota de aleatoriedad importante. Pero este tampoco parece +ser el caso, ya que no se observan cambios apreciables al pasar a usar marcado +preciso. + +Lo que se observa en esta oportunidad es un caso patológico de un mal factor +de ocupación del *heap* (ver :ref:`sol_ocup`). Lo que muy probablemente está +sucediendo con TBGC es que luego de ejecutar una recolección, se libera muy +poco espacio, entonces luego de un par de asignaciones, es necesaria una nueva +recolección. En este caso es donde dificulta la tarea de analizar los +resultados la falta de métricas para TBGC, dado que no se pueden observar la +cantidad de recolecciones ni de consumo máximo de memoria. Sin embargo es +fácil corroborar esta teoría experimentalmente, gracias a la opción +``min_free``. Utilizando la ``min_free=0`` para emular el comportamiento de +TBGC (se recuerda que el valor por omisión es ``min_free=5``), se obtiene una +media de 4 segundos, mucho más parecida a lo obtenido para TBGC. + +Otra particularidad de esta prueba es que al utilizar *early collection* el +tiempo de pausa real aumenta notablemente al usar un procesador, mientras que +al usar 4 (ver figura :vref:`fig:sol-voronoi-4cpu` disminuye levemente (además +de otros cambios en el nivel de variación, pero en general las medias no +cambian). + +Resultados para pruebas reales +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. flt:: fig:sol-dil-1cpu + + Resultados para ``dil`` (utilizando 1 procesador) + + Resultados para ``dil`` (utilizando 1 procesador). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-dil-1cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-dil-1cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-dil-1cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-dil-1cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-dil-1cpu.pdf + +A continuación se presentan los resultados obtenidos para las pruebas reales +(ver :ref:`sol_bench_real`). Recordamos que solo se pudo halla un programa que +pueda ser utilizado a este fin, Dil_, y que el objetivo principal de este +trabajo se centra alrededor de obtener resultados positivos para este +programa, por lo que a pesar de ser una única prueba, se le presta particular +atención. + +``dil`` +^^^^^^^ +En la figura :vref:`fig:sol-dil-1cpu` se presentan los resultados para +``dil`` al utilizar un procesador. Una vez más vemos una mejoría inmediata del +tiempo total de ejecución al pasar de TBGC a CDGC, y una vez más se debe +principalmente al mal factor de ocupación del *heap* de TBGC, dado que +utilizando CDGC con la opción ``min_free=0`` se obtiene una media del orden de +los 80 segundos, bastante más alta que el tiempo obtenido para TBGC. + +.. flt:: fig:sol-dil-4cpu + + Resultados para ``dil`` (utilizando 4 procesadores) + + Resultados para ``dil`` (utilizando 4 procesadores). Se presenta el + mínimos (en negro), la media centrada entre dos desvíos estándar (en gris), + y el máximo (en blanco) calculados sobre 50 corridas (para tiempo de + ejecución) o 20 corridas (para el resto). + + .. subflt:: + + Tiempo de ejecución (seg) + + .. image:: plots/time-dil-4cpu.pdf + + .. subflt:: + + Cantidad de recolecciones + + .. image:: plots/ncol-dil-4cpu.pdf + + .. subflt:: + + Uso máximo de memoria (MiB) + + .. image:: plots/mem-dil-4cpu.pdf + + .. subflt:: + + *Stop-the-world* máximo (seg) + + .. image:: plots/stw-dil-4cpu.pdf + + .. subflt:: + + Pausa real máxima (seg) + + .. image:: plots/pause-dil-4cpu.pdf + +Sin embargo se observa un pequeño incremento del tiempo de ejecución al +introducir marcado preciso, y un incremento bastante más importante (de +alrededor del 30%) en el consumo máximo de memoria. Nuevamente, como pasa con +la prueba ``bh``, el efecto es probablemente producto del incremento en el +espacio necesario para almacenar objetos debido a que el puntero a la +información del tipo se guarda al final del bloque (ver :ref:`sol_precise`). +En el cuadro :vref:`t:sol-prec-mem-dil` se puede observar la diferencia de +memoria desperdiciada entre el modo conservativo y preciso. + +.. flt:: t:sol-prec-mem-dil + :type: table + + Memoria pedida y asignada para ``dil`` según modo de marcado + + Memoria pedida y asignada para ``dil`` según modo de marcado conservativo + o preciso (acumulativo durante toda la vida del programa). + + ============== ============== ============== ================= + Memoria Pedida (MiB) Asignada (MiB) Desperdicio (MiB) + ============== ============== ============== ================= + Conservativo 307.48 399.94 92.46 (23%) + Preciso 307.48 460.24 152.76 (33%) + ============== ============== ============== ================= + +El pequeño incremento en el tiempo total de ejecución podría estar dado por la +mayor probabilidad de tener *falsos punteros* debido al incremento del tamaño +del *heap*; se recuerda que el *stack* y memoria estática se siguen marcado de +forma conservativa, incluso en modo preciso. + +También se puede observar una gran disminución del tiempo total de ejecución +(cerca de un 60%, y más de un 200% comparado con TBGC) alrededor de la mitad) +al empezar a usar *eager allocation*, acompañado como es usual de una baja en +la cantidad de recolecciones realizadas (esta vez mayor, de más de 3 veces) +y de una caída drástica del tiempo de pausa real (alrededor de 40 veces más +pequeño); todo esto con un incremento marginal en el consumo total de memoria +(aproximadamente un 5%). En este caso el uso de *early collection* apenas +ayuda a bajar el tiempo de pausa real en un 20% en promedio aproximadamente. +El tiempo de *stop-the-world* cae dramáticamente al empezar a realizar la fase +de marcado de manera concurrente; es 200 veces más pequeño. + +Al utilizar 4 procesadores (ver figura :vref:`fig:sol-dil-4cpu`), hay algunos +pequeños cambios. El tiempo total de ejecución es reducido todavía más (un 20% +que cuando se usa 1 procesador) cuando se utiliza *eager allocation*. Además +al utilizar *early collection*, hay otra pequeña ganancia de alrededor del +10%, tanto para el tiempo total de ejecución como para el tiempo de pausa +real. + + +.. _sol_accept: + +Aceptación +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Los avances de este trabajo fueron comunicados regularmente a la comunidad de +D_ a través de un blog [LMTDGC]_ y del grupo de noticias de D_. Los +comentarios hechos sobre el primero son en general positivos y denotan una +buena recepción por parte de la comunidad a las modificaciones propuestas. + +Una vez agregado el marcado concurrente se hace un anuncio en el grupo de +noticias que también muestra buenos comentarios y aceptación, en particular +por parte de Sean Kelly, encargado de mantener el *runtime* de `D 2.0`_, que +comienza a trabajar en adaptar el recolector con idea de tal vez incluirlo en +el futuro [NGA19235]_. Poco después Sean Kelly publica una versión preliminar +de la adaptación en la lista de correos que coordina el desarrollo del +*runtime* de `D 2.0`_ [DRT117]_. +También se ha mostrado interés de incluirlo en Tango_, aunque no se han ha +comenzado aún con la adaptación, pero debería ser trivial dado que este +trabajo se desarrolla usando Tango_ (y el recolector está basado en el de +Tango_) [TT1997]_. +.. include:: links.rst -.. vim: set ts=3 sts=3 sw=3 et tw=78 : +.. vim: set ts=3 sts=3 sw=3 et tw=78 spelllang=es :