Última atualização:2026-10-12
C++ restrict e otimização do compilador¶
Ao revisar um PR do ClickHouse (ClickHouse#19946) vi um uso prático de __restrict e fui estudar o assunto: quanto espaço de otimização uma única palavra-chave pode dar ao compilador? A conclusão primeiro: em loops quentes onde «o compilador não consegue provar que dois ponteiros não se sobrepõem», o restrict traz um ganho visível a olho nu.
1. Contexto: por que o aliasing de ponteiros deixa o código lento¶
Ao otimizar loops, o que o compilador mais teme é dois ponteiros apontando para a mesma memória (aliasing). Veja este cenário clássico:
Se out e a se sobrepõem, escrever em out[i] pode alterar a[i]; então o compilador não se atreve a reordenar leituras e escritas nem a usar instruções vetoriais, e só consegue seguir honestamente uma instrução por vez. Já o restrict é a promessa do programador ao compilador: a memória acessada por este ponteiro não se sobrepõe à de nenhum outro ponteiro. Prometido isso, o compilador pode se soltar.
2. Medição: o ponto quente do cálculo aggregate¶
O benchmark abaixo simula o caminho quente de um cálculo de agregação de banco de dados (versão simplificada do PR do ClickHouse); a única diferença entre as duas versões é o __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 e resumo¶
(1) a versão com __restrict (BM_loop_with_optimize) é claramente mais rápida: com os dois ponteiros de estado numerator/denominator anotados, o compilador os mantém em registradores e o loop se vetoriza por completo; a versão sem anotação recarrega da memória toda vez;
(2) restrict hoje não é uma palavra-chave padrão do C++; a abordagem predominante são extensões de compilador: GCC/Clang usam __restrict (ou __restrict__), o MSVC usa __restrict, e o C++26 discute a padronização (as propostas std::indirect/no_alias);
(3) aviso: restrict é uma promessa sem seguro — se houver aliasing, é comportamento indefinido. Use só em caminhos quentes onde você consiga provar que não há aliasing (por exemplo, blocos de estado independentes devolvidos por um alocador).
Resumo em uma frase: num loop quente com leituras e escritas por vários ponteiros e realmente sem aliasing, __restrict é uma otimização de custo-benefício altíssimo. O código que acompanha está no benchmark acima; se tiver interesse, rode você mesmo no quick-bench.
Tópicos relacionados¶
- Funções virtuais puras e classes abstratas em C++ | C++那些事
- Asserções com assert em C++ | C++那些事
- Bit fields em C++ | C++那些事
- Implementando polimorfismo de C++ em linguagem C | C++那些事
- Constantes e modificadores com const em C++ | C++那些事
- A família completa do const em C++ | C++那些事
- O operador de desreferência * em C++ | C++那些事
- Inicialização e atribuição em C++ | C++那些事
- Formas de retornar múltiplos valores em C++ | C++那些事
- A palavra-chave mutable em C++ | C++那些事