Ccache o cómo recompilar rápido y sin dolor
En Recompila tu propio Kodi explico de pasada que el proyecto Kodi utiliza ccache [1] si está disponible en el sistema.
[1] | Ccache significa "Compiler Cache". |
Ccache intercepta las llamadas al compilador de C y C++. Ccache comprueba sus ficheros de entrada expandidos (con las sustituciones de macros, variables de entorno, includes y demás ya sustituidos) y comprueba si ya tiene en su caché el fichero resultante de aplicar ese comando a esos ficheros de entrada. Si no es así, ejecuta el comando y se guarda una copia del resultado en su caché. Si ya teníamos el resultado en caché, nos ahorramos ejecutar el comando y ccache proporciona el resultado directamente.
Dado que compilar un fichero es un proceso costoso, poder ahorrárselo si no hay cambios respecto a una compilación anterior resulta muy conveniente.
Por ejemplo, compilar Kodi entero, pero teniendo una compilación previa relativamente cercana, necesita 36 minutos en mi portátil (viejo y cargado de actividad, eso sí). Compilarlo desde cero sin ccache necesita casi toda una jornada laboral.
Las estadísticas de ccache tras este proceso son interesantes:
$ ccache -s cache directory /home/jcea/.ccache primary config /home/jcea/.ccache/ccache.conf secondary config (readonly) /etc/ccache.conf cache hit (direct) 3021 cache hit (preprocessed) 0 cache miss 255 called for link 430 called for preprocessing 43 compile failed 8 preprocessor error 54 no input file 30 files in cache 85479 cache size 4.4 GB max cache size 5.0 GB
Aquí vemos que tengo configurados 5 GB de caché, de los cuales estoy usando 4.4 GB en 85479 ficheros. Antes de compilar Kodi he borrado las estadísticas, así que lo que se ve es el reflejo de esta compilación: la efectividad de la caché es superior al 92% (3021 / (3021 + 255)). Si todo el código se compilase a la misma velocidad, usando ccache aumentamos la velocidad de compilación del proyecto más de diez veces.
La efectividad de ccache depende de muchas cosas: Si actualizamos el compilador o alguno de los ficheros include usados en todo el proyecto, la efectividad de la caché será baja o nula y habrá que recompilarlo todo desde cero, pagando un pequeño coste adicional para las búsquedas y actualizaciones de la caché. Por el contrario, si compilamos un proyecto, hacemos luego un pequeño cambio y volvemos a recompilarlo entero, la ganancia será brutal.
Como ejemplo, voy a compilar Python 3.7 con ccache:
$ ccache -z # Borramos las estadísticas de ccache Statistics cleared $ cd /tmp $ hg clone ~/hg/python/cpython-priscine cpython ... $ cd cpython $ hg up -r 3.7 3746 files updated, 0 files merged, 2123 files removed, 0 files unresolved (activating bookmark 3.7) $ ./configure --enable-shared ... $ time make ... real 4m5.365s user 4m1.240s sys 0m15.472s $ ccache -s cache directory /home/jcea/.ccache primary config /home/jcea/.ccache/ccache.conf secondary config (readonly) /etc/ccache.conf cache hit (direct) 92 cache hit (preprocessed) 9 cache miss 309 called for link 75 called for preprocessing 91 compile failed 23 preprocessor error 28 bad compiler arguments 1 autoconf compile/link 277 no input file 9 files in cache 85758 cache size 4.4 GB max cache size 5.0 GB
Vemos que he tardado poco más de cuatro minutos en compilar Python 3.7 y que la efectividad de ccache es de un miserable 25%. Es decir, solo uno de cada cuatro compilaciones estaba en caché. Si limpiamos la compilación y repetimos el proceso, obtenemos:
$ make distclean ... $ ccache -z Statistics cleared $ ./configure --enable-shared ... $ time make ... real 0m26.552s user 0m6.996s sys 0m3.604s $ ccache -s cache directory /home/jcea/.ccache primary config /home/jcea/.ccache/ccache.conf secondary config (readonly) /etc/ccache.conf cache hit (direct) 408 cache hit (preprocessed) 0 cache miss 2 called for link 75 called for preprocessing 91 compile failed 23 preprocessor error 28 bad compiler arguments 1 autoconf compile/link 277 no input file 9 files in cache 85761 cache size 4.4 GB max cache size 5.0 GB
Hemos pasado de 4 minutos a 26 segundos (7 segundos de tiempo de CPU, en realidad, si mi portátil no estuviese saturado) y ccache tiene ahora una efectividad del 99.5%.
Ahora que hemos visto que usar ccache puede suponer una ganancia de tiempo considerable, ¿cómo debemos hacer para usar ccache a la hora de compilar código?.
-
Se puede instalar ccache reemplazando los compiladores de C y C++ del sistema. Esta opción no me gusta porque me parece intrusiva y si tenemos problemas de compatibilidad o similares, podemos vernos en una situación complicada de resolver. En el caso de optar por ella, el uso de ccache será transparente.
Ccache no es compatible con todo. Por ejemplo, tiene problemas con las precompiled headers, así que reemplazar los compiladores de C y C++ por completo nos dará problemas complejos de resolver tarde o temprano.
-
Hay proyectos como Kodi que detectan la presencia de ccache en el sistema y lo utilizan de forma automática. Esto es muy conveniente, pero lo cierto es que es poco habitual.
-
Otros proyectos se pueden compilar con ccache configurando correctamente las variables de entorno. Por ejemplo:
$ CC="ccache gcc" CXX="ccache g++" ./configure
Este comando configuraría un proyecto intentando usar ccache como compilador de C y de C++.
Hasta hace poco era mi forma habitual de trabajar.
-
Existen proyectos que no obedecen esas variables de entorno o, como Python, las opciones de compilación del intérprete se utilizan también para compilar módulos dinámicos futuros y no queremos dejar rastros de ccache por ahí que nos muerdan el culo dentro de tres años. En estos casos una solución simple y muy efectiva es usar la interposición.
Por ejemplo, si instalamos el paquete estándar ccache en Debian o Ubuntu, se creará un directorio /usr/lib/ccache con el siguiente contenido (Ubuntu 16.04):
$ ls -la /usr/lib/ccache/ total 39 drwxr-xr-x 2 root root 14 Jul 27 2017 . drwxr-xr-x 147 root root 504 May 30 02:45 .. lrwxrwxrwx 1 root root 16 Jul 27 2017 c++ -> ../../bin/ccache lrwxrwxrwx 1 root root 16 Jul 27 2017 c89-gcc -> ../../bin/ccache lrwxrwxrwx 1 root root 16 Jul 27 2017 c99-gcc -> ../../bin/ccache lrwxrwxrwx 1 root root 16 Jul 27 2017 cc -> ../../bin/ccache lrwxrwxrwx 1 root root 16 Jul 27 2017 g++ -> ../../bin/ccache lrwxrwxrwx 1 root root 16 Jul 27 2017 g++-5 -> ../../bin/ccache lrwxrwxrwx 1 root root 16 Jul 27 2017 gcc -> ../../bin/ccache lrwxrwxrwx 1 root root 16 Jul 27 2017 gcc-5 -> ../../bin/ccache lrwxrwxrwx 1 root root 16 Jul 27 2017 x86_64-linux-gnu-g++ -> ../../bin/ccache lrwxrwxrwx 1 root root 16 Jul 27 2017 x86_64-linux-gnu-g++-5 -> ../../bin/ccache lrwxrwxrwx 1 root root 16 Jul 27 2017 x86_64-linux-gnu-gcc -> ../../bin/ccache lrwxrwxrwx 1 root root 16 Jul 27 2017 x86_64-linux-gnu-gcc-5 -> ../../bin/ccache
En otras palabras, si metemos ese directorio en nuestro PATH de búsqueda de comandos, cuando se invoquen los comandos gcc o g++, por ejemplo, se invocará ccache de forma transparente. Basta con ejecutar en el terminal algo similar a:
$ export PATH=/usr/lib/ccache/:$PATH
Este comando modifica la variable de entorno PATH para que busque primero en el directorio /usr/lib/ccache. Basta con que ejecutemos ese comando en el terminal en el que vamos a ejecutar código, no afectará a nada más del ordenador y esa configuración desaparecerá una vez que cerremos esa terminal de trabajo.
Creo que esta opción es la más transparente y la que recomiendo si no te importa ser consciente y recordar activar esta configuración cuando la necesites.
Esta configuración está disponible en las instalaciones estándar de Ubuntu y Debian. Una instalación desde código fuente no genera este directorio. Os recomiendo encarecidamente que hagáis algo del estilo de:
# mkdir /usr/local/ccache/ # cd /usr/local/ccache/ # ln -s /usr/local/bin/ccache gcc # ln -s /usr/local/bin/ccache cc # ln -s /usr/local/bin/ccache g++ # ln -s /usr/local/bin/ccache c++
Si en el futuro queremos compilar algo usando ccache, simplemente escribimos en esa terminal:
$ export PATH=/usr/local/ccache/:$PATH
Hay unas cuantas cosas más a tener en cuenta a la hora de sacar el máximo partido de ccache. Estúdialas con atención. Por ejemplo, por defecto la ruta de los ficheros a compilar se tiene en cuenta a la hora de que ccache busque el resultado en su caché.