最后更新:2026-10-13
C++ restrict 与编译优化¶
在 review ClickHouse 的一个 PR(ClickHouse#19946)时看到了 __restrict 的实战用法,顺手研究了一下:一个关键字能给编译器多大的优化空间?先说结论:在"编译器无法证明两个指针不别名"的热点循环里,restrict 能带来肉眼可见的提升。
1.背景:指针别名(aliasing)为什么拖慢代码¶
编译器做循环优化时,最怕的就是两个指针指向同一块内存(别名)。看下面这个经典场景:
如果 out 和 a 有别名,那么写 out[i] 可能改掉 a[i],编译器就不敢把读写重排、不敢用向量指令,只能老老实实一条条来。而 restrict 是程序员对编译器的**承诺**:通过这个指针访问的内存,不会与其他指针别名。承诺了,编译器就敢放开手脚。
2.实测:aggregate 计算热点¶
下面这个 benchmark 模拟的是数据库聚合计算的热点路径(ClickHouse PR 的简化版),两个版本唯一区别是 __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++那些事