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é.