Последнее обновление:2026-10-12
C++ restrict и оптимизации компилятора¶
Просматривая PR в ClickHouse (ClickHouse#19946), я увидел практическое применение __restrict и сразу взялся его изучать: сколько простора для оптимизации может дать компилятору одно ключевое слово? Сразу вывод: в горячих циклах, где «компилятор не может доказать, что два указателя не алиасятся», restrict даёт заметный на глаз прирост.
1. Предыстория: почему алиасинг указателей замедляет код¶
Занимаясь оптимизацией циклов, компилятор больше всего боится, что два указателя указывают на одну и ту же память (алиасинг). Взгляните на классический сценарий:
Если out и a алиасятся, запись в out[i] может изменить a[i] — тогда компилятор не решается переупорядочивать чтения и записи, не решается использовать векторные инструкции и честно тянет всё по одной. А restrict — это обещание программиста компилятору: память, к которой обращаются через этот указатель, не алиасится ни с какими другими указателями. Раз обещано — компилятор может развернуться в полную силу.
2. Замер: горячий участок агрегирования¶
Приведённый ниже benchmark имитирует горячий путь агрегирующих вычислений в СУБД (упрощённая версия PR из ClickHouse); единственное различие двух версий — __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. Результаты и итог¶
(1) версия с __restrict (BM_loop_with_optimize) заметно быстрее: когда два указателя состояния numerator/denominator помечены, компилятор держит их в регистрах, и цикл полностью векторизуется; версия без пометки каждый раз перезагружает данные из памяти;
(2) restrict пока не является стандартным ключевым словом C++; общепринятый вариант — расширения компиляторов: GCC/Clang используют __restrict (или __restrict__), MSVC — __restrict, а в C++26 идёт обсуждение стандартизации (предложения std::indirect/no_alias);
(3) предупреждение: restrict — это обещание без страховки; начнётся алиасинг — получите неопределённое поведение. Используйте его только на горячих путях, где отсутствие алиасинга можно доказать (например, независимые блоки состояния, возвращаемые аллокатором).
Резюме одной фразой: если в горячем цикле несколько указателей читают и пишут память, а алиасинга действительно нет, __restrict — крайне выгодная по соотношению цена/эффект оптимизация. Сопроводительный код — в benchmark выше; при желании прогоните его сами на quick-bench.
Похожие темы¶
- C++ чисто виртуальные функции и абстрактные классы | C++那些事
- C++ утверждения assert | C++那些事
- C++ битовые поля | C++那些事
- Реализация полиморфизма C++ на языке C | C++那些事
- C++ константы const и его модификация | C++那些事
- C++ всё семейство const | C++那些事
- C++ оператор разыменования * | C++那些事
- C++ инициализация и присваивание | C++那些事
- C++ возврат нескольких значений | C++那些事
- C++ ключевое слово mutable | C++那些事