Última actualización:2026-10-12
C++ restrict y optimización del compilador¶
Al revisar un PR de ClickHouse (ClickHouse#19946) vi un uso real de __restrict y me puse a estudiarlo: ¿cuánto margen de optimización puede darle una sola palabra clave al compilador? Conclusión primero: en bucles calientes donde «el compilador no puede demostrar que dos punteros no se solapan», restrict aporta una mejora perfectamente visible.
1. Contexto: por qué el aliasing de punteros ralentiza el código¶
Al optimizar bucles, lo que más teme el compilador es que dos punteros apunten a la misma memoria (aliasing). Mira este escenario clásico:
Si out y a se solapan, escribir out[i] puede modificar a[i], así que el compilador no se atreve a reordenar lecturas y escrituras ni a usar instrucciones vectoriales, y solo puede ir instrucción por instrucción, honestamente. Y restrict es la promesa del programador al compilador: la memoria accedida a través de este puntero no se solapará con ningún otro puntero. Hecha la promesa, el compilador puede soltarse.
2. Medición: el punto caliente del cálculo aggregate¶
El benchmark siguiente simula la ruta caliente de un cálculo de agregación de base de datos (versión simplificada del PR de ClickHouse); la única diferencia entre las dos versiones es __restrict:
#include <vector>
#include <memory>
#include <benchmark/benchmark.h>
#define LEN 1<<20
using AggregateDataPtr = char *;
struct AvgState {
uint64_t numerator{0};
uint64_t denominator{0};
double divide() { return numerator / denominator; }
};
class Column {
public:
std::vector<uint64_t> & data() {return data_; }
private:
std::vector<uint64_t> data_;
};
template <class Derived>
class IAggregate {
public:
void addBatch(size_t batch_size, AggregateDataPtr state, Column *args) __attribute__((noinline)) {
for (size_t i = 0; i < batch_size; ++i) {
static_cast<const Derived *>(this)->add(state, args, i);
}
}
void addBatchWithoutOpt(size_t batch_size, AggregateDataPtr state, Column *args) __attribute__((noinline)) {
for (size_t i = 0; i < batch_size; ++i) {
static_cast<const Derived *>(this)->addWithoutOpt(state, args, i);
}
}
};
class AvgAggregator final : public IAggregate<AvgAggregator> {
public:
static AvgState& data(AggregateDataPtr __restrict place) { return *reinterpret_cast<AvgState *>(place); }
static AvgState& data_without_opt(AggregateDataPtr place) { return *reinterpret_cast<AvgState *>(place); }
void add(AggregateDataPtr __restrict state, Column * args, uint64_t row_num) const {
data(state).numerator += args[0].data()[row_num];
++(data(state).denominator);
}
void addWithoutOpt(AggregateDataPtr state, Column * args, uint64_t row_num) const {
data_without_opt(state).numerator += args[0].data()[row_num];
++(data_without_opt(state).denominator);
}
};
static void BM_loop_with_optimize(benchmark::State& state) {
AvgAggregator avgAggregator;
Column column;
for (size_t i = 0 ; i < LEN; i++) {
column.data().push_back(i);
}
Column arg0[] = {column};
// Code inside this loop is measured repeatedly
for(auto _ : state) {
auto avgState = std::make_unique<AvgState>();
avgAggregator.addBatch(LEN, reinterpret_cast<AggregateDataPtr>(avgState.get()), arg0);
}
}
static void BM_loop_without_optimize(benchmark::State& state) {
AvgAggregator avgAggregator;
Column column;
for (size_t i = 0 ; i < LEN; i++) {
column.data().push_back(i);
}
Column arg0[] = {column};
// Code inside this loop is measured repeatedly
for(auto _ : state) {
auto avgState = std::make_unique<AvgState>();
avgAggregator.addBatchWithoutOpt(LEN, reinterpret_cast<AggregateDataPtr>(avgState.get()), arg0);
}
}
BENCHMARK(BM_loop_without_optimize);
BENCHMARK(BM_loop_with_optimize);
https://github.com/ClickHouse/ClickHouse/pull/19946
3. Resultados y resumen¶
(1) la versión con __restrict (BM_loop_with_optimize) es claramente más rápida: con los dos punteros de estado numerator/denominator anotados, el compilador los mantiene en registros y el bucle se vectoriza por completo; la versión sin anotar recarga desde memoria en cada pasada;
(2) restrict hoy no es una palabra clave estándar de C++; el enfoque mayoritario son las extensiones del compilador: GCC/Clang usan __restrict (o __restrict__), MSVC usa __restrict, y C++26 está discutiendo su estandarización (las propuestas std::indirect/no_alias);
(3) aviso: restrict es una promesa sin seguro; si hay aliasing, es comportamiento indefinido. Úsalo solo en rutas calientes donde puedas demostrar que no hay aliasing (por ejemplo, bloques de estado independientes devueltos por un asignador).
Resumen en una frase: si en un bucle caliente hay lecturas/escrituras por varios punteros y de verdad no hay aliasing, __restrict es una optimización de coste-beneficio excepcional. El código que lo acompaña está en el benchmark de arriba; si te interesa, córrelo tú mismo en quick-bench.
Temas relacionados¶
- Funciones virtuales puras y clases abstractas en C++ | C++那些事
- Aserciones assert en C++ | C++那些事
- Campos de bits en C++ | C++那些事
- Implementar el polimorfismo de C++ en lenguaje C | C++那些事
- const en C++: constantes y modificación | C++那些事
- La familia completa de const en C++ | C++那些事
- El operador de desreferencia * en C++ | C++那些事
- Inicialización y asignación en C++ | C++那些事
- Formas de devolver varios valores en C++ | C++那些事
- La palabra clave mutable en C++ | C++那些事