[This article was first published on R | Dr Tom Palmer, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.IntroductionIn my previous job my work computer was a Windows desktop – yes, those were the days before laptops and hotdesking!My PhD student was interested in Bayesian methods and we put together an R package which included some Stan models. I was always frustrated by how slowly these compiled on our Windows machines. A few years later, when I got a MacBook Air I was shocked how much faster they compiled.On my Windows machine our mrbayes package takes 3 minutes 55 seconds to compile and install. On my M4 MacBook Air it takes 1 minute 16 seconds.The following tips show how to improve those timings.To generate the timings I usedtime R CMD INSTALL --preclean .Big win 1: Enable parallel compilations with the MAKEFLAGS environment variableSet the MAKEFLAGS environment variable in your ~/.Renviron file. This controls how many make jobs run concurrently. Choose a number no larger than the number of processing cores your machine has. To find this run# Windows - in a Git Bash shellecho $NUMBER_OF_PROCESSORS# macOSsysctl -n hw.logicalcpu# Ubuntu LinuxnprocA reasonable starting point is your core count, or a few fewer to leave headroom for whatever else you’re doing during a compilation. For example,# In ~/.RenvironMAKEFLAGS=-j6Close and restart R/RStudio after making this change.On my Windows machine this reduced the build from 3:55 to 1:15. To find your own sweet spot empirically, see the example at the end of Big win 2.Big win 2: Enable C/C++ compiler cache using ccacheInstall ccache, I find it easiest to use a package manager, e.g.,# macOSbrew install ccache# Ubuntu/Debian Linuxapt install ccache# Windowswinget install ccacheWhichever installation method you use make sure ccache is on your PATH after installation. You can test with, say,ccache --versionTo enable ccache, on macOS and Linux this goes in ~/.R/Makevars; on Windows it’s ~/.R/Makevars.win (create the directory and file if they don’t exist), set# macOSCC = ccache clangCXX = ccache clang++CXX17 = ccache clang++# Windows and Linux# Most Linux users will be on gcc by default# Change to clang if you're using thatCC = ccache gccCXX = ccache g++CXX17 = ccache g++After a first compilation run for the cache to be generated, subsequent compilations are much faster.Windows, second compilation: 18 secondsM4 MacBook Air, second compilation: 5 secondsPerhaps more importantly, if, say, your package has 5 models and you only amend the code for one of them, ccache knows to use the cache for the 4 unchanged models.Windows, second compilation, only 1 model edited: 1 minute 10 secondsM4 MacBook Air, second compilation, only 1 model edited: 19 secondsYou can verify ccache is working, by observing the timing decrease and by checking the output ofccache -sIt is also useful to zero the ccache statistics before a timing run withccache -zTesting which of your models takes the longest to compileHere’s a quick script to test which model takes the longest to compile. Save it as say test.sh at the top level of your repo and add ^test\.sh$ to your .Rbuildignore file (to avoid an R CMD check NOTE about unknown files at the top level).for model in inst/stan/*.stan; do cp "$model" "$model.bak" # Insert at the top of the file sed -i "1i // benchmark $(date +%s%N)" "$model" ccache -z SECONDS=0 R CMD INSTALL --preclean . >/dev/null 2>&1 echo "$(basename $model): ${SECONDS}s" ccache -s | grep -E "Hits|Misses" | head -2 mv "$model.bak" "$model"doneFinding your MAKEFLAGS sweet spotWith ccache installed you can now benchmark different -jN values cleanly (the ccache -C calls ensure each run is a cold compile, so you measure raw compilation cost rather than cache hits). You can increase the number sequence up to the number of processing cores your machine has.for j in 1 2 3 4 6 8 10; do ccache -C >/dev/null echo "=== -j$j ===" SECONDS=0 MAKEFLAGS=-j$j R CMD INSTALL --preclean . >/dev/null 2>&1 echo "elapsed: ${SECONDS}s"doneThe timings on my MacBook Air were=== -j1 ===elapsed: 76s=== -j2 ===elapsed: 48s=== -j3 ===elapsed: 35s=== -j4 ===elapsed: 36s=== -j6 ===elapsed: 27s=== -j8 ===elapsed: 27s=== -j10 ===elapsed: 28sMy MacBook Air has 10 cores, but only 4 of those are performance cores, so I settled on -j6 as that is where my timings plateaued — and it leaves headroom for me inevitably checking my email during a compilation.Big win 3: Combining these in GitHub Actions workflowsIn my .github/workflows/R-CMD-check.yaml I have steps for these speedups. Firstly, to set MAKEFLAGS. - name: Set parallel compilation flags (Linux and macOS) if: runner.os != 'Windows' shell: bash run: | NCPUS=$(nproc 2>/dev/null || sysctl -n hw.logicalcpu) echo "Detected ${NCPUS} processors" echo "MAKEFLAGS=-j${NCPUS}" >> ~/.Renviron - name: Set parallel compilation flags (Windows) if: runner.os == 'Windows' shell: pwsh run: | Write-Output "Detected $env:NUMBER_OF_PROCESSORS processors" Add-Content -Path "$HOME\.Renviron" -Value "MAKEFLAGS=-j$env:NUMBER_OF_PROCESSORS"You can also use ccache in GitHub Actions, as follows: # ccache speeds up Stan model compilation dramatically on warm cache. # Note: Windows support via ccache-action is documented as "probably works" # rather than fully stable; if it causes issues, scope this step to non-Windows. - name: Setup ccache uses: hendrikmuhs/ccache-action@v1.2.23 with: # Key invalidates when Stan models or DESCRIPTION change. # Older caches partially seed new ones via restore-keys. key: ccache-${{ matrix.config.os }}-R-${{ matrix.config.r }}-${{ hashFiles('inst/stan/**/*.stan', 'DESCRIPTION') }} restore-keys: | ccache-${{ matrix.config.os }}-R-${{ matrix.config.r }}- ccache-${{ matrix.config.os }}-R- max-size: "2G" - name: Configure R to use ccache (Linux and macOS) if: runner.os != 'Windows' shell: bash run: | mkdir -p ~/.R if [ "$RUNNER_OS" = "macOS" ]; then cat >> ~/.R/Makevars ~/.R/Makevars