Saltar a contenido

Ú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:

void add(int* a, int* b, int* out, int n) {
  for (int i = 0; i < n; ++i) {
    out[i] = a[i] + b[i];
  }
}

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://quick-bench.com/q/ccjuWr_JcLozAfy3Gxf3uVu76Yk

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

Comentarios