R Code Optimization III: Hardware Utilization and Performance | Blas M. Benito, PhD
The third post in a four-part series on code optimization, covering vectorization, parallelization, and memory management techniques to maximize computational efficiency.
Now we’ll talk a bit about where the real performance gains happen: vectorization, parallelization, and memory management.
Hardware utilization refers to how code leverages computational resources. For example, vectorization and parallelization help us squeeze every last drop of juice from our CPUs, while in-place modification, object size pre-allocation, and on-demand data access are useful to manage memory usage.
Vectorization refers to the application of an operation to multiple elements simultaneously.
At the hardware level, vectorization is enabled by an architectural feature known as Single Instruction Multiple Data (SIMD). SIMD operations can, for example, sum 16 pairs of vector elements simultaneously within a single core, offering substantial speed-ups. However, only compiled languages (C, C++, Fortran, etc) can leverage SIMD instructions via specific compiler optimizations.
At the software level, many languages implement vectorized semantics. Think of adding two vectors b and c with the expression a = b + c. This abstraction makes code concise, and can also unlock performance gains in different ways. In compiled languages like Fortran, such expressions are typically optimized for SIMD vectorization
In interpreted languages like R, many vectorized functions are backed by compiled code. For instance, primitives like + are implemented as fast C loops, that may or may not be optimized for SIMD by the compiler (see the section R side: how can R possibly use SIMD? in this excellent StackOverflow answer for details). In contrast, matrix operations rely on blazing-fast matrix algebra backends such as BLAS and LAPACK, which explicitly exploit SIMD vectorization (and parallelization!).
However, it’s not uncommon to find vectorized semantics without performance gains. This is the case with R functions like apply(), lapply(), purrr::map(), and the likes, which are essentially loops in a trenchcoat.
By combining SIMD vectorization for raw performance with semantics-level vectorization for expressiveness, we maximize hardware utilization while keeping our code clean and efficient